@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/LICENSE.md +21 -0
- package/README.md +376 -0
- package/package.json +65 -0
- package/src/conversion_matrices.js +268 -0
- package/src/core.js +186 -0
- package/src/gamut.js +336 -0
- package/src/index.js +6 -0
- package/src/okhsl.js +345 -0
- package/src/spaces/a98-rgb.js +50 -0
- package/src/spaces/display-p3.js +28 -0
- package/src/spaces/oklab.js +70 -0
- package/src/spaces/prophoto-rgb.js +72 -0
- package/src/spaces/rec2020.js +47 -0
- package/src/spaces/srgb.js +29 -0
- package/src/spaces/util.js +44 -0
- package/src/spaces/xyz.js +44 -0
- package/src/spaces.js +44 -0
- package/src/util.js +119 -0
- package/test/almost-equal.js +15 -0
- package/test/banner.png +0 -0
- package/test/bench-colorjs.js +138 -0
- package/test/bench-node.js +51 -0
- package/test/bench-size.js +3 -0
- package/test/canvas-graph.js +210 -0
- package/test/logo.js +112 -0
- package/test/logo.png +0 -0
- package/test/profiles/DisplayP3.icc +0 -0
- package/test/test-colorjs.js +87 -0
- package/test/test.js +321 -0
- package/tools/__pycache__/calc_oklab_matrices.cpython-311.pyc +0 -0
- package/tools/calc_oklab_matrices.py +233 -0
- package/tools/print_matrices.py +509 -0
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
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
|
+
};
|