palette-shader 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1691 @@
1
+ import { ShaderMaterial as v, Scene as m, OrthographicCamera as k, WebGLRenderer as w, PlaneGeometry as y, Mesh as C, WebGLRenderTarget as R, LinearFilter as T, DoubleSide as H, PerspectiveCamera as P, BoxGeometry as B, NearestFilter as L, AmbientLight as $, DirectionalLight as V, MeshStandardMaterial as Y, Vector3 as x, CylinderGeometry as Z, SphereGeometry as K, Object3D as j, Color as I, InstancedMesh as z, DataTexture as X, RGBAFormat as J, FloatType as Q, ClampToEdgeWrapping as E } from "three";
2
+ const A = `// https://lygia.xyz/
3
+ float srgb2rgb(const in float v) { return (v < 0.04045) ? v * 0.0773993808 : pow((v + 0.055) * 0.947867298578199, 2.4); }
4
+ vec3 srgb2rgb(const in vec3 srgb) { return vec3(srgb2rgb(srgb.r), srgb2rgb(srgb.g), srgb2rgb(srgb.b)); }
5
+ vec4 srgb2rgb(const in vec4 srgb) { return vec4(srgb2rgb(srgb.rgb), srgb.a); }
6
+ `, O = `// Copyright(c) 2021 Björn Ottosson
7
+ //
8
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of
9
+ // this softwareand associated documentation files(the "Software"), to deal in
10
+ // the Software without restriction, including without limitation the rights to
11
+ // use, copy, modify, merge, publish, distribute, sublicense, and /or sell copies
12
+ // of the Software, and to permit persons to whom the Software is furnished to do
13
+ // so, subject to the following conditions :
14
+ // The above copyright noticeand this permission notice shall be included in all
15
+ // copies or substantial portions of the Software.
16
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
19
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ // SOFTWARE.
23
+
24
+ #define M_PI 3.1415926535897932384626433832795
25
+
26
+ float cbrt( float x )
27
+ {
28
+ return sign(x)*pow(abs(x),1.0f/3.0f);
29
+ }
30
+
31
+ float srgb_transfer_function(float a)
32
+ {
33
+ return .0031308f >= a ? 12.92f * a : 1.055f * pow(a, .4166666666666667f) - .055f;
34
+ }
35
+
36
+ float srgb_transfer_function_inv(float a)
37
+ {
38
+ return .04045f < a ? pow((a + .055f) / 1.055f, 2.4f) : a / 12.92f;
39
+ }
40
+
41
+ vec3 linear_srgb_to_oklab(vec3 c)
42
+ {
43
+ float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b;
44
+ float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b;
45
+ float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b;
46
+
47
+ float l_ = cbrt(l);
48
+ float m_ = cbrt(m);
49
+ float s_ = cbrt(s);
50
+
51
+ return vec3(
52
+ 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_,
53
+ 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_,
54
+ 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_
55
+ );
56
+ }
57
+
58
+ vec3 oklab_to_linear_srgb(vec3 c)
59
+ {
60
+ float l_ = c.x + 0.3963377774f * c.y + 0.2158037573f * c.z;
61
+ float m_ = c.x - 0.1055613458f * c.y - 0.0638541728f * c.z;
62
+ float s_ = c.x - 0.0894841775f * c.y - 1.2914855480f * c.z;
63
+
64
+ float l = l_ * l_ * l_;
65
+ float m = m_ * m_ * m_;
66
+ float s = s_ * s_ * s_;
67
+
68
+ return vec3(
69
+ +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
70
+ -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
71
+ -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s
72
+ );
73
+ }
74
+
75
+ // Finds the maximum saturation possible for a given hue that fits in sRGB
76
+ // Saturation here is defined as S = C/L
77
+ // a and b must be normalized so a^2 + b^2 == 1
78
+ float compute_max_saturation(float a, float b)
79
+ {
80
+ // Max saturation will be when one of r, g or b goes below zero.
81
+
82
+ // Select different coefficients depending on which component goes below zero first
83
+ float k0, k1, k2, k3, k4, wl, wm, ws;
84
+
85
+ if (-1.88170328f * a - 0.80936493f * b > 1.f)
86
+ {
87
+ // Red component
88
+ k0 = +1.19086277f; k1 = +1.76576728f; k2 = +0.59662641f; k3 = +0.75515197f; k4 = +0.56771245f;
89
+ wl = +4.0767416621f; wm = -3.3077115913f; ws = +0.2309699292f;
90
+ }
91
+ else if (1.81444104f * a - 1.19445276f * b > 1.f)
92
+ {
93
+ // Green component
94
+ k0 = +0.73956515f; k1 = -0.45954404f; k2 = +0.08285427f; k3 = +0.12541070f; k4 = +0.14503204f;
95
+ wl = -1.2684380046f; wm = +2.6097574011f; ws = -0.3413193965f;
96
+ }
97
+ else
98
+ {
99
+ // Blue component
100
+ k0 = +1.35733652f; k1 = -0.00915799f; k2 = -1.15130210f; k3 = -0.50559606f; k4 = +0.00692167f;
101
+ wl = -0.0041960863f; wm = -0.7034186147f; ws = +1.7076147010f;
102
+ }
103
+
104
+ // Approximate max saturation using a polynomial:
105
+ float S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b;
106
+
107
+ // Do one step Halley's method to get closer
108
+ // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
109
+ // this should be sufficient for most applications, otherwise do two/three steps
110
+
111
+ float k_l = +0.3963377774f * a + 0.2158037573f * b;
112
+ float k_m = -0.1055613458f * a - 0.0638541728f * b;
113
+ float k_s = -0.0894841775f * a - 1.2914855480f * b;
114
+
115
+ {
116
+ float l_ = 1.f + S * k_l;
117
+ float m_ = 1.f + S * k_m;
118
+ float s_ = 1.f + S * k_s;
119
+
120
+ float l = l_ * l_ * l_;
121
+ float m = m_ * m_ * m_;
122
+ float s = s_ * s_ * s_;
123
+
124
+ float l_dS = 3.f * k_l * l_ * l_;
125
+ float m_dS = 3.f * k_m * m_ * m_;
126
+ float s_dS = 3.f * k_s * s_ * s_;
127
+
128
+ float l_dS2 = 6.f * k_l * k_l * l_;
129
+ float m_dS2 = 6.f * k_m * k_m * m_;
130
+ float s_dS2 = 6.f * k_s * k_s * s_;
131
+
132
+ float f = wl * l + wm * m + ws * s;
133
+ float f1 = wl * l_dS + wm * m_dS + ws * s_dS;
134
+ float f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2;
135
+
136
+ S = S - f * f1 / (f1 * f1 - 0.5f * f * f2);
137
+ }
138
+
139
+ return S;
140
+ }
141
+
142
+ // finds L_cusp and C_cusp for a given hue
143
+ // a and b must be normalized so a^2 + b^2 == 1
144
+ vec2 find_cusp(float a, float b)
145
+ {
146
+ // First, find the maximum saturation (saturation S = C/L)
147
+ float S_cusp = compute_max_saturation(a, b);
148
+
149
+ // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
150
+ vec3 rgb_at_max = oklab_to_linear_srgb(vec3( 1, S_cusp * a, S_cusp * b ));
151
+ float L_cusp = cbrt(1.f / max(max(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b));
152
+ float C_cusp = L_cusp * S_cusp;
153
+
154
+ return vec2( L_cusp , C_cusp );
155
+ }
156
+
157
+ // Finds intersection of the line defined by
158
+ // L = L0 * (1 - t) + t * L1;
159
+ // C = t * C1;
160
+ // a and b must be normalized so a^2 + b^2 == 1
161
+ float find_gamut_intersection(float a, float b, float L1, float C1, float L0, vec2 cusp)
162
+ {
163
+ // Find the intersection for upper and lower half seprately
164
+ float t;
165
+ if (((L1 - L0) * cusp.y - (cusp.x - L0) * C1) <= 0.f)
166
+ {
167
+ // Lower half
168
+
169
+ t = cusp.y * L0 / (C1 * cusp.x + cusp.y * (L0 - L1));
170
+ }
171
+ else
172
+ {
173
+ // Upper half
174
+
175
+ // First intersect with triangle
176
+ t = cusp.y * (L0 - 1.f) / (C1 * (cusp.x - 1.f) + cusp.y * (L0 - L1));
177
+
178
+ // Then one step Halley's method
179
+ {
180
+ float dL = L1 - L0;
181
+ float dC = C1;
182
+
183
+ float k_l = +0.3963377774f * a + 0.2158037573f * b;
184
+ float k_m = -0.1055613458f * a - 0.0638541728f * b;
185
+ float k_s = -0.0894841775f * a - 1.2914855480f * b;
186
+
187
+ float l_dt = dL + dC * k_l;
188
+ float m_dt = dL + dC * k_m;
189
+ float s_dt = dL + dC * k_s;
190
+
191
+
192
+ // If higher accuracy is required, 2 or 3 iterations of the following block can be used:
193
+ {
194
+ float L = L0 * (1.f - t) + t * L1;
195
+ float C = t * C1;
196
+
197
+ float l_ = L + C * k_l;
198
+ float m_ = L + C * k_m;
199
+ float s_ = L + C * k_s;
200
+
201
+ float l = l_ * l_ * l_;
202
+ float m = m_ * m_ * m_;
203
+ float s = s_ * s_ * s_;
204
+
205
+ float ldt = 3.f * l_dt * l_ * l_;
206
+ float mdt = 3.f * m_dt * m_ * m_;
207
+ float sdt = 3.f * s_dt * s_ * s_;
208
+
209
+ float ldt2 = 6.f * l_dt * l_dt * l_;
210
+ float mdt2 = 6.f * m_dt * m_dt * m_;
211
+ float sdt2 = 6.f * s_dt * s_dt * s_;
212
+
213
+ float r = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s - 1.f;
214
+ float r1 = 4.0767416621f * ldt - 3.3077115913f * mdt + 0.2309699292f * sdt;
215
+ float r2 = 4.0767416621f * ldt2 - 3.3077115913f * mdt2 + 0.2309699292f * sdt2;
216
+
217
+ float u_r = r1 / (r1 * r1 - 0.5f * r * r2);
218
+ float t_r = -r * u_r;
219
+
220
+ float g = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s - 1.f;
221
+ float g1 = -1.2684380046f * ldt + 2.6097574011f * mdt - 0.3413193965f * sdt;
222
+ float g2 = -1.2684380046f * ldt2 + 2.6097574011f * mdt2 - 0.3413193965f * sdt2;
223
+
224
+ float u_g = g1 / (g1 * g1 - 0.5f * g * g2);
225
+ float t_g = -g * u_g;
226
+
227
+ float b = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s - 1.f;
228
+ float b1 = -0.0041960863f * ldt - 0.7034186147f * mdt + 1.7076147010f * sdt;
229
+ float b2 = -0.0041960863f * ldt2 - 0.7034186147f * mdt2 + 1.7076147010f * sdt2;
230
+
231
+ float u_b = b1 / (b1 * b1 - 0.5f * b * b2);
232
+ float t_b = -b * u_b;
233
+
234
+ t_r = u_r >= 0.f ? t_r : 10000.f;
235
+ t_g = u_g >= 0.f ? t_g : 10000.f;
236
+ t_b = u_b >= 0.f ? t_b : 10000.f;
237
+
238
+ t += min(t_r, min(t_g, t_b));
239
+ }
240
+ }
241
+ }
242
+
243
+ return t;
244
+ }
245
+
246
+ float find_gamut_intersection(float a, float b, float L1, float C1, float L0)
247
+ {
248
+ // Find the cusp of the gamut triangle
249
+ vec2 cusp = find_cusp(a, b);
250
+
251
+ return find_gamut_intersection(a, b, L1, C1, L0, cusp);
252
+ }
253
+
254
+ vec3 gamut_clip_preserve_chroma(vec3 rgb)
255
+ {
256
+ if (rgb.r < 1.f && rgb.g < 1.f && rgb.b < 1.f && rgb.r > 0.f && rgb.g > 0.f && rgb.b > 0.f)
257
+ return rgb;
258
+
259
+ vec3 lab = linear_srgb_to_oklab(rgb);
260
+
261
+ float L = lab.x;
262
+ float eps = 0.00001f;
263
+ float C = max(eps, sqrt(lab.y * lab.y + lab.z * lab.z));
264
+ float a_ = lab.y / C;
265
+ float b_ = lab.z / C;
266
+
267
+ float L0 = clamp(L, 0.f, 1.f);
268
+
269
+ float t = find_gamut_intersection(a_, b_, L, C, L0);
270
+ float L_clipped = L0 * (1.f - t) + t * L;
271
+ float C_clipped = t * C;
272
+
273
+ return oklab_to_linear_srgb(vec3( L_clipped, C_clipped * a_, C_clipped * b_ ));
274
+ }
275
+
276
+ vec3 gamut_clip_project_to_0_5(vec3 rgb)
277
+ {
278
+ if (rgb.r < 1.f && rgb.g < 1.f && rgb.b < 1.f && rgb.r > 0.f && rgb.g > 0.f && rgb.b > 0.f)
279
+ return rgb;
280
+
281
+ vec3 lab = linear_srgb_to_oklab(rgb);
282
+
283
+ float L = lab.x;
284
+ float eps = 0.00001f;
285
+ float C = max(eps, sqrt(lab.y * lab.y + lab.z * lab.z));
286
+ float a_ = lab.y / C;
287
+ float b_ = lab.z / C;
288
+
289
+ float L0 = 0.5;
290
+
291
+ float t = find_gamut_intersection(a_, b_, L, C, L0);
292
+ float L_clipped = L0 * (1.f - t) + t * L;
293
+ float C_clipped = t * C;
294
+
295
+ return oklab_to_linear_srgb(vec3( L_clipped, C_clipped * a_, C_clipped * b_ ));
296
+ }
297
+
298
+ vec3 gamut_clip_project_to_L_cusp(vec3 rgb)
299
+ {
300
+ if (rgb.r < 1.f && rgb.g < 1.f && rgb.b < 1.f && rgb.r > 0.f && rgb.g > 0.f && rgb.b > 0.f)
301
+ return rgb;
302
+
303
+ vec3 lab = linear_srgb_to_oklab(rgb);
304
+
305
+ float L = lab.x;
306
+ float eps = 0.00001f;
307
+ float C = max(eps, sqrt(lab.y * lab.y + lab.z * lab.z));
308
+ float a_ = lab.y / C;
309
+ float b_ = lab.z / C;
310
+
311
+ // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
312
+ vec2 cusp = find_cusp(a_, b_);
313
+
314
+ float L0 = cusp.x;
315
+
316
+ float t = find_gamut_intersection(a_, b_, L, C, L0);
317
+
318
+ float L_clipped = L0 * (1.f - t) + t * L;
319
+ float C_clipped = t * C;
320
+
321
+ return oklab_to_linear_srgb(vec3( L_clipped, C_clipped * a_, C_clipped * b_ ));
322
+ }
323
+
324
+ vec3 gamut_clip_adaptive_L0_0_5(vec3 rgb, float alpha)
325
+ {
326
+ if (rgb.r < 1.f && rgb.g < 1.f && rgb.b < 1.f && rgb.r > 0.f && rgb.g > 0.f && rgb.b > 0.f)
327
+ return rgb;
328
+
329
+ vec3 lab = linear_srgb_to_oklab(rgb);
330
+
331
+ float L = lab.x;
332
+ float eps = 0.00001f;
333
+ float C = max(eps, sqrt(lab.y * lab.y + lab.z * lab.z));
334
+ float a_ = lab.y / C;
335
+ float b_ = lab.z / C;
336
+
337
+ float Ld = L - 0.5f;
338
+ float e1 = 0.5f + abs(Ld) + alpha * C;
339
+ float L0 = 0.5f * (1.f + sign(Ld) * (e1 - sqrt(e1 * e1 - 2.f * abs(Ld))));
340
+
341
+ float t = find_gamut_intersection(a_, b_, L, C, L0);
342
+ float L_clipped = L0 * (1.f - t) + t * L;
343
+ float C_clipped = t * C;
344
+
345
+ return oklab_to_linear_srgb(vec3( L_clipped, C_clipped * a_, C_clipped * b_ ));
346
+ }
347
+
348
+ vec3 gamut_clip_adaptive_L0_L_cusp(vec3 rgb, float alpha)
349
+ {
350
+ if (rgb.r < 1.f && rgb.g < 1.f && rgb.b < 1.f && rgb.r > 0.f && rgb.g > 0.f && rgb.b > 0.f)
351
+ return rgb;
352
+
353
+ vec3 lab = linear_srgb_to_oklab(rgb);
354
+
355
+ float L = lab.x;
356
+ float eps = 0.00001f;
357
+ float C = max(eps, sqrt(lab.y * lab.y + lab.z * lab.z));
358
+ float a_ = lab.y / C;
359
+ float b_ = lab.z / C;
360
+
361
+ // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
362
+ vec2 cusp = find_cusp(a_, b_);
363
+
364
+ float Ld = L - cusp.x;
365
+ float k = 2.f * (Ld > 0.f ? 1.f - cusp.x : cusp.x);
366
+
367
+ float e1 = 0.5f * k + abs(Ld) + alpha * C / k;
368
+ float L0 = cusp.x + 0.5f * (sign(Ld) * (e1 - sqrt(e1 * e1 - 2.f * k * abs(Ld))));
369
+
370
+ float t = find_gamut_intersection(a_, b_, L, C, L0);
371
+ float L_clipped = L0 * (1.f - t) + t * L;
372
+ float C_clipped = t * C;
373
+
374
+ return oklab_to_linear_srgb(vec3( L_clipped, C_clipped * a_, C_clipped * b_ ));
375
+ }
376
+
377
+ float toe(float x)
378
+ {
379
+ float k_1 = 0.206f;
380
+ float k_2 = 0.03f;
381
+ float k_3 = (1.f + k_1) / (1.f + k_2);
382
+ return 0.5f * (k_3 * x - k_1 + sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4.f * k_2 * k_3 * x));
383
+ }
384
+
385
+ float toe_inv(float x)
386
+ {
387
+ float k_1 = 0.206f;
388
+ float k_2 = 0.03f;
389
+ float k_3 = (1.f + k_1) / (1.f + k_2);
390
+ return (x * x + k_1 * x) / (k_3 * (x + k_2));
391
+ }
392
+
393
+ vec2 to_ST(vec2 cusp)
394
+ {
395
+ float L = cusp.x;
396
+ float C = cusp.y;
397
+ return vec2( C / L, C / (1.f - L) );
398
+ }
399
+
400
+ // Returns a smooth approximation of the location of the cusp
401
+ // This polynomial was created by an optimization process
402
+ // It has been designed so that S_mid < S_max and T_mid < T_max
403
+ vec2 get_ST_mid(float a_, float b_)
404
+ {
405
+ float S = 0.11516993f + 1.f / (
406
+ +7.44778970f + 4.15901240f * b_
407
+ + a_ * (-2.19557347f + 1.75198401f * b_
408
+ + a_ * (-2.13704948f - 10.02301043f * b_
409
+ + a_ * (-4.24894561f + 5.38770819f * b_ + 4.69891013f * a_
410
+ )))
411
+ );
412
+
413
+ float T = 0.11239642f + 1.f / (
414
+ +1.61320320f - 0.68124379f * b_
415
+ + a_ * (+0.40370612f + 0.90148123f * b_
416
+ + a_ * (-0.27087943f + 0.61223990f * b_
417
+ + a_ * (+0.00299215f - 0.45399568f * b_ - 0.14661872f * a_
418
+ )))
419
+ );
420
+
421
+ return vec2( S, T );
422
+ }
423
+
424
+ vec3 get_Cs(float L, float a_, float b_)
425
+ {
426
+ vec2 cusp = find_cusp(a_, b_);
427
+
428
+ float C_max = find_gamut_intersection(a_, b_, L, 1.f, L, cusp);
429
+ vec2 ST_max = to_ST(cusp);
430
+
431
+ // Scale factor to compensate for the curved part of gamut shape:
432
+ float k = C_max / min((L * ST_max.x), (1.f - L) * ST_max.y);
433
+
434
+ float C_mid;
435
+ {
436
+ vec2 ST_mid = get_ST_mid(a_, b_);
437
+
438
+ // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
439
+ float C_a = L * ST_mid.x;
440
+ float C_b = (1.f - L) * ST_mid.y;
441
+ C_mid = 0.9f * k * sqrt(sqrt(1.f / (1.f / (C_a * C_a * C_a * C_a) + 1.f / (C_b * C_b * C_b * C_b))));
442
+ }
443
+
444
+ float C_0;
445
+ {
446
+ // for C_0, the shape is independent of hue, so vec2 are constant. Values picked to roughly be the average values of vec2.
447
+ float C_a = L * 0.4f;
448
+ float C_b = (1.f - L) * 0.8f;
449
+
450
+ // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
451
+ C_0 = sqrt(1.f / (1.f / (C_a * C_a) + 1.f / (C_b * C_b)));
452
+ }
453
+
454
+ return vec3( C_0, C_mid, C_max );
455
+ }
456
+
457
+ vec3 okhsl_to_srgb(vec3 hsl)
458
+ {
459
+ float h = hsl.x;
460
+ float s = hsl.y;
461
+ float l = hsl.z;
462
+
463
+ if (l == 1.0f)
464
+ {
465
+ return vec3( 1.f, 1.f, 1.f );
466
+ }
467
+
468
+ else if (l == 0.f)
469
+ {
470
+ return vec3( 0.f, 0.f, 0.f );
471
+ }
472
+
473
+ float a_ = cos(2.f * M_PI * h);
474
+ float b_ = sin(2.f * M_PI * h);
475
+ float L = toe_inv(l);
476
+
477
+ vec3 cs = get_Cs(L, a_, b_);
478
+ float C_0 = cs.x;
479
+ float C_mid = cs.y;
480
+ float C_max = cs.z;
481
+
482
+ float mid = 0.8f;
483
+ float mid_inv = 1.25f;
484
+
485
+ float C, t, k_0, k_1, k_2;
486
+
487
+ if (s < mid)
488
+ {
489
+ t = mid_inv * s;
490
+
491
+ k_1 = mid * C_0;
492
+ k_2 = (1.f - k_1 / C_mid);
493
+
494
+ C = t * k_1 / (1.f - k_2 * t);
495
+ }
496
+ else
497
+ {
498
+ t = (s - mid)/ (1.f - mid);
499
+
500
+ k_0 = C_mid;
501
+ k_1 = (1.f - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0;
502
+ k_2 = (1.f - (k_1) / (C_max - C_mid));
503
+
504
+ C = k_0 + t * k_1 / (1.f - k_2 * t);
505
+ }
506
+
507
+ vec3 rgb = oklab_to_linear_srgb(vec3( L, C * a_, C * b_ ));
508
+ return vec3(
509
+ srgb_transfer_function(rgb.r),
510
+ srgb_transfer_function(rgb.g),
511
+ srgb_transfer_function(rgb.b)
512
+ );
513
+ }
514
+
515
+ vec3 srgb_to_okhsl(vec3 rgb)
516
+ {
517
+ vec3 lab = linear_srgb_to_oklab(vec3(
518
+ srgb_transfer_function_inv(rgb.r),
519
+ srgb_transfer_function_inv(rgb.g),
520
+ srgb_transfer_function_inv(rgb.b)
521
+ ));
522
+
523
+ float C = sqrt(lab.y * lab.y + lab.z * lab.z);
524
+ float a_ = lab.y / C;
525
+ float b_ = lab.z / C;
526
+
527
+ float L = lab.x;
528
+ float h = 0.5f + 0.5f * atan(-lab.z, -lab.y) / M_PI;
529
+
530
+ vec3 cs = get_Cs(L, a_, b_);
531
+ float C_0 = cs.x;
532
+ float C_mid = cs.y;
533
+ float C_max = cs.z;
534
+
535
+ // Inverse of the interpolation in okhsl_to_srgb:
536
+
537
+ float mid = 0.8f;
538
+ float mid_inv = 1.25f;
539
+
540
+ float s;
541
+ if (C < C_mid)
542
+ {
543
+ float k_1 = mid * C_0;
544
+ float k_2 = (1.f - k_1 / C_mid);
545
+
546
+ float t = C / (k_1 + k_2 * C);
547
+ s = t * mid;
548
+ }
549
+ else
550
+ {
551
+ float k_0 = C_mid;
552
+ float k_1 = (1.f - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0;
553
+ float k_2 = (1.f - (k_1) / (C_max - C_mid));
554
+
555
+ float t = (C - k_0) / (k_1 + k_2 * (C - k_0));
556
+ s = mid + (1.f - mid) * t;
557
+ }
558
+
559
+ float l = toe(L);
560
+ return vec3( h, s, l );
561
+ }
562
+
563
+
564
+ vec3 okhsv_to_srgb(vec3 hsv)
565
+ {
566
+ float h = hsv.x;
567
+ float s = hsv.y;
568
+ float v = hsv.z;
569
+
570
+ float a_ = cos(2.f * M_PI * h);
571
+ float b_ = sin(2.f * M_PI * h);
572
+
573
+ vec2 cusp = find_cusp(a_, b_);
574
+ vec2 ST_max = to_ST(cusp);
575
+ float S_max = ST_max.x;
576
+ float T_max = ST_max.y;
577
+ float S_0 = 0.5f;
578
+ float k = 1.f- S_0 / S_max;
579
+
580
+ // first we compute L and V as if the gamut is a perfect triangle:
581
+
582
+ // L, C when v==1:
583
+ float L_v = 1.f - s * S_0 / (S_0 + T_max - T_max * k * s);
584
+ float C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s);
585
+
586
+ float L = v * L_v;
587
+ float C = v * C_v;
588
+
589
+ // then we compensate for both toe and the curved top part of the triangle:
590
+ float L_vt = toe_inv(L_v);
591
+ float C_vt = C_v * L_vt / L_v;
592
+
593
+ float L_new = toe_inv(L);
594
+ C = C * L_new / L;
595
+ L = L_new;
596
+
597
+ vec3 rgb_scale = oklab_to_linear_srgb(vec3( L_vt, a_ * C_vt, b_ * C_vt ));
598
+ float scale_L = cbrt(1.f / max(max(rgb_scale.r, rgb_scale.g), max(rgb_scale.b, 0.f)));
599
+
600
+ L = L * scale_L;
601
+ C = C * scale_L;
602
+
603
+ vec3 rgb = oklab_to_linear_srgb(vec3( L, C * a_, C * b_ ));
604
+ return vec3(
605
+ srgb_transfer_function(rgb.r),
606
+ srgb_transfer_function(rgb.g),
607
+ srgb_transfer_function(rgb.b)
608
+ );
609
+ }
610
+
611
+ vec3 srgb_to_okhsv(vec3 rgb)
612
+ {
613
+ vec3 lab = linear_srgb_to_oklab(vec3(
614
+ srgb_transfer_function_inv(rgb.r),
615
+ srgb_transfer_function_inv(rgb.g),
616
+ srgb_transfer_function_inv(rgb.b)
617
+ ));
618
+
619
+ float C = sqrt(lab.y * lab.y + lab.z * lab.z);
620
+ float a_ = lab.y / C;
621
+ float b_ = lab.z / C;
622
+
623
+ float L = lab.x;
624
+ float h = 0.5f + 0.5f * atan(-lab.z, -lab.y) / M_PI;
625
+
626
+ vec2 cusp = find_cusp(a_, b_);
627
+ vec2 ST_max = to_ST(cusp);
628
+ float S_max = ST_max.x;
629
+ float T_max = ST_max.y;
630
+ float S_0 = 0.5f;
631
+ float k = 1.f - S_0 / S_max;
632
+
633
+ // first we find L_v, C_v, L_vt and C_vt
634
+
635
+ float t = T_max / (C + L * T_max);
636
+ float L_v = t * L;
637
+ float C_v = t * C;
638
+
639
+ float L_vt = toe_inv(L_v);
640
+ float C_vt = C_v * L_vt / L_v;
641
+
642
+ // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle:
643
+ vec3 rgb_scale = oklab_to_linear_srgb(vec3( L_vt, a_ * C_vt, b_ * C_vt ));
644
+ float scale_L = cbrt(1.f / max(max(rgb_scale.r, rgb_scale.g), max(rgb_scale.b, 0.f)));
645
+
646
+ L = L / scale_L;
647
+ C = C / scale_L;
648
+
649
+ C = C * toe(L) / L;
650
+ L = toe(L);
651
+
652
+ // we can now compute v and s:
653
+
654
+ float v = L / L_v;
655
+ float s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v);
656
+
657
+ return vec3 (h, s, v );
658
+ }`, F = `vec3 hsl2rgb( in vec3 c ) {
659
+ vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 );
660
+ return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0));
661
+ }`, U = `vec3 hsv2rgb(vec3 c) {
662
+ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
663
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
664
+ return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
665
+ }`, G = `// slightly rearranged vector components so it matches with LCH
666
+ // M_PI and srgb_transfer_function are provided by oklab.frag.glsl (included before this file)
667
+ vec3 lch2rgb(vec3 lch) {
668
+ lch.y *= 0.34;
669
+
670
+ vec3 lab = vec3(
671
+ lch.x,
672
+ lch.y * cos(lch.z * M_PI*2.0),
673
+ lch.y * sin(lch.z * M_PI*2.0)
674
+ );
675
+
676
+ vec3 lms = vec3(
677
+ lab.x + 0.3963377774f * lab.y + 0.2158037573f * lab.z,
678
+ lab.x - 0.1055613458f * lab.y - 0.0638541728f * lab.z,
679
+ lab.x - 0.0894841775f * lab.y - 1.2914855480f * lab.z
680
+ );
681
+
682
+ lms = pow(max(lms, vec3(0.0)), vec3(3.0));
683
+
684
+ vec3 rgb = vec3(
685
+ +4.0767416621f * lms.x - 3.3077115913f * lms.y + 0.2309699292f * lms.z,
686
+ -1.2684380046f * lms.x + 2.6097574011f * lms.y - 0.3413193965f * lms.z,
687
+ -0.0041960863f * lms.x - 0.7034186147f * lms.y + 1.7076147010f * lms.z
688
+ );
689
+
690
+ return vec3(
691
+ srgb_transfer_function(rgb.r),
692
+ srgb_transfer_function(rgb.g),
693
+ srgb_transfer_function(rgb.b)
694
+ );
695
+ }
696
+ `, D = `// Kotsarenko/Ramos weighted RGB distance.
697
+ // Operates on sRGB values directly (no linearisation needed).
698
+ // Weights red and blue channels by the mean red value, which improves
699
+ // perceptual uniformity compared to plain Euclidean RGB at minimal cost.
700
+ float kotsarenkoRamos(vec3 c1, vec3 c2) {
701
+ float rMean = (c1.r + c2.r) * 0.5;
702
+ vec3 d = c1 - c2;
703
+ return sqrt((2.0 + rMean) * d.r*d.r + 4.0 * d.g*d.g + (3.0 - rMean) * d.b*d.b);
704
+ }
705
+
706
+ // ── CIELab ────────────────────────────────────────────────────────────────────
707
+ // sRGB -> XYZ (D65) -> CIELab
708
+ // Depends on: srgb2rgb() from srgb2rgb.frag.glsl, cbrt() from oklab.frag.glsl
709
+
710
+ float _lab_f(float t) {
711
+ float delta = 6.0 / 29.0;
712
+ return t > delta * delta * delta
713
+ ? cbrt(t)
714
+ : t / (3.0 * delta * delta) + 4.0 / 29.0;
715
+ }
716
+
717
+ vec3 srgb_to_cielab(vec3 srgb) {
718
+ vec3 lin = srgb2rgb(srgb);
719
+
720
+ // Linear sRGB -> XYZ (D65 illuminant)
721
+ vec3 xyz = vec3(
722
+ 0.4124564 * lin.r + 0.3575761 * lin.g + 0.1804375 * lin.b,
723
+ 0.2126729 * lin.r + 0.7151522 * lin.g + 0.0721750 * lin.b,
724
+ 0.0193339 * lin.r + 0.1191920 * lin.g + 0.9503041 * lin.b
725
+ );
726
+
727
+ // XYZ -> Lab (D65 white point: 0.95047, 1.00000, 1.08883)
728
+ float fx = _lab_f(xyz.x / 0.95047);
729
+ float fy = _lab_f(xyz.y);
730
+ float fz = _lab_f(xyz.z / 1.08883);
731
+
732
+ return vec3(
733
+ 116.0 * fy - 16.0, // L*
734
+ 500.0 * (fx - fy), // a*
735
+ 200.0 * (fy - fz) // b*
736
+ );
737
+ }
738
+
739
+ // CIE76: plain Euclidean distance in CIELab
740
+ float deltaE76(vec3 lab1, vec3 lab2) {
741
+ vec3 d = lab1 - lab2;
742
+ return sqrt(d.x*d.x + d.y*d.y + d.z*d.z);
743
+ }
744
+
745
+ // CIEDE2000
746
+ float deltaE2000(vec3 lab1, vec3 lab2) {
747
+ float L1 = lab1.x, a1 = lab1.y, b1 = lab1.z;
748
+ float L2 = lab2.x, a2 = lab2.y, b2 = lab2.z;
749
+
750
+ // Chroma
751
+ float C1 = sqrt(a1*a1 + b1*b1);
752
+ float C2 = sqrt(a2*a2 + b2*b2);
753
+ float Cavg = (C1 + C2) * 0.5;
754
+ float Cavg7 = pow(Cavg, 7.0);
755
+
756
+ // G factor: adjustment to a* axis
757
+ float G = 0.5 * (1.0 - sqrt(Cavg7 / (Cavg7 + 6103515625.0))); // 25^7
758
+
759
+ float a1p = a1 * (1.0 + G);
760
+ float a2p = a2 * (1.0 + G);
761
+ float C1p = sqrt(a1p*a1p + b1*b1);
762
+ float C2p = sqrt(a2p*a2p + b2*b2);
763
+
764
+ // Guard atan(0,0): GLSL ES leaves that undefined, so skip it for achromatic colors.
765
+ // When a color has no chroma its hue angle is meaningless — we just need it to
766
+ // be a well-defined number so it doesn't corrupt the rest of the formula.
767
+ bool c1Achromatic = C1p < 1e-6;
768
+ bool c2Achromatic = C2p < 1e-6;
769
+
770
+ float h1p = c1Achromatic ? 0.0 : atan(b1, a1p);
771
+ if (h1p < 0.0) h1p += TWO_PI;
772
+ float h2p = c2Achromatic ? 0.0 : atan(b2, a2p);
773
+ if (h2p < 0.0) h2p += TWO_PI;
774
+
775
+ // Deltas
776
+ float dLp = L2 - L1;
777
+ float dCp = C2p - C1p;
778
+
779
+ float dhp = 0.0;
780
+ if (!c1Achromatic && !c2Achromatic) {
781
+ dhp = h2p - h1p;
782
+ if (dhp > M_PI) dhp -= TWO_PI;
783
+ else if (dhp < -M_PI) dhp += TWO_PI;
784
+ }
785
+ float dHp = 2.0 * sqrt(C1p * C2p) * sin(dhp * 0.5);
786
+
787
+ // Averages
788
+ float Lp = (L1 + L2) * 0.5;
789
+ float Cp = (C1p + C2p) * 0.5;
790
+
791
+ // When one color is achromatic, its hue is 0 and the average is simply the other's hue
792
+ float hp;
793
+ if (c1Achromatic || c2Achromatic) {
794
+ hp = h1p + h2p;
795
+ } else if (abs(h1p - h2p) <= M_PI) {
796
+ hp = (h1p + h2p) * 0.5;
797
+ } else if (h1p + h2p < TWO_PI) {
798
+ hp = (h1p + h2p + TWO_PI) * 0.5;
799
+ } else {
800
+ hp = (h1p + h2p - TWO_PI) * 0.5;
801
+ }
802
+
803
+ float T = 1.0
804
+ - 0.17 * cos(hp - radians(30.0))
805
+ + 0.24 * cos(2.0 * hp)
806
+ + 0.32 * cos(3.0 * hp + radians(6.0))
807
+ - 0.20 * cos(4.0 * hp - radians(63.0));
808
+
809
+ // Weighting functions
810
+ float Lpm50sq = (Lp - 50.0) * (Lp - 50.0);
811
+ float SL = 1.0 + 0.015 * Lpm50sq / sqrt(20.0 + Lpm50sq);
812
+ float SC = 1.0 + 0.045 * Cp;
813
+ float SH = 1.0 + 0.015 * Cp * T;
814
+
815
+ // Rotation term
816
+ float Cp7 = pow(Cp, 7.0);
817
+ float RC = 2.0 * sqrt(Cp7 / (Cp7 + 6103515625.0));
818
+ float hpDeg = degrees(hp);
819
+ float dTheta = radians(30.0) * exp(-((hpDeg - 275.0) / 25.0) * ((hpDeg - 275.0) / 25.0));
820
+ float RT = -sin(2.0 * dTheta) * RC;
821
+
822
+ float dLn = dLp / SL;
823
+ float dCn = dCp / SC;
824
+ float dHn = dHp / SH;
825
+
826
+ return sqrt(dLn*dLn + dCn*dCn + dHn*dHn + RT * dCn * dHn);
827
+ }
828
+ `, N = `// distanceMetric uniform: 0 = rgb, 1 = oklab, 2 = deltaE76, 3 = deltaE2000
829
+ vec3 closestColor(vec3 color, sampler2D paletteTexture, int paletteSize) {
830
+ float minDist = 1000000.0;
831
+ vec3 closest = vec3(0.0);
832
+
833
+ for (int i = 0; i < paletteSize; i++) {
834
+ vec3 paletteColor = texture2D(paletteTexture, vec2(float(i) / float(paletteSize), 0.5)).rgb;
835
+
836
+ float dist;
837
+ if (distanceMetric == 1) {
838
+ // OKLab: perceptually uniform Euclidean distance
839
+ dist = distance(linear_srgb_to_oklab(srgb2rgb(color)), linear_srgb_to_oklab(srgb2rgb(paletteColor)));
840
+ } else if (distanceMetric == 2) {
841
+ // CIE76: Euclidean distance in CIELab
842
+ dist = deltaE76(srgb_to_cielab(color), srgb_to_cielab(paletteColor));
843
+ } else if (distanceMetric == 3) {
844
+ // CIEDE2000: perceptually weighted color difference
845
+ dist = deltaE2000(srgb_to_cielab(color), srgb_to_cielab(paletteColor));
846
+ } else if (distanceMetric == 4) {
847
+ // Kotsarenko/Ramos: weighted RGB distance (fast, no conversion needed)
848
+ dist = kotsarenkoRamos(color, paletteColor);
849
+ } else {
850
+ // RGB: plain Euclidean distance in sRGB space
851
+ dist = distance(color, paletteColor);
852
+ }
853
+
854
+ if (dist < minDist) {
855
+ minDist = dist;
856
+ closest = paletteColor;
857
+ }
858
+ }
859
+
860
+ return closest;
861
+ }
862
+ `, tt = `
863
+ #define TWO_PI 6.28318530718
864
+ varying vec2 vUv;
865
+ uniform float progress;
866
+ uniform bool isPolar;
867
+ uniform int distanceMetric;
868
+ uniform int progress_axis;
869
+ uniform sampler2D paletteTexture;
870
+ uniform int paletteLength;
871
+ uniform bool debug;
872
+ uniform int polarColorModel;
873
+ uniform bool invertZ;
874
+
875
+ ${A}
876
+ ${O}
877
+ ${F}
878
+ ${U}
879
+ ${G}
880
+ ${D}
881
+ ${N}
882
+
883
+ // polarColorModel: 0=hsv, 1=okhsv, 2=hsl, 3=okhsl, 4=oklch
884
+ vec3 polarToRGB(vec3 colorCoords) {
885
+ if (polarColorModel == 0) {
886
+ return hsv2rgb(colorCoords);
887
+ } else if (polarColorModel == 1) {
888
+ return okhsv_to_srgb(colorCoords);
889
+ } else if (polarColorModel == 2) {
890
+ return hsl2rgb(colorCoords);
891
+ } else if (polarColorModel == 3) {
892
+ return okhsl_to_srgb(colorCoords);
893
+ } else {
894
+ // oklch — lch2rgb uses the OKLab matrix so this is OKLCH
895
+ return lch2rgb(vec3(colorCoords.z, colorCoords.y, colorCoords.x));
896
+ }
897
+ }
898
+
899
+ void main(){
900
+ vec3 colorCoords = vec3(progress, vUv.x, vUv.y);
901
+ if(progress_axis == 1){
902
+ colorCoords = vec3(vUv.x, progress, vUv.y);
903
+ } else if(progress_axis == 2){
904
+ colorCoords = vec3(vUv.x, vUv.y, 1. - progress);
905
+ }
906
+
907
+ if(isPolar) {
908
+ vec2 toCenter = vUv - 0.5;
909
+ float angle = atan(toCenter.y, toCenter.x);
910
+ float radius = length(toCenter) * 2.0;
911
+
912
+ if(progress_axis == 2){
913
+ colorCoords = vec3((angle / TWO_PI), radius, 1. - progress);
914
+ } else if(progress_axis == 1){
915
+ colorCoords = vec3((angle / TWO_PI), 1. - progress, radius);
916
+ if (radius > 1.0) {
917
+ discard;
918
+ }
919
+ } else {
920
+ float hue = 1.0 - abs(0.5 - progress * .5) * 2.0;
921
+ if (vUv.x > 0.5) {
922
+ hue += 0.5;
923
+ }
924
+ colorCoords = vec3(hue, abs(0.5 - vUv.x) * 2.0, vUv.y);
925
+ }
926
+ }
927
+
928
+ if(invertZ){
929
+ colorCoords.z = 1. - colorCoords.z;
930
+ }
931
+
932
+ vec3 rgb = polarToRGB(colorCoords);
933
+ vec3 closest = closestColor(rgb, paletteTexture, paletteLength);
934
+
935
+ if (debug) {
936
+ closest = rgb;
937
+ }
938
+
939
+ gl_FragColor = vec4(closest, 1.);
940
+ }`, et = `varying vec2 vUv;
941
+ void main(){
942
+ vUv = uv;
943
+ gl_Position = vec4(position, 1.);
944
+ }`, d = (u) => {
945
+ const t = new Float32Array(
946
+ u.flatMap((o) => {
947
+ try {
948
+ const i = new I(o).convertLinearToSRGB();
949
+ return [i.r, i.g, i.b, 1];
950
+ } catch {
951
+ return console.error(`Invalid color: ${o}`), [0, 0, 0, 1];
952
+ }
953
+ })
954
+ ), e = new X(t, u.length, 1, J, Q);
955
+ return e.needsUpdate = !0, e.wrapS = E, e.wrapT = E, e.minFilter = L, e.magFilter = L, e;
956
+ }, M = (u = 20) => Array.from(
957
+ { length: u },
958
+ () => `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`
959
+ );
960
+ class it {
961
+ #t = [];
962
+ #r = 512;
963
+ #l = 512;
964
+ #P = 1;
965
+ // shader state
966
+ #f = 0;
967
+ #x = "y";
968
+ #C = "okhsv";
969
+ #p = "oklab";
970
+ #u = !0;
971
+ #g = !1;
972
+ #_ = !1;
973
+ // uniform value maps
974
+ #k = { x: 0, y: 1, z: 2 };
975
+ #h = { hsv: 0, okhsv: 1, hsl: 2, okhsl: 3, oklch: 4 };
976
+ #a = { rgb: 0, oklab: 1, deltaE76: 2, deltaE2000: 3, kotsarenkoRamos: 4 };
977
+ // three.js
978
+ #s;
979
+ #i;
980
+ #z;
981
+ #d;
982
+ #o;
983
+ #c;
984
+ #M;
985
+ #E;
986
+ #m = null;
987
+ // dom
988
+ #L;
989
+ constructor({
990
+ palette: t = M(),
991
+ width: e = 512,
992
+ height: o = 512,
993
+ pixelRatio: i = window.devicePixelRatio,
994
+ container: n = document.body,
995
+ colorModel: s = "okhsv",
996
+ distanceMetric: a = "oklab",
997
+ isPolar: l = !0,
998
+ axis: c = "y",
999
+ position: _ = 0,
1000
+ invertLightness: f = !1,
1001
+ showRaw: r = !1
1002
+ } = {}) {
1003
+ this.#t = t, this.#r = e, this.#l = o, this.#P = i, this.#C = s, this.#p = a, this.#u = l, this.#x = c, this.#f = _, this.#g = f, this.#_ = r, this.#L = n, this.#s = d(this.#t), this.#i = {
1004
+ progress: { value: this.#f },
1005
+ progress_axis: { value: this.#k[this.#x] },
1006
+ polarColorModel: { value: this.#h[this.#C] },
1007
+ isPolar: { value: this.#u },
1008
+ distanceMetric: { value: this.#a[this.#p] },
1009
+ paletteTexture: { value: this.#s },
1010
+ paletteLength: { value: this.#t.length },
1011
+ debug: { value: this.#_ },
1012
+ invertZ: { value: this.#g }
1013
+ }, this.#z = new v({ uniforms: this.#i, vertexShader: et, fragmentShader: tt }), this.#y();
1014
+ }
1015
+ #y() {
1016
+ this.#E = new m(), this.#M = new k(-1, 1, 1, -1, 0.1, 1), this.#c = new w(), this.#c.setPixelRatio(this.#P), this.#c.setSize(this.#r, this.#l), this.#c.domElement.classList.add("palette-viz"), this.#d = new y(2, 2), this.#o = new C(this.#d, this.#z), this.#E.add(this.#o), this.#L.appendChild(this.#c.domElement), this.#e();
1017
+ }
1018
+ #e() {
1019
+ this.#m !== null && cancelAnimationFrame(this.#m), this.#m = requestAnimationFrame(() => {
1020
+ this.#c.render(this.#E, this.#M);
1021
+ });
1022
+ }
1023
+ // ── Public API ────────────────────────────────────────────────────────────
1024
+ get canvas() {
1025
+ return this.#c.domElement;
1026
+ }
1027
+ get width() {
1028
+ return this.#r;
1029
+ }
1030
+ get height() {
1031
+ return this.#l;
1032
+ }
1033
+ resize(t, e = null) {
1034
+ this.#r = t, this.#l = e === null ? t : e, this.#c.setSize(this.#r, this.#l), this.#M.updateProjectionMatrix(), this.#e();
1035
+ }
1036
+ destroy() {
1037
+ this.#m !== null && (cancelAnimationFrame(this.#m), this.#m = null), this.#s.dispose(), this.#z.dispose(), this.#d.dispose(), this.#c.dispose(), this.#c.domElement.remove();
1038
+ }
1039
+ // ── Palette ───────────────────────────────────────────────────────────────
1040
+ set palette(t) {
1041
+ this.#t = t, this.#s = d(t), this.#i.paletteTexture.value = this.#s, this.#i.paletteLength.value = t.length, this.#e();
1042
+ }
1043
+ get palette() {
1044
+ return this.#t;
1045
+ }
1046
+ setColor(t, e) {
1047
+ if (e < 0 || e >= this.#t.length)
1048
+ throw new Error(`Index ${e} out of range`);
1049
+ this.#t[e] = t, this.#s = d(this.#t), this.#i.paletteTexture.value = this.#s, this.#e();
1050
+ }
1051
+ addColor(t, e) {
1052
+ this.#t.splice(e ?? this.#t.length, 0, t), this.#s = d(this.#t), this.#i.paletteTexture.value = this.#s, this.#i.paletteLength.value = this.#t.length, this.#e();
1053
+ }
1054
+ removeColor(t) {
1055
+ const e = typeof t == "number" ? t : this.#t.indexOf(t);
1056
+ if (e === -1) throw new Error("Color not found in palette");
1057
+ if (e < 0 || e >= this.#t.length) throw new Error(`Index ${e} out of range`);
1058
+ this.#t.splice(e, 1), this.#s = d(this.#t), this.#i.paletteTexture.value = this.#s, this.#i.paletteLength.value = this.#t.length, this.#e();
1059
+ }
1060
+ // ── Shader properties ─────────────────────────────────────────────────────
1061
+ set position(t) {
1062
+ this.#f = t, this.#i.progress.value = t, this.#e();
1063
+ }
1064
+ get position() {
1065
+ return this.#f;
1066
+ }
1067
+ set axis(t) {
1068
+ if (!(t in this.#k)) throw new Error("axis must be 'x', 'y', or 'z'");
1069
+ this.#x = t, this.#i.progress_axis.value = this.#k[t], this.#e();
1070
+ }
1071
+ get axis() {
1072
+ return this.#x;
1073
+ }
1074
+ set colorModel(t) {
1075
+ if (!(t in this.#h)) throw new Error("colorModel must be 'hsv', 'okhsv', 'hsl', 'okhsl', or 'oklch'");
1076
+ this.#C = t, this.#i.polarColorModel.value = this.#h[t], this.#e();
1077
+ }
1078
+ get colorModel() {
1079
+ return this.#C;
1080
+ }
1081
+ set distanceMetric(t) {
1082
+ if (!(t in this.#a)) throw new Error("distanceMetric must be 'rgb', 'oklab', 'deltaE76', 'deltaE2000', or 'kotsarenkoRamos'");
1083
+ this.#p = t, this.#i.distanceMetric.value = this.#a[t], this.#e();
1084
+ }
1085
+ get distanceMetric() {
1086
+ return this.#p;
1087
+ }
1088
+ set isPolar(t) {
1089
+ this.#u = t, this.#i.isPolar.value = t, this.#e();
1090
+ }
1091
+ get isPolar() {
1092
+ return this.#u;
1093
+ }
1094
+ set invertLightness(t) {
1095
+ this.#g = t, this.#i.invertZ.value = t, this.#e();
1096
+ }
1097
+ get invertLightness() {
1098
+ return this.#g;
1099
+ }
1100
+ set showRaw(t) {
1101
+ this.#_ = t, this.#i.debug.value = t, this.#e();
1102
+ }
1103
+ get showRaw() {
1104
+ return this.#_;
1105
+ }
1106
+ static paletteToTexture = (t) => d(t);
1107
+ }
1108
+ const W = `
1109
+ varying vec2 vUv;
1110
+ void main() {
1111
+ vUv = uv;
1112
+ gl_Position = vec4(position, 1.0);
1113
+ }`, q = `
1114
+ #define TWO_PI 6.28318530718
1115
+ varying vec2 vUv;
1116
+
1117
+ uniform float slices;
1118
+ uniform bool isPolar;
1119
+ uniform int distanceMetric;
1120
+ uniform sampler2D paletteTexture;
1121
+ uniform int paletteLength;
1122
+ uniform bool debug;
1123
+ uniform int polarColorModel;
1124
+ uniform bool invertZ;
1125
+
1126
+ ${A}
1127
+ ${O}
1128
+ ${F}
1129
+ ${U}
1130
+ ${G}
1131
+ ${D}
1132
+ ${N}
1133
+
1134
+ vec3 polarToRGB(vec3 cc) {
1135
+ if (polarColorModel == 0) return hsv2rgb(cc);
1136
+ if (polarColorModel == 1) return okhsv_to_srgb(cc);
1137
+ if (polarColorModel == 2) return hsl2rgb(cc);
1138
+ if (polarColorModel == 3) return okhsl_to_srgb(cc);
1139
+ return lch2rgb(vec3(cc.z, cc.y, cc.x));
1140
+ }
1141
+
1142
+ void main() {
1143
+ // Decode which slice and the UV within that slice from the atlas Y coordinate.
1144
+ float sliceF = vUv.y * slices;
1145
+ float progress = floor(sliceF) / (slices - 1.0); // 0…1 along value axis
1146
+ vec2 sliceUV = vec2(vUv.x, fract(sliceF));
1147
+
1148
+ vec3 colorCoords;
1149
+ if (isPolar) {
1150
+ vec2 toCenter = sliceUV - 0.5;
1151
+ float angle = atan(toCenter.y, toCenter.x);
1152
+ float radius = length(toCenter) * 2.0;
1153
+ // Outside the unit circle: store black; display shader discards it.
1154
+ if (radius > 1.0) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }
1155
+ colorCoords = vec3(angle / TWO_PI, radius, progress);
1156
+ } else {
1157
+ colorCoords = vec3(sliceUV.x, sliceUV.y, progress);
1158
+ }
1159
+
1160
+ if (invertZ) colorCoords.z = 1.0 - colorCoords.z;
1161
+
1162
+ vec3 rgb = polarToRGB(colorCoords);
1163
+ vec3 col = debug ? rgb : closestColor(rgb, paletteTexture, paletteLength);
1164
+ gl_FragColor = vec4(col, 1.0);
1165
+ }`, nt = `
1166
+ varying vec3 vWorldPos;
1167
+ void main() {
1168
+ vec4 wp = modelMatrix * vec4(position, 1.0);
1169
+ vWorldPos = wp.xyz;
1170
+ gl_Position = projectionMatrix * viewMatrix * wp;
1171
+ }`, st = `
1172
+ varying vec3 vWorldPos;
1173
+
1174
+ uniform sampler2D atlas;
1175
+ uniform float slices;
1176
+ uniform float opacity;
1177
+ uniform bool isPolar;
1178
+
1179
+ void main() {
1180
+ // World-space XZ → slice UV, Y → progress along value axis
1181
+ vec2 sliceUV = vWorldPos.xz + 0.5;
1182
+ float progress = vWorldPos.y + 0.5;
1183
+
1184
+ if (isPolar && length(sliceUV - 0.5) * 2.0 > 1.0) discard;
1185
+
1186
+ float si = floor(progress * (slices - 1.0) + 0.5);
1187
+ float atlasV = (si + sliceUV.y) / slices;
1188
+ vec3 col = texture2D(atlas, vec2(sliceUV.x, atlasV)).rgb;
1189
+
1190
+ gl_FragColor = vec4(col, opacity);
1191
+ }`;
1192
+ class ot {
1193
+ #t = [];
1194
+ #r = 512;
1195
+ #l = 512;
1196
+ #P = 1;
1197
+ #f = 32;
1198
+ #x = 64;
1199
+ #C = "okhsv";
1200
+ #p = "oklab";
1201
+ #u = !1;
1202
+ #g = !1;
1203
+ #_ = !0;
1204
+ #k = 0.15;
1205
+ #h = 0.5;
1206
+ #a = 0.4;
1207
+ #s = 1.5;
1208
+ #i = { hsv: 0, okhsv: 1, hsl: 2, okhsl: 3, oklch: 4 };
1209
+ #z = { rgb: 0, oklab: 1, deltaE76: 2, deltaE2000: 3, kotsarenkoRamos: 4 };
1210
+ // compute pass
1211
+ #d;
1212
+ #o;
1213
+ #c;
1214
+ #M;
1215
+ #E;
1216
+ #m;
1217
+ // display pass
1218
+ #L;
1219
+ #y;
1220
+ #e = [];
1221
+ #v;
1222
+ #S;
1223
+ #n;
1224
+ #w = null;
1225
+ #U;
1226
+ #b = null;
1227
+ constructor({
1228
+ palette: t = M(),
1229
+ width: e = 512,
1230
+ height: o = 512,
1231
+ pixelRatio: i = window.devicePixelRatio,
1232
+ container: n = document.body,
1233
+ colorModel: s = "okhsv",
1234
+ distanceMetric: a = "oklab",
1235
+ invertLightness: l = !1,
1236
+ showRaw: c = !1,
1237
+ isPolar: _ = !0,
1238
+ yaw: f = 0.5,
1239
+ pitch: r = 0.4,
1240
+ zoom: h = 1.5,
1241
+ slices: b = 32,
1242
+ sliceResolution: p = 64,
1243
+ sliceOpacity: g
1244
+ } = {}) {
1245
+ this.#t = t, this.#r = e, this.#l = o, this.#P = i, this.#C = s, this.#p = a, this.#u = l, this.#g = c, this.#_ = _, this.#h = f, this.#a = r, this.#s = h, this.#f = Math.max(2, b), this.#x = p, this.#k = g ?? Math.min(1, 5 / this.#f), this.#U = n, this.#d = d(this.#t), this.#D(), this.#N(), this.#G(), this.#T();
1246
+ }
1247
+ // ── Atlas precomputation ───────────────────────────────────────────────────
1248
+ #G() {
1249
+ this.#o.paletteTexture.value = this.#d, this.#o.paletteLength.value = this.#t.length, this.#o.polarColorModel.value = this.#i[this.#C], this.#o.distanceMetric.value = this.#z[this.#p], this.#o.debug.value = this.#g, this.#o.invertZ.value = this.#u, this.#o.isPolar.value = this.#_, this.#n.setRenderTarget(this.#m), this.#n.render(this.#M, this.#E), this.#n.setRenderTarget(null);
1250
+ }
1251
+ // Recompute atlas then repaint — called for any setting that changes the Voronoi.
1252
+ #R() {
1253
+ this.#G(), this.#T();
1254
+ }
1255
+ // ── Camera ────────────────────────────────────────────────────────────────
1256
+ #I() {
1257
+ const t = this.#s * Math.sin(this.#h) * Math.cos(this.#a), e = this.#s * Math.sin(this.#a), o = this.#s * Math.cos(this.#h) * Math.cos(this.#a);
1258
+ this.#S.position.set(t, e, o), this.#S.lookAt(0, 0, 0);
1259
+ }
1260
+ #T() {
1261
+ this.#w !== null && cancelAnimationFrame(this.#w), this.#w = requestAnimationFrame(() => {
1262
+ const t = this.#e.length;
1263
+ this.#e.forEach((e, o) => {
1264
+ e.renderOrder = this.#a >= 0 ? o : t - 1 - o;
1265
+ }), this.#n.render(this.#v, this.#S);
1266
+ });
1267
+ }
1268
+ // ── Three.js setup ────────────────────────────────────────────────────────
1269
+ #D() {
1270
+ this.#n = new w({ antialias: !0 }), this.#n.setPixelRatio(this.#P), this.#n.setSize(this.#r, this.#l), this.#n.domElement.classList.add("palette-viz-3d"), this.#U.appendChild(this.#n.domElement), this.#m = new R(
1271
+ this.#x,
1272
+ this.#x * this.#f,
1273
+ { minFilter: T, magFilter: T }
1274
+ ), this.#o = {
1275
+ slices: { value: this.#f },
1276
+ isPolar: { value: this.#_ },
1277
+ distanceMetric: { value: this.#z[this.#p] },
1278
+ paletteTexture: { value: this.#d },
1279
+ paletteLength: { value: this.#t.length },
1280
+ debug: { value: this.#g },
1281
+ polarColorModel: { value: this.#i[this.#C] },
1282
+ invertZ: { value: this.#u }
1283
+ }, this.#c = new v({
1284
+ uniforms: this.#o,
1285
+ vertexShader: W,
1286
+ fragmentShader: q
1287
+ }), this.#M = new m(), this.#E = new k(-1, 1, 1, -1, 0.1, 1), this.#M.add(new C(new y(2, 2), this.#c)), this.#L = {
1288
+ atlas: { value: this.#m.texture },
1289
+ slices: { value: this.#f },
1290
+ opacity: { value: this.#k },
1291
+ isPolar: { value: this.#_ }
1292
+ }, this.#y = new v({
1293
+ uniforms: this.#L,
1294
+ vertexShader: nt,
1295
+ fragmentShader: st,
1296
+ transparent: !0,
1297
+ depthWrite: !1,
1298
+ side: H
1299
+ });
1300
+ const t = this.#f, e = t > 1 ? 1.001 / (t - 1) : 1;
1301
+ this.#v = new m(), this.#S = new P(60, this.#r / this.#l, 0.01, 10), this.#e = [];
1302
+ for (let o = 0; o < t; o++) {
1303
+ const i = new C(new B(1, e, 1), this.#y);
1304
+ i.position.y = t > 1 ? o / (t - 1) - 0.5 : 0, this.#v.add(i), this.#e.push(i);
1305
+ }
1306
+ this.#I();
1307
+ }
1308
+ #N() {
1309
+ const t = this.#n.domElement;
1310
+ t.addEventListener("pointerdown", (e) => {
1311
+ this.#b = { x: e.clientX, y: e.clientY, yaw: this.#h, pitch: this.#a }, t.setPointerCapture(e.pointerId);
1312
+ }), t.addEventListener("pointermove", (e) => {
1313
+ if (!this.#b) return;
1314
+ const o = e.clientX - this.#b.x, i = e.clientY - this.#b.y;
1315
+ this.#h = this.#b.yaw - o * 5e-3, this.#a = Math.max(-Math.PI / 2.1, Math.min(
1316
+ Math.PI / 2.1,
1317
+ this.#b.pitch + i * 5e-3
1318
+ )), this.#I(), this.#T();
1319
+ }), t.addEventListener("pointerup", () => {
1320
+ this.#b = null;
1321
+ }), t.addEventListener("pointercancel", () => {
1322
+ this.#b = null;
1323
+ }), t.addEventListener("wheel", (e) => {
1324
+ e.preventDefault(), this.#s = Math.max(0.2, Math.min(5, this.#s + e.deltaY * 1e-3)), this.#I(), this.#T();
1325
+ }, { passive: !1 });
1326
+ }
1327
+ // ── Public API ─────────────────────────────────────────────────────────────
1328
+ get canvas() {
1329
+ return this.#n.domElement;
1330
+ }
1331
+ get width() {
1332
+ return this.#r;
1333
+ }
1334
+ get height() {
1335
+ return this.#l;
1336
+ }
1337
+ resize(t, e = null) {
1338
+ this.#r = t, this.#l = e ?? t, this.#n.setSize(this.#r, this.#l), this.#S.aspect = this.#r / this.#l, this.#S.updateProjectionMatrix(), this.#T();
1339
+ }
1340
+ destroy() {
1341
+ this.#w !== null && (cancelAnimationFrame(this.#w), this.#w = null), this.#d.dispose(), this.#m.dispose(), this.#c.dispose(), this.#e.forEach((t) => t.geometry.dispose()), this.#y.dispose(), this.#n.dispose(), this.#n.domElement.remove();
1342
+ }
1343
+ // ── Palette ────────────────────────────────────────────────────────────────
1344
+ set palette(t) {
1345
+ this.#t = t, this.#d = d(t), this.#R();
1346
+ }
1347
+ get palette() {
1348
+ return this.#t;
1349
+ }
1350
+ setColor(t, e) {
1351
+ if (e < 0 || e >= this.#t.length) throw new Error(`Index ${e} out of range`);
1352
+ this.#t[e] = t, this.#d = d(this.#t), this.#R();
1353
+ }
1354
+ // ── Shader properties — all trigger atlas recomputation ───────────────────
1355
+ set colorModel(t) {
1356
+ this.#C = t, this.#R();
1357
+ }
1358
+ get colorModel() {
1359
+ return this.#C;
1360
+ }
1361
+ set distanceMetric(t) {
1362
+ this.#p = t, this.#R();
1363
+ }
1364
+ get distanceMetric() {
1365
+ return this.#p;
1366
+ }
1367
+ set isPolar(t) {
1368
+ this.#_ = t, this.#L.isPolar.value = t, this.#R();
1369
+ }
1370
+ get isPolar() {
1371
+ return this.#_;
1372
+ }
1373
+ set invertLightness(t) {
1374
+ this.#u = t, this.#R();
1375
+ }
1376
+ get invertLightness() {
1377
+ return this.#u;
1378
+ }
1379
+ set showRaw(t) {
1380
+ this.#g = t, this.#R();
1381
+ }
1382
+ get showRaw() {
1383
+ return this.#g;
1384
+ }
1385
+ // ── Orbit — cheap repaint only, no recomputation ──────────────────────────
1386
+ set yaw(t) {
1387
+ this.#h = t, this.#I(), this.#T();
1388
+ }
1389
+ get yaw() {
1390
+ return this.#h;
1391
+ }
1392
+ set pitch(t) {
1393
+ this.#a = Math.max(-Math.PI / 2.1, Math.min(Math.PI / 2.1, t)), this.#I(), this.#T();
1394
+ }
1395
+ get pitch() {
1396
+ return this.#a;
1397
+ }
1398
+ set zoom(t) {
1399
+ this.#s = Math.max(0.2, Math.min(5, t)), this.#I(), this.#T();
1400
+ }
1401
+ get zoom() {
1402
+ return this.#s;
1403
+ }
1404
+ }
1405
+ class rt {
1406
+ #t = [];
1407
+ #r = 512;
1408
+ #l = 512;
1409
+ #P = 1;
1410
+ #f = 32;
1411
+ #x = 8e-3;
1412
+ #C = 8;
1413
+ #p = "okhsv";
1414
+ #u = "oklab";
1415
+ #g = !1;
1416
+ #_ = !1;
1417
+ #k = !0;
1418
+ #h = 0.5;
1419
+ #a = 0.4;
1420
+ #s = 1.5;
1421
+ #i = { hsv: 0, okhsv: 1, hsl: 2, okhsl: 3, oklch: 4 };
1422
+ #z = { rgb: 0, oklab: 1, deltaE76: 2, deltaE2000: 3, kotsarenkoRamos: 4 };
1423
+ // compute pass — reuses module-level shader strings and ComputeUniforms3D type
1424
+ #d;
1425
+ #o;
1426
+ #c;
1427
+ #M;
1428
+ #E;
1429
+ #m;
1430
+ // display
1431
+ #L;
1432
+ #y;
1433
+ #e = null;
1434
+ #v = null;
1435
+ #S;
1436
+ #n;
1437
+ #w = null;
1438
+ #U;
1439
+ #b = null;
1440
+ constructor({
1441
+ palette: t = M(),
1442
+ width: e = 512,
1443
+ height: o = 512,
1444
+ pixelRatio: i = window.devicePixelRatio,
1445
+ container: n = document.body,
1446
+ colorModel: s = "okhsv",
1447
+ distanceMetric: a = "oklab",
1448
+ invertLightness: l = !1,
1449
+ showRaw: c = !1,
1450
+ isPolar: _ = !0,
1451
+ yaw: f = 0.5,
1452
+ pitch: r = 0.4,
1453
+ zoom: h = 1.5,
1454
+ resolution: b = 32,
1455
+ tubeRadius: p = 8e-3,
1456
+ tubeSegments: g = 8
1457
+ } = {}) {
1458
+ this.#t = t, this.#r = e, this.#l = o, this.#P = i, this.#p = s, this.#u = a, this.#g = l, this.#_ = c, this.#k = _, this.#h = f, this.#a = r, this.#s = h, this.#f = Math.max(4, b), this.#x = p, this.#C = g, this.#U = n, this.#d = d(this.#t), this.#G(), this.#W(), this.#A();
1459
+ }
1460
+ #G() {
1461
+ this.#n = new w({ antialias: !0 }), this.#n.setPixelRatio(this.#P), this.#n.setSize(this.#r, this.#l), this.#n.setClearColor(1118481), this.#n.domElement.classList.add("palette-viz-voronoi"), this.#U.appendChild(this.#n.domElement);
1462
+ const t = this.#f;
1463
+ this.#m = new R(t, t * t, {
1464
+ minFilter: L,
1465
+ magFilter: L
1466
+ }), this.#o = {
1467
+ slices: { value: t },
1468
+ isPolar: { value: this.#k },
1469
+ distanceMetric: { value: this.#z[this.#u] },
1470
+ paletteTexture: { value: this.#d },
1471
+ paletteLength: { value: this.#t.length },
1472
+ debug: { value: this.#_ },
1473
+ polarColorModel: { value: this.#i[this.#p] },
1474
+ invertZ: { value: this.#g }
1475
+ }, this.#c = new v({
1476
+ uniforms: this.#o,
1477
+ vertexShader: W,
1478
+ fragmentShader: q
1479
+ }), this.#M = new m(), this.#E = new k(-1, 1, 1, -1, 0.1, 1), this.#M.add(new C(new y(2, 2), this.#c)), this.#L = new m(), this.#y = new P(60, this.#r / this.#l, 0.01, 10), this.#L.add(new $(16777215, 0.5));
1480
+ const e = new V(16777215, 1.5);
1481
+ e.position.set(2, 3, 2), this.#L.add(e), this.#S = new Y({ roughness: 0.35, metalness: 0.1, vertexColors: !0 }), this.#F();
1482
+ }
1483
+ #R() {
1484
+ this.#o.paletteTexture.value = this.#d, this.#o.paletteLength.value = this.#t.length, this.#o.polarColorModel.value = this.#i[this.#p], this.#o.distanceMetric.value = this.#z[this.#u], this.#o.debug.value = this.#_, this.#o.invertZ.value = this.#g, this.#o.isPolar.value = this.#k, this.#n.setRenderTarget(this.#m), this.#n.render(this.#M, this.#E), this.#n.setRenderTarget(null);
1485
+ }
1486
+ // Returns an encoded color label (R<<16|G<<8|B) for a voxel, or null if
1487
+ // the voxel is out of bounds or (in polar mode) outside the unit disc.
1488
+ #I(t, e, o, i) {
1489
+ const n = this.#f;
1490
+ if (e < 0 || e >= n || o < 0 || o >= n || i < 0 || i >= n) return null;
1491
+ if (this.#k) {
1492
+ const a = (e + 0.5) / n - 0.5, l = (o + 0.5) / n - 0.5;
1493
+ if (Math.sqrt(a * a + l * l) * 2 > 1) return null;
1494
+ }
1495
+ const s = ((i * n + o) * n + e) * 4;
1496
+ return t[s] << 16 | t[s + 1] << 8 | t[s + 2];
1497
+ }
1498
+ // Atlas corner (cx, cy, cz) → Three.js world position.
1499
+ // Mirrors the display shader: sliceUV = worldPos.xz + 0.5, progress = worldPos.y + 0.5
1500
+ #T(t, e, o) {
1501
+ const i = this.#f;
1502
+ return new x(t / i - 0.5, o / i - 0.5, e / i - 0.5);
1503
+ }
1504
+ // Walk all 3 families of axis-aligned grid edges.
1505
+ // An edge is a Voronoi edge when its 4 surrounding voxels have ≥ 3 distinct labels.
1506
+ #D(t) {
1507
+ const e = this.#f, o = [], i = (n, s, a) => {
1508
+ const l = [];
1509
+ for (const [r, h, b] of n) {
1510
+ const p = this.#I(t, r, h, b);
1511
+ p !== null && l.push(p);
1512
+ }
1513
+ if (new Set(l).size < 3) return;
1514
+ const c = /* @__PURE__ */ new Map();
1515
+ for (const r of l) c.set(r, (c.get(r) ?? 0) + 1);
1516
+ let _ = l[0], f = 0;
1517
+ for (const [r, h] of c) h > f && (_ = r, f = h);
1518
+ o.push({ p0: this.#T(...s), p1: this.#T(...a), label: _ });
1519
+ };
1520
+ for (let n = 0; n <= e; n++)
1521
+ for (let s = 0; s <= e; s++)
1522
+ for (let a = 0; a < e; a++)
1523
+ i(
1524
+ [[n - 1, s - 1, a], [n, s - 1, a], [n - 1, s, a], [n, s, a]],
1525
+ [n, s, a],
1526
+ [n, s, a + 1]
1527
+ );
1528
+ for (let n = 0; n <= e; n++)
1529
+ for (let s = 0; s <= e; s++)
1530
+ for (let a = 0; a < e; a++)
1531
+ i(
1532
+ [[a, n - 1, s - 1], [a, n, s - 1], [a, n - 1, s], [a, n, s]],
1533
+ [a, n, s],
1534
+ [a + 1, n, s]
1535
+ );
1536
+ for (let n = 0; n <= e; n++)
1537
+ for (let s = 0; s <= e; s++)
1538
+ for (let a = 0; a < e; a++)
1539
+ i(
1540
+ [[n - 1, a, s - 1], [n, a, s - 1], [n - 1, a, s], [n, a, s]],
1541
+ [n, a, s],
1542
+ [n, a + 1, s]
1543
+ );
1544
+ return o;
1545
+ }
1546
+ #N(t) {
1547
+ if (this.#e && (this.#L.remove(this.#e), this.#e.geometry.dispose(), this.#e = null), this.#v && (this.#L.remove(this.#v), this.#v.geometry.dispose(), this.#v = null), t.length === 0) return;
1548
+ const e = new Z(this.#x, this.#x, 1, this.#C), o = new K(this.#x, this.#C, this.#C >> 1), i = new j(), n = new x(0, 1, 0), s = new x(), a = new I();
1549
+ this.#e = new z(e, this.#S, t.length);
1550
+ const l = /* @__PURE__ */ new Map();
1551
+ let c = 0;
1552
+ for (let f = 0; f < t.length; f++) {
1553
+ const { p0: r, p1: h, label: b } = t[f];
1554
+ s.copy(h).sub(r);
1555
+ const p = s.length();
1556
+ if (p < 1e-7) continue;
1557
+ i.position.copy(r).add(h).multiplyScalar(0.5), i.scale.set(1, p, 1), i.quaternion.setFromUnitVectors(n, s.divideScalar(p)), i.updateMatrix(), this.#e.setMatrixAt(c, i.matrix), a.setRGB((b >> 16 & 255) / 255, (b >> 8 & 255) / 255, (b & 255) / 255), this.#e.setColorAt(c, a), c++;
1558
+ const g = `${r.x.toFixed(5)},${r.y.toFixed(5)},${r.z.toFixed(5)}`, S = `${h.x.toFixed(5)},${h.y.toFixed(5)},${h.z.toFixed(5)}`;
1559
+ l.has(g) || l.set(g, { pos: r, label: b }), l.has(S) || l.set(S, { pos: h, label: b });
1560
+ }
1561
+ this.#e.count = c, this.#e.instanceMatrix.needsUpdate = !0, this.#e.instanceColor && (this.#e.instanceColor.needsUpdate = !0), this.#L.add(this.#e);
1562
+ const _ = Array.from(l.values());
1563
+ this.#v = new z(o, this.#S, _.length), i.scale.set(1, 1, 1), i.quaternion.set(0, 0, 0, 1);
1564
+ for (let f = 0; f < _.length; f++) {
1565
+ const { pos: r, label: h } = _[f];
1566
+ i.position.copy(r), i.updateMatrix(), this.#v.setMatrixAt(f, i.matrix), a.setRGB((h >> 16 & 255) / 255, (h >> 8 & 255) / 255, (h & 255) / 255), this.#v.setColorAt(f, a);
1567
+ }
1568
+ this.#v.instanceMatrix.needsUpdate = !0, this.#v.instanceColor && (this.#v.instanceColor.needsUpdate = !0), this.#L.add(this.#v);
1569
+ }
1570
+ #A() {
1571
+ this.#R();
1572
+ const t = this.#f, e = new Uint8Array(t * t * t * 4);
1573
+ this.#n.readRenderTargetPixels(this.#m, 0, 0, t, t * t, e);
1574
+ const o = this.#D(e);
1575
+ this.#N(o), this.#O();
1576
+ }
1577
+ #F() {
1578
+ const t = this.#s * Math.sin(this.#h) * Math.cos(this.#a), e = this.#s * Math.sin(this.#a), o = this.#s * Math.cos(this.#h) * Math.cos(this.#a);
1579
+ this.#y.position.set(t, e, o), this.#y.lookAt(0, 0, 0);
1580
+ }
1581
+ #O() {
1582
+ this.#w !== null && cancelAnimationFrame(this.#w), this.#w = requestAnimationFrame(() => {
1583
+ this.#n.render(this.#L, this.#y);
1584
+ });
1585
+ }
1586
+ #W() {
1587
+ const t = this.#n.domElement;
1588
+ t.addEventListener("pointerdown", (e) => {
1589
+ this.#b = { x: e.clientX, y: e.clientY, yaw: this.#h, pitch: this.#a }, t.setPointerCapture(e.pointerId);
1590
+ }), t.addEventListener("pointermove", (e) => {
1591
+ if (!this.#b) return;
1592
+ const o = e.clientX - this.#b.x, i = e.clientY - this.#b.y;
1593
+ this.#h = this.#b.yaw - o * 5e-3, this.#a = Math.max(-Math.PI / 2.1, Math.min(
1594
+ Math.PI / 2.1,
1595
+ this.#b.pitch + i * 5e-3
1596
+ )), this.#F(), this.#O();
1597
+ }), t.addEventListener("pointerup", () => {
1598
+ this.#b = null;
1599
+ }), t.addEventListener("pointercancel", () => {
1600
+ this.#b = null;
1601
+ }), t.addEventListener("wheel", (e) => {
1602
+ e.preventDefault(), this.#s = Math.max(0.2, Math.min(5, this.#s + e.deltaY * 1e-3)), this.#F(), this.#O();
1603
+ }, { passive: !1 });
1604
+ }
1605
+ // ── Public API ─────────────────────────────────────────────────────────────
1606
+ get canvas() {
1607
+ return this.#n.domElement;
1608
+ }
1609
+ get width() {
1610
+ return this.#r;
1611
+ }
1612
+ get height() {
1613
+ return this.#l;
1614
+ }
1615
+ resize(t, e = null) {
1616
+ this.#r = t, this.#l = e ?? t, this.#n.setSize(this.#r, this.#l), this.#y.aspect = this.#r / this.#l, this.#y.updateProjectionMatrix(), this.#O();
1617
+ }
1618
+ destroy() {
1619
+ this.#w !== null && (cancelAnimationFrame(this.#w), this.#w = null), this.#d.dispose(), this.#m.dispose(), this.#c.dispose(), this.#S.dispose(), this.#e?.geometry.dispose(), this.#v?.geometry.dispose(), this.#n.dispose(), this.#n.domElement.remove();
1620
+ }
1621
+ // ── Palette ────────────────────────────────────────────────────────────────
1622
+ set palette(t) {
1623
+ this.#t = t, this.#d = d(t), this.#A();
1624
+ }
1625
+ get palette() {
1626
+ return this.#t;
1627
+ }
1628
+ setColor(t, e) {
1629
+ if (e < 0 || e >= this.#t.length) throw new Error(`Index ${e} out of range`);
1630
+ this.#t[e] = t, this.#d = d(this.#t), this.#A();
1631
+ }
1632
+ // ── Shader properties — all trigger a full rebuild ─────────────────────────
1633
+ set colorModel(t) {
1634
+ this.#p = t, this.#A();
1635
+ }
1636
+ get colorModel() {
1637
+ return this.#p;
1638
+ }
1639
+ set distanceMetric(t) {
1640
+ this.#u = t, this.#A();
1641
+ }
1642
+ get distanceMetric() {
1643
+ return this.#u;
1644
+ }
1645
+ set isPolar(t) {
1646
+ this.#k = t, this.#A();
1647
+ }
1648
+ get isPolar() {
1649
+ return this.#k;
1650
+ }
1651
+ set invertLightness(t) {
1652
+ this.#g = t, this.#A();
1653
+ }
1654
+ get invertLightness() {
1655
+ return this.#g;
1656
+ }
1657
+ set showRaw(t) {
1658
+ this.#_ = t, this.#A();
1659
+ }
1660
+ get showRaw() {
1661
+ return this.#_;
1662
+ }
1663
+ // ── Orbit — no recomputation ───────────────────────────────────────────────
1664
+ set yaw(t) {
1665
+ this.#h = t, this.#F(), this.#O();
1666
+ }
1667
+ get yaw() {
1668
+ return this.#h;
1669
+ }
1670
+ set pitch(t) {
1671
+ this.#a = Math.max(-Math.PI / 2.1, Math.min(Math.PI / 2.1, t)), this.#F(), this.#O();
1672
+ }
1673
+ get pitch() {
1674
+ return this.#a;
1675
+ }
1676
+ set zoom(t) {
1677
+ this.#s = Math.max(0.2, Math.min(5, t)), this.#F(), this.#O();
1678
+ }
1679
+ get zoom() {
1680
+ return this.#s;
1681
+ }
1682
+ }
1683
+ export {
1684
+ it as PaletteViz,
1685
+ ot as PaletteViz3D,
1686
+ rt as PaletteViz3DVoronoi,
1687
+ tt as fragmentShader,
1688
+ d as paletteToTexture,
1689
+ M as randomPalette
1690
+ };
1691
+ //# sourceMappingURL=palette-shader.js.map