@texel/color 1.0.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.
package/src/gamut.js ADDED
@@ -0,0 +1,336 @@
1
+ import {
2
+ clamp,
3
+ degToRad,
4
+ lerp,
5
+ clampedRGB,
6
+ isRGBInGamut,
7
+ vec3,
8
+ } from "./util.js";
9
+ import { OKLab_to_LMS_M } from "./conversion_matrices.js";
10
+ import { sRGBGamut, OKLCH, OKLab } from "./spaces.js";
11
+ import { OKLab_to, convert } from "./core.js";
12
+
13
+ const DEFAULT_ALPHA = 0.05;
14
+
15
+ export const MapToL = (oklch) => oklch[0];
16
+ export const MapToGray = () => 0.5;
17
+ export const MapToCuspL = (_, cusp) => cusp[0];
18
+ export const MapToAdaptiveGray = (oklch, cusp) => {
19
+ const Ld = oklch[0] - cusp[0];
20
+ const k = 2 * (Ld > 0 ? 1 - cusp[0] : cusp[0]);
21
+ const e1 = 0.5 * k + Math.abs(Ld) + (DEFAULT_ALPHA * oklch[1]) / k;
22
+ return (
23
+ cusp[0] +
24
+ 0.5 * (Math.sign(Ld) * (e1 - Math.sqrt(e1 * e1 - 2 * k * Math.abs(Ld))))
25
+ );
26
+ };
27
+
28
+ export const MapToAdaptiveCuspL = (oklch) => {
29
+ const Ld = oklch[0] - 0.5;
30
+ const e1 = 0.5 + Math.abs(Ld) + DEFAULT_ALPHA * oklch[1];
31
+ return (
32
+ 0.5 * (1 + Math.sign(Ld) * (e1 - Math.sqrt(e1 * e1 - 2.0 * Math.abs(Ld))))
33
+ );
34
+ };
35
+
36
+ const floatMax = Number.MAX_VALUE;
37
+
38
+ const tmp2 = [0, 0];
39
+ const tmp3 = vec3();
40
+
41
+ // performs dot between vec3 A and B, but only on the YZ channels
42
+ const dotYZ = (a, b) => a[1] * b[1] + a[2] * b[2];
43
+
44
+ // regular dot product for 2D and 3D vectors
45
+ const dotXY = (a, b) => a[0] * b[0] + a[1] * b[1];
46
+ const dotXYZ = (vec, x, y, z) => vec[0] * x + vec[1] * y + vec[2] * z;
47
+
48
+ const setXY = (v, a, b) => {
49
+ v[0] = a;
50
+ v[1] = b;
51
+ };
52
+
53
+ const setYZ = (v, a, b) => {
54
+ v[1] = a;
55
+ v[2] = b;
56
+ };
57
+
58
+ export const computeMaxSaturationOKLC = (a, b, lmsToRgb, okCoeff) => {
59
+ // https://github.com/color-js/color.js/blob/main/src/spaces/okhsl.js
60
+ // Finds the maximum saturation possible for a given hue that fits in RGB.
61
+ //
62
+ // Saturation here is defined as `S = C/L`.
63
+ // `a` and `b` must be normalized so `a^2 + b^2 == 1`.
64
+
65
+ // Max saturation will be when one of r, g or b goes below zero.
66
+
67
+ // Select different coefficients depending on which component goes below zero first.
68
+
69
+ let k0, k1, k2, k3, k4, wl, wm, ws;
70
+
71
+ setXY(tmp2, a, b);
72
+ setYZ(tmp3, a, b);
73
+
74
+ let chnlCoeff, chnlLMS;
75
+ // TODO: check performance of array destructuring...
76
+ if (dotXY(okCoeff[0][0], tmp2) > 1) {
77
+ // Red component
78
+ chnlCoeff = okCoeff[0][1];
79
+ chnlLMS = lmsToRgb[0];
80
+ } else if (dotXY(okCoeff[1][0], tmp2) > 1) {
81
+ // Green component
82
+ chnlCoeff = okCoeff[1][1];
83
+ chnlLMS = lmsToRgb[1];
84
+ } else {
85
+ // Blue component
86
+ chnlCoeff = okCoeff[2][1];
87
+ chnlLMS = lmsToRgb[2];
88
+ }
89
+
90
+ k0 = chnlCoeff[0];
91
+ k1 = chnlCoeff[1];
92
+ k2 = chnlCoeff[2];
93
+ k3 = chnlCoeff[3];
94
+ k4 = chnlCoeff[4];
95
+ wl = chnlLMS[0];
96
+ wm = chnlLMS[1];
97
+ ws = chnlLMS[2];
98
+
99
+ // Approximate max saturation using a polynomial:
100
+ let sat = k0 + k1 * a + k2 * b + k3 * (a * a) + k4 * a * b;
101
+
102
+ // Do one step Halley's method to get closer.
103
+ // This gives an error less than 10e6, except for some blue hues where the `dS/dh` is close to infinite.
104
+ // This should be sufficient for most applications, otherwise do two/three steps.
105
+
106
+ let kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
107
+ let km = dotYZ(OKLab_to_LMS_M[1], tmp3);
108
+ let ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
109
+
110
+ let l_ = 1.0 + sat * kl;
111
+ let m_ = 1.0 + sat * km;
112
+ let s_ = 1.0 + sat * ks;
113
+
114
+ let l = l_ * l_ * l_;
115
+ let m = m_ * m_ * m_;
116
+ let s = s_ * s_ * s_;
117
+
118
+ let lds = 3.0 * kl * (l_ * l_);
119
+ let mds = 3.0 * km * (m_ * m_);
120
+ let sds = 3.0 * ks * (s_ * s_);
121
+
122
+ let lds2 = 6.0 * (kl * kl) * l_;
123
+ let mds2 = 6.0 * (km * km) * m_;
124
+ let sds2 = 6.0 * (ks * ks) * s_;
125
+
126
+ let f = wl * l + wm * m + ws * s;
127
+ let f1 = wl * lds + wm * mds + ws * sds;
128
+ let f2 = wl * lds2 + wm * mds2 + ws * sds2;
129
+
130
+ sat = sat - (f * f1) / (f1 * f1 - 0.5 * f * f2);
131
+
132
+ return sat;
133
+ };
134
+
135
+ export const getGamutLMStoRGB = (gamut) => {
136
+ if (!gamut) throw new Error(`expected gamut to have { space }`);
137
+ const lmsToRGB = (gamut.space.base ?? gamut.space).fromLMS_M;
138
+ if (!lmsToRGB)
139
+ throw new Error(`expected gamut { space } to have a fromLMS_M matrix`);
140
+ return lmsToRGB;
141
+ };
142
+
143
+ export const findCuspOKLCH = (a, b, gamut, out = [0, 0]) => {
144
+ const lmsToRgb = getGamutLMStoRGB(gamut);
145
+ const okCoeff = gamut.coefficients;
146
+ if (!okCoeff) throw new Error("expected gamut to have { coefficients }");
147
+ // const lmsToRgb, okCoeff
148
+ // First, find the maximum saturation (saturation S = C/L)
149
+ var S_cusp = computeMaxSaturationOKLC(a, b, lmsToRgb, okCoeff);
150
+ // Convert to linear RGB to find the first point where at least one of r,g or b >= 1:
151
+ tmp3[0] = 1;
152
+ tmp3[1] = S_cusp * a;
153
+ tmp3[2] = S_cusp * b;
154
+ var rgb_at_max = OKLab_to(tmp3, lmsToRgb, tmp3);
155
+ var L_cusp = Math.cbrt(
156
+ 1 / Math.max(Math.max(rgb_at_max[0], rgb_at_max[1]), rgb_at_max[2])
157
+ );
158
+ var C_cusp = L_cusp * S_cusp;
159
+ out[0] = L_cusp;
160
+ out[1] = C_cusp;
161
+ return out;
162
+ };
163
+
164
+ export const findGamutIntersectionOKLCH = (a, b, l1, c1, l0, cusp, gamut) => {
165
+ // Finds intersection of the line.
166
+ //
167
+ // Defined by the following:
168
+ //
169
+ // ```
170
+ // L = L0 * (1 - t) + t * L1
171
+ // C = t * C1
172
+ // ```
173
+ //
174
+ // `a` and `b` must be normalized so `a^2 + b^2 == 1`.
175
+
176
+ let t;
177
+
178
+ const lmsToRgb = getGamutLMStoRGB(gamut);
179
+ if (!cusp) throw new Error("must pass cusp");
180
+
181
+ setYZ(tmp3, a, b);
182
+
183
+ // Find the intersection for upper and lower half separately
184
+ if ((l1 - l0) * cusp[1] - (cusp[0] - l0) * c1 <= 0.0) {
185
+ // Lower half
186
+ t = (cusp[1] * l0) / (c1 * cusp[0] + cusp[1] * (l0 - l1));
187
+ } else {
188
+ // Upper half
189
+
190
+ // First intersect with triangle
191
+ t = (cusp[1] * (l0 - 1.0)) / (c1 * (cusp[0] - 1.0) + cusp[1] * (l0 - l1));
192
+
193
+ // Then one step Halley's method
194
+ let dl = l1 - l0;
195
+ let dc = c1;
196
+
197
+ let kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
198
+ let km = dotYZ(OKLab_to_LMS_M[1], tmp3);
199
+ let ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
200
+
201
+ let ldt_ = dl + dc * kl;
202
+ let mdt_ = dl + dc * km;
203
+ let sdt_ = dl + dc * ks;
204
+
205
+ // If higher accuracy is required, 2 or 3 iterations of the following block can be used:
206
+ let L = l0 * (1.0 - t) + t * l1;
207
+ let C = t * c1;
208
+
209
+ let l_ = L + C * kl;
210
+ let m_ = L + C * km;
211
+ let s_ = L + C * ks;
212
+
213
+ let l = l_ * l_ * l_;
214
+ let m = m_ * m_ * m_;
215
+ let s = s_ * s_ * s_;
216
+
217
+ let ldt = 3 * ldt_ * l_ * l_;
218
+ let mdt = 3 * mdt_ * m_ * m_;
219
+ let sdt = 3 * sdt_ * s_ * s_;
220
+
221
+ let ldt2 = 6 * ldt_ * ldt_ * l_;
222
+ let mdt2 = 6 * mdt_ * mdt_ * m_;
223
+ let sdt2 = 6 * sdt_ * sdt_ * s_;
224
+
225
+ let r_ = dotXYZ(lmsToRgb[0], l, m, s) - 1;
226
+ let r1 = dotXYZ(lmsToRgb[0], ldt, mdt, sdt);
227
+ let r2 = dotXYZ(lmsToRgb[0], ldt2, mdt2, sdt2);
228
+
229
+ let ur = r1 / (r1 * r1 - 0.5 * r_ * r2);
230
+ let tr = -r_ * ur;
231
+
232
+ let g_ = dotXYZ(lmsToRgb[1], l, m, s) - 1;
233
+ let g1 = dotXYZ(lmsToRgb[1], ldt, mdt, sdt);
234
+ let g2 = dotXYZ(lmsToRgb[1], ldt2, mdt2, sdt2);
235
+
236
+ let ug = g1 / (g1 * g1 - 0.5 * g_ * g2);
237
+ let tg = -g_ * ug;
238
+
239
+ let b_ = dotXYZ(lmsToRgb[2], l, m, s) - 1;
240
+ let b1 = dotXYZ(lmsToRgb[2], ldt, mdt, sdt);
241
+ let b2 = dotXYZ(lmsToRgb[2], ldt2, mdt2, sdt2);
242
+
243
+ let ub = b1 / (b1 * b1 - 0.5 * b_ * b2);
244
+ let tb = -b_ * ub;
245
+
246
+ tr = ur >= 0.0 ? tr : floatMax;
247
+ tg = ug >= 0.0 ? tg : floatMax;
248
+ tb = ub >= 0.0 ? tb : floatMax;
249
+
250
+ t += Math.min(tr, Math.min(tg, tb));
251
+ }
252
+
253
+ return t;
254
+ };
255
+
256
+ /**
257
+ * Takes any OKLCH value and maps it to fall within the given gamut.
258
+ */
259
+ export const gamutMapOKLCH = (
260
+ oklch,
261
+ gamut = sRGBGamut,
262
+ targetSpace = gamut.space,
263
+ out = vec3(),
264
+ mapping = MapToCuspL,
265
+ cusp
266
+ ) => {
267
+ const gamutSpace = gamut.space;
268
+ const coeff = gamut.coefficients;
269
+ if (!coeff || !gamutSpace) {
270
+ throw new Error(`expected gamut with { space, coefficients }`);
271
+ }
272
+ const gamutSpaceBase = gamutSpace.base ?? gamutSpace;
273
+ const lmsToRgb = gamutSpaceBase.fromLMS_M;
274
+ if (!lmsToRgb) {
275
+ throw new Error(
276
+ `color space ${outSpace.id} has no base with LMS to RGB matrix`
277
+ );
278
+ }
279
+
280
+ // tmp output for R,G,B
281
+ const rgbVec = tmp3;
282
+
283
+ // first, let's clamp lightness and chroma
284
+ out[0] = clamp(oklch[0], 0, 1);
285
+ out[1] = Math.max(oklch[1], 0);
286
+ out[2] = oklch[2]; // hue remains constant
287
+
288
+ // convert oklch to base gamut space (i.e. linear sRGB)
289
+ convert(out, OKLCH, gamutSpaceBase, rgbVec);
290
+
291
+ // check where the point lies in gamut space
292
+ if (!isRGBInGamut(rgbVec, 0)) {
293
+ // we aren't in gamut, so let's map toward it
294
+ const L = out[0];
295
+ const C = out[1];
296
+ const H = out[2];
297
+ const hueAngle = degToRad(H);
298
+ const aNorm = Math.cos(hueAngle);
299
+ const bNorm = Math.sin(hueAngle);
300
+
301
+ // choose our strategy
302
+ cusp = cusp || findCuspOKLCH(aNorm, bNorm, gamut, tmp2);
303
+ const LTarget = mapping(out, cusp);
304
+
305
+ let t = findGamutIntersectionOKLCH(
306
+ aNorm,
307
+ bNorm,
308
+ L,
309
+ C,
310
+ LTarget,
311
+ cusp,
312
+ gamut
313
+ );
314
+ out[0] = lerp(LTarget, L, t);
315
+ out[1] *= t;
316
+
317
+ // special case: if requested targetSpace is base=OKLCH, we can return early.
318
+ // note this creates a potential difference compared to other targetSpaces, which
319
+ // will be clipped in RGB before converting to the target space.
320
+ // however, due to floating point arithmetic, a user doing OKLCH -> RGB will still
321
+ // need to clip the result again anyways, so perhaps this difference is negligible.
322
+ const targetSpaceBase = targetSpace.base ?? targetSpaceBase;
323
+ if (targetSpaceBase == OKLab) {
324
+ return convert(out, OKLCH, targetSpace, out);
325
+ }
326
+
327
+ // now that we have a LCH that sits on the gamut, convert again to linear space
328
+ convert(out, OKLCH, gamutSpaceBase, rgbVec);
329
+ }
330
+ // clip the linear RGB to 0..1 range
331
+ clampedRGB(rgbVec, rgbVec);
332
+ // finally, convert linear RGB to the final target space (e.g. sRGB or XYZ)
333
+ // this is often just a linear to gamma transfer, unless another target space is specified
334
+ convert(rgbVec, gamutSpaceBase, targetSpace, out);
335
+ return out;
336
+ };
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./conversion_matrices.js";
2
+ export * from "./spaces.js";
3
+ export * from "./gamut.js";
4
+ export * from "./core.js";
5
+ export * from "./okhsl.js";
6
+ export * from "./util.js";
package/src/okhsl.js ADDED
@@ -0,0 +1,345 @@
1
+ import { vec3, constrainAngle as constrain } from "./util.js";
2
+ import { OKLab_to } from "./core.js";
3
+ import { sRGBGamut } from "./spaces.js";
4
+ import {
5
+ findCuspOKLCH,
6
+ findGamutIntersectionOKLCH,
7
+ getGamutLMStoRGB,
8
+ } from "./gamut.js";
9
+
10
+ const K1 = 0.206;
11
+ const K2 = 0.03;
12
+ const K3 = (1.0 + K1) / (1.0 + K2);
13
+
14
+ const tmp2A = [0, 0];
15
+ const tmp2B = [0, 0];
16
+ const tmp3A = vec3();
17
+ const tmp2Cusp = [0, 0];
18
+
19
+ const tau = 2 * Math.PI;
20
+
21
+ const copySign = (to, from) => (Math.sign(to) === Math.sign(from) ? to : -to);
22
+
23
+ const spow = (base, exp) => copySign(Math.abs(base) ** exp, base);
24
+
25
+ const toe = (x) =>
26
+ 0.5 *
27
+ (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x));
28
+
29
+ const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2));
30
+
31
+ const computeSt = (cusp, out) => {
32
+ // To ST.
33
+ let l = cusp[0];
34
+ let c = cusp[1];
35
+ out[0] = c / l;
36
+ out[1] = c / (1 - l);
37
+ };
38
+
39
+ const toScaleL = (lv, cv, a_, b_, lmsToRgb) => {
40
+ let lvt = toeInv(lv);
41
+ let cvt = (cv * lvt) / lv;
42
+
43
+ // RGB scale
44
+ tmp3A[0] = lvt;
45
+ tmp3A[1] = a_ * cvt;
46
+ tmp3A[2] = b_ * cvt;
47
+ let ret = OKLab_to(tmp3A, lmsToRgb, tmp3A);
48
+ return spow(
49
+ 1.0 / Math.max(Math.max(ret[0], ret[1]), Math.max(ret[2], 0.0)),
50
+ 1 / 3
51
+ );
52
+ };
53
+
54
+ const computeStMid = (a, b, out) => {
55
+ // Returns a smooth approximation of the location of the cusp.
56
+ //
57
+ // This polynomial was created by an optimization process.
58
+ // It has been designed so that S_mid < S_max and T_mid < T_max.
59
+
60
+ let s =
61
+ 0.11516993 +
62
+ 1.0 /
63
+ (7.4477897 +
64
+ 4.1590124 * b +
65
+ a *
66
+ (-2.19557347 +
67
+ 1.75198401 * b +
68
+ a *
69
+ (-2.13704948 -
70
+ 10.02301043 * b +
71
+ a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a))));
72
+
73
+ let t =
74
+ 0.11239642 +
75
+ 1.0 /
76
+ (1.6132032 -
77
+ 0.68124379 * b +
78
+ a *
79
+ (0.40370612 +
80
+ 0.90148123 * b +
81
+ a *
82
+ (-0.27087943 +
83
+ 0.6122399 * b +
84
+ a * (0.00299215 - 0.45399568 * b - 0.14661872 * a))));
85
+
86
+ out[0] = s;
87
+ out[1] = t;
88
+ };
89
+
90
+ const getCs = (l, a, b, cusp, gamut) => {
91
+ // Get Cs
92
+ let cMax = findGamutIntersectionOKLCH(a, b, l, 1, l, cusp, gamut);
93
+ let stMax = tmp2A;
94
+ computeSt(cusp, stMax);
95
+
96
+ // Scale factor to compensate for the curved part of gamut shape:
97
+ let k = cMax / Math.min(l * stMax[0], (1 - l) * stMax[1]);
98
+
99
+ const stMid = tmp2B;
100
+ computeStMid(a, b, stMid);
101
+
102
+ // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
103
+ let ca = l * stMid[0];
104
+ let cb = (1.0 - l) * stMid[1];
105
+ let cMid =
106
+ 0.9 * k * Math.sqrt(Math.sqrt(1.0 / (1.0 / ca ** 4 + 1.0 / cb ** 4)));
107
+
108
+ // For `C_0`, the shape is independent of hue, so `ST` are constant.
109
+ // Values picked to roughly be the average values of `ST`.
110
+ ca = l * 0.4;
111
+ cb = (1.0 - l) * 0.8;
112
+
113
+ // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
114
+ let c0 = Math.sqrt(1.0 / (1.0 / ca ** 2 + 1.0 / cb ** 2));
115
+
116
+ return [c0, cMid, cMax];
117
+ };
118
+
119
+ export const OKHSLToOKLab = (hsl, gamut = sRGBGamut, out = vec3()) => {
120
+ // Convert Okhsl to Oklab.
121
+ let h = hsl[0],
122
+ s = hsl[1],
123
+ l = hsl[2];
124
+ let L = toeInv(l);
125
+ let a = 0;
126
+ let b = 0;
127
+ h = constrain(h) / 360.0;
128
+
129
+ if (L !== 0.0 && L !== 1.0 && s !== 0) {
130
+ let a_ = Math.cos(tau * h);
131
+ let b_ = Math.sin(tau * h);
132
+
133
+ const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
134
+ let Cs = getCs(L, a_, b_, cusp, gamut);
135
+ let c0 = Cs[0],
136
+ cMid = Cs[1],
137
+ cMax = Cs[2];
138
+
139
+ // Interpolate the three values for C so that:
140
+ // ```
141
+ // At s=0: dC/ds = C_0, C=0
142
+ // At s=0.8: C=C_mid
143
+ // At s=1.0: C=C_max
144
+ // ```
145
+
146
+ let mid = 0.8;
147
+ let midInv = 1.25;
148
+ let t, k0, k1, k2;
149
+
150
+ if (s < mid) {
151
+ t = midInv * s;
152
+ k0 = 0.0;
153
+ k1 = mid * c0;
154
+ k2 = 1.0 - k1 / cMid;
155
+ } else {
156
+ t = 5 * (s - 0.8);
157
+ k0 = cMid;
158
+ k1 = (0.2 * cMid ** 2 * 1.25 ** 2) / c0;
159
+ k2 = 1.0 - k1 / (cMax - cMid);
160
+ }
161
+
162
+ let c = k0 + (t * k1) / (1.0 - k2 * t);
163
+
164
+ a = c * a_;
165
+ b = c * b_;
166
+ }
167
+
168
+ out[0] = L;
169
+ out[1] = a;
170
+ out[2] = b;
171
+ return out;
172
+ };
173
+
174
+ export const OKLabToOKHSL = (lab, gamut = sRGBGamut, out = vec3()) => {
175
+ // Oklab to Okhsl.
176
+
177
+ // Epsilon for lightness should approach close to 32 bit lightness
178
+ // Epsilon for saturation just needs to be sufficiently close when denoting achromatic
179
+ let εL = 1e-7;
180
+ let εS = 1e-4;
181
+ let L = lab[0];
182
+
183
+ let s = 0.0;
184
+ let l = toe(L);
185
+
186
+ let c = Math.sqrt(lab[1] ** 2 + lab[2] ** 2);
187
+ let h = 0.5 + Math.atan2(-lab[2], -lab[1]) / tau;
188
+
189
+ if (l !== 0.0 && l !== 1.0 && c !== 0) {
190
+ let a_ = lab[1] / c;
191
+ let b_ = lab[2] / c;
192
+
193
+ const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
194
+ let Cs = getCs(L, a_, b_, cusp, gamut);
195
+ let c0 = Cs[0],
196
+ cMid = Cs[1],
197
+ cMax = Cs[2];
198
+
199
+ let mid = 0.8;
200
+ let midInv = 1.25;
201
+ let k0, k1, k2, t;
202
+
203
+ if (c < cMid) {
204
+ k1 = mid * c0;
205
+ k2 = 1.0 - k1 / cMid;
206
+
207
+ t = c / (k1 + k2 * c);
208
+ s = t * mid;
209
+ } else {
210
+ k0 = cMid;
211
+ k1 = (0.2 * cMid ** 2 * midInv ** 2) / c0;
212
+ k2 = 1.0 - k1 / (cMax - cMid);
213
+
214
+ t = (c - k0) / (k1 + k2 * (c - k0));
215
+ s = mid + 0.2 * t;
216
+ }
217
+ }
218
+
219
+ const achromatic = Math.abs(s) < εS;
220
+ if (achromatic || l === 0.0 || Math.abs(1 - l) < εL) {
221
+ // Due to floating point imprecision near lightness of 1, we can end up
222
+ // with really high around white, this is to provide consistency as
223
+ // saturation can be really high for white due this imprecision.
224
+ if (!achromatic) {
225
+ s = 0.0;
226
+ }
227
+ }
228
+
229
+ h = constrain(h * 360);
230
+
231
+ out[0] = h;
232
+ out[1] = s;
233
+ out[2] = l;
234
+ return out;
235
+ };
236
+
237
+ export const OKHSVToOKLab = (hsv, gamut = sRGBGamut, out = vec3()) => {
238
+ // Convert from Okhsv to Oklab."""
239
+
240
+ let h = hsv[0],
241
+ s = hsv[1],
242
+ v = hsv[2];
243
+ h = constrain(h) / 360.0;
244
+
245
+ let l = toeInv(v);
246
+ let a = 0;
247
+ let b = 0;
248
+
249
+ // Avoid processing gray or colors with undefined hues
250
+ if (l !== 0.0 && s !== 0.0) {
251
+ let a_ = Math.cos(tau * h);
252
+ let b_ = Math.sin(tau * h);
253
+
254
+ const lmsToRgb = getGamutLMStoRGB(gamut);
255
+ const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
256
+ computeSt(cusp, tmp2A);
257
+ const sMax = tmp2A[0];
258
+ const tMax = tmp2A[1];
259
+ let s0 = 0.5;
260
+ let k = 1 - s0 / sMax;
261
+
262
+ // first we compute L and V as if the gamut is a perfect triangle:
263
+
264
+ // L, C when v==1:
265
+ let lv = 1 - (s * s0) / (s0 + tMax - tMax * k * s);
266
+ let cv = (s * tMax * s0) / (s0 + tMax - tMax * k * s);
267
+
268
+ l = v * lv;
269
+ let c = v * cv;
270
+
271
+ // then we compensate for both toe and the curved top part of the triangle:
272
+ const scaleL = toScaleL(lv, cv, a_, b_, lmsToRgb);
273
+
274
+ let lNew = toeInv(l);
275
+ c = (c * lNew) / l;
276
+ l = lNew;
277
+
278
+ l = l * scaleL;
279
+ c = c * scaleL;
280
+
281
+ a = c * a_;
282
+ b = c * b_;
283
+ }
284
+
285
+ out[0] = l;
286
+ out[1] = a;
287
+ out[2] = b;
288
+ return out;
289
+ };
290
+
291
+ export const OKLabToOKHSV = (lab, gamut = sRGBGamut, out = vec3()) => {
292
+ // Oklab to Okhsv.
293
+ const lmsToRgb = getGamutLMStoRGB(gamut);
294
+
295
+ // Epsilon for saturation just needs to be sufficiently close when denoting achromatic
296
+ let ε = 1e-4;
297
+ let l = lab[0];
298
+ let s = 0.0;
299
+ let v = toe(l);
300
+ let c = Math.sqrt(lab[1] ** 2 + lab[2] ** 2);
301
+ let h = 0.5 + Math.atan2(-lab[2], -lab[1]) / tau;
302
+
303
+ if (l !== 0.0 && l !== 1 && c !== 0.0) {
304
+ let a_ = lab[1] / c;
305
+ let b_ = lab[2] / c;
306
+
307
+ const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
308
+ computeSt(cusp, tmp2A);
309
+ const sMax = tmp2A[0];
310
+ const tMax = tmp2A[1];
311
+
312
+ let s0 = 0.5;
313
+ let k = 1 - s0 / sMax;
314
+
315
+ // first we find `L_v`, `C_v`, `L_vt` and `C_vt`
316
+ let t = tMax / (c + l * tMax);
317
+ let lv = t * l;
318
+ let cv = t * c;
319
+
320
+ const scaleL = toScaleL(lv, cv, a_, b_, lmsToRgb);
321
+
322
+ l = l / scaleL;
323
+ c = c / scaleL;
324
+
325
+ const toeL = toe(l);
326
+ c = (c * toeL) / l;
327
+ l = toeL;
328
+
329
+ // we can now compute v and s:
330
+ v = l / lv;
331
+ s = ((s0 + tMax) * cv) / (tMax * s0 + tMax * k * cv);
332
+ }
333
+
334
+ // unlike colorjs.io, we are not worknig with none-types
335
+ // if (Math.abs(s) < ε || v === 0.0) {
336
+ // h = null;
337
+ // }
338
+
339
+ h = constrain(h * 360);
340
+
341
+ out[0] = h;
342
+ out[1] = s;
343
+ out[2] = v;
344
+ return out;
345
+ };