/ concept-collection / dulcimer
Sign in
concept-collection / dulcimer
dulcimer / src / render / colormaps.ts
113 lines · 2.5 KBCodeBlameHistory
2 * Colormaps: each maps a normalized value in [0, 1] to [r, g, b] in [0, 255].
3 * Adapted from figpack's SphereEmbedding view (figpack_experimental).
4 */
6export type ColormapFunc = (t: number) => [number, number, number];
8const clamp01 = (t: number) => Math.max(0, Math.min(1, t));
10// Piecewise-linear interpolation through control points (r, g, b in 0-255)
11const makeInterpolated = (stops: [number, number, number][]): ColormapFunc => {
12 const n = stops.length;
13 return (t: number) => {
14 t = clamp01(t);
15 const x = t * (n - 1);
16 const i = Math.min(n - 2, Math.floor(x));
17 const f = x - i;
18 const a = stops[i];
19 const b = stops[i + 1];
20 return [
21 Math.round(a[0] + (b[0] - a[0]) * f),
22 Math.round(a[1] + (b[1] - a[1]) * f),
23 Math.round(a[2] + (b[2] - a[2]) * f),
24 ];
25 };
26};
28// Control points sampled from matplotlib colormaps
29const viridis = makeInterpolated([
30 [68, 1, 84],
31 [72, 40, 120],
32 [62, 74, 137],
33 [49, 104, 142],
34 [38, 130, 142],
35 [31, 158, 137],
36 [53, 183, 121],
37 [109, 205, 89],
38 [180, 222, 44],
39 [253, 231, 37],
40]);
42const plasma = makeInterpolated([
43 [13, 8, 135],
44 [84, 2, 163],
45 [139, 10, 165],
46 [185, 50, 137],
47 [219, 92, 104],
48 [244, 136, 73],
49 [254, 188, 43],
50 [240, 249, 33],
51]);
53const inferno = makeInterpolated([
54 [0, 0, 4],
55 [40, 11, 84],
56 [101, 21, 110],
57 [159, 42, 99],
58 [212, 72, 66],
59 [245, 125, 21],
60 [250, 193, 39],
61 [252, 255, 164],
62]);
64const coolwarm = makeInterpolated([
65 [59, 76, 192],
66 [124, 159, 249],
67 [192, 212, 245],
68 [242, 242, 242],
69 [245, 195, 157],
70 [222, 96, 77],
71 [180, 4, 38],
72]);
74// matplotlib's `seismic`: harder contrast about the middle than coolwarm, and
75// dark at both ends, which suits a wavefield whose interesting parts are the
76// extremes.
77const seismic = makeInterpolated([
78 [0, 0, 76],
79 [0, 0, 255],
80 [255, 255, 255],
81 [255, 0, 0],
82 [128, 0, 0],
83]);
85const jet = makeInterpolated([
86 [0, 0, 128],
87 [0, 0, 255],
88 [0, 255, 255],
89 [0, 255, 0],
90 [255, 255, 0],
91 [255, 0, 0],
92 [128, 0, 0],
93]);
95const grayscale: ColormapFunc = (t: number) => {
96 const v = Math.round(clamp01(t) * 255);
97 return [v, v, v];
98};
100/** Diverging maps first: the pressure field is signed and is drawn
101 * symmetrically about zero, so a map with a distinct middle is what makes
102 * the wavefronts read. */
103export const colormaps: Record<string, ColormapFunc> = {
104 coolwarm,
105 seismic,
106 grayscale,
107 viridis,
108 plasma,
109 inferno,
110 jet,
111};
113export const colormapNames = Object.keys(colormaps);
moveopenescclose