saturon 0.2.0 → 0.2.1

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.
@@ -1,1545 +0,0 @@
1
- import { Color } from "../Color";
2
- import { colorModels } from "../converters.js";
3
- import { EASINGS, MATRICES } from "../math.js";
4
- import { configure, extractBalancedExpression, fit, multiplyMatrices, register, registerColorBase, registerColorFunction, registerColorSpace, registerColorType, registerFitMethod, registerNamedColor, unregister, use, } from "../utils.js";
5
- describe("Color", () => {
6
- it("should define a Color instance in different ways", () => {
7
- expect(Color.from("red")).toBeInstanceOf(Color);
8
- expect(new Color("rgb", [233, 45, 92])).toBeInstanceOf(Color);
9
- expect(new Color("display-p3", [NaN, Infinity, -Infinity])).toBeInstanceOf(Color);
10
- });
11
- it("should correctly identify all supported color syntaxes", () => {
12
- const cases = [
13
- ["#ff5733", "hex-color"],
14
- ["rgb(255, 87, 51)", "rgb"],
15
- ["hsl(9, 100%, 60%)", "hsl"],
16
- ["hwb(9 10% 20%)", "hwb"],
17
- ["lab(53.23288% 80.10933 67.22006)", "lab"],
18
- ["lch(50% 80% 30)", "lch"],
19
- ["oklab(59% 0.1 0.1 / 0.5)", "oklab"],
20
- ["oklch(60% 0.15 50)", "oklch"],
21
- ["color(srgb 0.88 0.75 0.49)", "srgb"],
22
- ["color(srgb-linear 0.5 0.3 0.2)", "srgb-linear"],
23
- ["color(display-p3 0.5 0.34 0.2)", "display-p3"],
24
- ["color(rec2020 0.5 0.34 0.2)", "rec2020"],
25
- ["color(a98-rgb 0.5 0.34 0.2)", "a98-rgb"],
26
- ["color(prophoto-rgb 0.5 0.34 0.2)", "prophoto-rgb"],
27
- ["color(xyz-d65 0.37 0.4 0.42)", "xyz-d65"],
28
- ["color(xyz-d50 0.37 0.4 0.32)", "xyz-d50"],
29
- ["color(xyz 0.37 0.4 0.42)", "xyz"],
30
- ["red", "named-color"],
31
- ["color-mix(in hsl, red, blue)", "color-mix"],
32
- ["transparent", "transparent"],
33
- ["currentColor", "currentColor"],
34
- ["ButtonText", "system-color"],
35
- ["contrast-color(lime)", "contrast-color"],
36
- ["device-cmyk(0.1 0.2 0.3 0.4)", "device-cmyk"],
37
- ["light-dark(green, yellow)", "light-dark"],
38
- ];
39
- cases.forEach(([input, expected]) => {
40
- expect(Color.type(input)).toBe(expected);
41
- });
42
- });
43
- it("should correctly identify relative colors", () => {
44
- const cases = [
45
- ["color(from red a98-rgb r g b)", "a98-rgb"],
46
- ["color(from red xyz-d50 x y z / alpha)", "xyz-d50"],
47
- ["hsl(from red calc(h + s) s l)", "hsl"],
48
- ["hwb(from red h 50 b / alpha)", "hwb"],
49
- ["lab(from lch(51.51% 52.21 325.8) l a b)", "lab"],
50
- ["oklab(from oklch(100% calc(NaN) none) a calc(l * (a + b)) b / calc(alpha))", "oklab"],
51
- ];
52
- cases.forEach(([input, expected]) => {
53
- expect(Color.type(input)).toBe(expected);
54
- });
55
- });
56
- it("should regognize color space name in deeply nested color() syntax", () => {
57
- const color = Color.from("color(from color(from red srgb 1 1 1) srgb-linear r g b)");
58
- expect(color.model).toBe("srgb-linear");
59
- });
60
- it("should return correct coords", () => {
61
- const cases = [
62
- ["blanchedalmond", [255, 235, 205, 1]],
63
- ["#7a7239", [122, 114, 57, 1]],
64
- ["rgb(68% 16% 50% / 0.3)", [173, 41, 128, 0.3]],
65
- ["hsla(182, 43%, 33%, 0.8)", [182, 43, 33, 0.8]],
66
- ["hwb(228 6% 9% / 0.6)", [228, 6, 9, 0.6]],
67
- ["lab(52.23% 40.16% 59.99% / 0.5)", [52.23, 50.2, 74.9875, 0.5]],
68
- ["lch(62.23% 59.2% 126.2 / 0.5)", [62.23, 88.8, 126.2, 0.5]],
69
- ["oklab(42.1% 41% -25% / 0.5)", [0.421, 0.164, -0.1, 0.5]],
70
- ["oklch(72.32% 0.12% 247.99 / 0.5)", [0.7232, 0.00048, 247.99, 0.5]],
71
- ["color(srgb 0.7 0.2 0.5 / 0.3)", [0.7, 0.2, 0.5, 0.3]],
72
- ["color(srgb-linear 0.49 0.04 0.25 / 0.4)", [0.49, 0.04, 0.25, 0.4]],
73
- ["color(rec2020 0.6 0.3 0.4 / 0.5)", [0.6, 0.3, 0.4, 0.5]],
74
- ["color(prophoto-rgb 0.8 0.1 0.6 / 0.6)", [0.8, 0.1, 0.6, 0.6]],
75
- ["color(a98-rgb 0.5 0.4 0.7 / 0.7)", [0.5, 0.4, 0.7, 0.7]],
76
- ["color(xyz-d65 0.4 0.5 0.2 / 0.8)", [0.4, 0.5, 0.2, 0.8]],
77
- ["color(xyz-d50 0.3 0.6 0.1 / 0.9)", [0.3, 0.6, 0.1, 0.9]],
78
- ["color(xyz 0.2 0.7 0.3 / 0.2)", [0.2, 0.7, 0.3, 0.2]],
79
- ];
80
- cases.forEach(([input, expected]) => {
81
- expect(Color.from(input).toArray({ fit: "clip", precision: undefined })).toEqual(expected);
82
- });
83
- });
84
- it("should convert HEX color to RGB", () => {
85
- expect(Color.from("#ff5733").toString()).toBe("rgb(255 87 51)");
86
- });
87
- it("should output with different options", () => {
88
- const hsl = Color.from("hsl(339 83 46 / 0.5)");
89
- const lch = Color.from("lch(83 122 270)");
90
- const oklab = Color.from("oklab(0.18751241 0.22143 -0.398685234)");
91
- const xyz = Color.from("color(xyz 1.4 0.3 -0.2)");
92
- expect(hsl.toString({ legacy: true })).toBe("hsla(339, 83%, 46%, 0.5)");
93
- expect(lch.toString({ units: true })).toBe("lch(83% 122 270deg)");
94
- expect(oklab.toString({ precision: 1 })).toBe("oklab(0.2 0.2 -0.4)");
95
- expect(xyz.toString({ fit: "none" })).toBe("color(xyz 1.4 0.3 -0.2)");
96
- });
97
- it("should parse deeply nested colors", () => {
98
- const color = Color.from(`
99
- color-mix(
100
- in oklch longer hue,
101
- color(
102
- from hsl(240deg none calc(-infinity) / 0.5)
103
- display-p3
104
- r calc(g + b) 100 / alpha
105
- ),
106
- rebeccapurple 20%
107
- )
108
- `);
109
- expect(color.to("hwb")).toBeDefined();
110
- const getNestedColor = (deepness = 1) => {
111
- const randomNum = () => Math.floor(Math.random() * 49) + 1;
112
- const randomRgbSpace = () => ["srgb", "srgb-linear", "display-p3", "rec2020", "a98-rgb", "prophoto-rgb"][Math.floor(Math.random() * 6)];
113
- const randomXyzSpace = () => ["xyz-d65", "xyz-d50", "xyz"][Math.floor(Math.random() * 3)];
114
- const randomModel = () => ["hsl", "hwb", "lab", "lch", "oklab", "oklch"][Math.floor(Math.random() * 6)];
115
- const optionalAlpha = () => {
116
- return Math.random() < 0.5 ? " / alpha" : "";
117
- };
118
- const colorFns = [
119
- (inner) => `light-dark(${inner}, ${getNestedColor()})`,
120
- (inner) => `color-mix(in ${randomModel()}, ${inner} ${randomNum()}%, rebeccapurple ${randomNum()}%)`,
121
- (inner) => `contrast-color(${inner})`,
122
- (inner) => `rgb(from ${inner} r g b${optionalAlpha()})`,
123
- (inner) => `hsl(from ${inner} h s l${optionalAlpha()})`,
124
- (inner) => `hwb(from ${inner} h w b${optionalAlpha()})`,
125
- (inner) => `lab(from ${inner} l a b${optionalAlpha()})`,
126
- (inner) => `lch(from ${inner} l c h${optionalAlpha()})`,
127
- (inner) => `oklab(from ${inner} l a b${optionalAlpha()})`,
128
- (inner) => `oklch(from ${inner} l c h${optionalAlpha()})`,
129
- (inner) => `color(from ${inner} ${randomRgbSpace()} r g b${optionalAlpha()})`,
130
- (inner) => `color(from ${inner} ${randomXyzSpace()} x y z${optionalAlpha()})`,
131
- ];
132
- if (deepness <= 0) {
133
- return `hsl(120deg ${randomNum()}% ${randomNum()}%)`;
134
- }
135
- const randomFn = colorFns[Math.floor(Math.random() * colorFns.length)];
136
- const inner = getNestedColor(deepness - 1);
137
- return randomFn(inner);
138
- };
139
- for (let depth = 1; depth <= 100; depth++) {
140
- const color = Color.from(getNestedColor(depth));
141
- expect(color).toBeInstanceOf(Color);
142
- }
143
- });
144
- it("should calculate contrast ratio correctly", () => {
145
- expect(Color.from("#fff").contrast("#000")).toBeCloseTo(21);
146
- });
147
- it("should determine if a color is cool", () => {
148
- const color = Color.from("rgb(0, 0, 255)");
149
- const { h } = color.in("hsl").toObject();
150
- expect(h > 60 && h < 300).toBe(true);
151
- });
152
- it("should determine if a color is warm", () => {
153
- const color = Color.from("rgb(255, 0, 0)");
154
- const { h } = color.in("hsl").toObject();
155
- expect(h <= 60 || h >= 300).toBe(true);
156
- });
157
- it("should check color equality correctly", () => {
158
- expect(Color.from("#ff5733").equals("rgb(255, 87, 51)")).toBe(true);
159
- });
160
- it("should return random Color instance based on different options", () => {
161
- const c1 = Color.random();
162
- expect(c1).toBeInstanceOf(Color);
163
- expect(typeof c1.model).toBe("string");
164
- expect(Array.isArray(c1.coords)).toBe(true);
165
- const c2 = Color.random({ model: "oklch" });
166
- expect(c2.model).toBe("oklch");
167
- expect(c2.coords.length).toBe(4);
168
- const c3 = Color.random({
169
- model: "oklch",
170
- limits: { l: [0.4, 0.6] },
171
- });
172
- expect(c3.coords[0]).toBeGreaterThanOrEqual(0.4);
173
- expect(c3.coords[0]).toBeLessThanOrEqual(0.6);
174
- const samplesBias = Array.from({ length: 100 }, () => Color.random({
175
- model: "oklch",
176
- bias: { l: EASINGS["ease-out"] },
177
- }).coords[0]);
178
- const avgBias = samplesBias.reduce((a, b) => a + b, 0) / samplesBias.length;
179
- expect(avgBias).toBeGreaterThan(0.5);
180
- const base = { l: 0.7, c: 0.2 };
181
- const deviation = { l: 0.05, c: 0.05 };
182
- const samplesDev = Array.from({ length: 50 }, () => Color.random({ model: "oklch", base, deviation }).coords);
183
- const avgL = samplesDev.reduce((a, b) => a + b[0], 0) / samplesDev.length;
184
- expect(avgL).toBeGreaterThan(0.6);
185
- expect(avgL).toBeLessThan(0.8);
186
- const c4 = Color.random({
187
- model: "lch",
188
- base: { h: 400 },
189
- deviation: { h: 10 },
190
- });
191
- expect(c4.coords[2]).toBeGreaterThanOrEqual(0);
192
- expect(c4.coords[2]).toBeLessThanOrEqual(360);
193
- expect(() => Color.random({
194
- model: "rgb",
195
- base: { h: 120 },
196
- })).toThrow();
197
- });
198
- it("should fit color into a supported gamut using default method", () => {
199
- const color = Color.from("color(display-p3 1.2 -0.3 0.5)");
200
- expect(color.inGamut("srgb")).toBe(false);
201
- const fitted = color.within("srgb");
202
- expect(fitted.model).toBe("display-p3");
203
- expect(fitted.inGamut("srgb")).toBe(true);
204
- });
205
- it("should return true if a color is in gamut", () => {
206
- expect(Color.from("color(display-p3 1 0 0)").inGamut("srgb")).toBe(false);
207
- expect(Color.from("color(display-p3 1 0 0)").inGamut("xyz")).toBe(true);
208
- });
209
- it("should handle none and calc(NaN) components correctly", () => {
210
- const color = Color.from("hsl(none calc(NaN) 50%)");
211
- expect(color.toString()).toBe("hsl(0 0 50)");
212
- const adjusted = color.with({ h: 150, s: 100 });
213
- expect(adjusted.toString()).toBe("hsl(150 100 50)");
214
- });
215
- it("should handle calc(infinity) components correctly", () => {
216
- const color = Color.from("hsl(calc(infinity) calc(-infinity) 50%)");
217
- expect(color.toString()).toBe("hsl(0 0 50)");
218
- const adjusted = color.with({ h: 100, s: 100 });
219
- expect(adjusted.toString()).toBe("hsl(100 100 50)");
220
- });
221
- it("should return correct component values", () => {
222
- const color = Color.from("rgb(0, 157, 255)");
223
- const rgb = color.toObject({ fit: "clip" });
224
- expect(rgb).toEqual({ r: 0, g: 157, b: 255, alpha: 1 });
225
- });
226
- it("should retrieve the correct array of components", () => {
227
- const color = Color.from("rgb(0, 157, 255)");
228
- expect(color.toArray({ fit: "clip" })).toEqual([0, 157, 255, 1]);
229
- });
230
- it("should mix two colors correctly", () => {
231
- const color1 = Color.from("red").in("hsl").mix("lime", { hue: "shorter" }).to("named-color");
232
- const color2 = Color.from("red").in("hsl").mix("lime", { hue: "longer" }).to("named-color");
233
- expect(color1).toBe("yellow");
234
- expect(color2).toBe("blue");
235
- });
236
- it("should clamp component values when getting components", () => {
237
- const rgbColor = Color.from("rgb(200, 100, 50)").with({ g: 400 });
238
- const [, g] = rgbColor.toArray({ fit: "clip" });
239
- expect(g).toBe(255);
240
- });
241
- it("should throw an error for an invalid model", () => {
242
- expect(() => Color.from("rgb(255, 255, 255)").in("invalidModel")).toThrow();
243
- });
244
- it("should update multiple components with an object", () => {
245
- const color = Color.from("hsl(0, 100%, 50%)");
246
- const updated = color.with({
247
- h: (h) => h + 50,
248
- s: (s) => s - 20,
249
- });
250
- const [h, s] = updated.toArray({ fit: "clip" });
251
- expect([h, s]).toStrictEqual([50, 80]);
252
- });
253
- it("should update multiple components with an array", () => {
254
- const color = Color.from("hsl(200 100% 50%)");
255
- const updated = color.with([undefined, 50, 80]);
256
- const coords = updated.toArray({ fit: "clip" });
257
- expect(coords).toStrictEqual([200, 50, 80, 1]);
258
- });
259
- it("should adjust opacity correctly", () => {
260
- const color = Color.from("rgb(120, 20, 170)");
261
- const adjusted = color.with({ alpha: 0.5 });
262
- expect(adjusted.toString()).toBe("rgb(120 20 170 / 0.5)");
263
- });
264
- it("should adjust saturation correctly", () => {
265
- const color = Color.from("hsl(120, 80%, 50%)");
266
- const adjusted = color.with({ s: 10 });
267
- expect(adjusted.toString({ units: true })).toBe("hsl(120deg 10% 50%)");
268
- });
269
- it("should adjust hue correctly", () => {
270
- const color = Color.from("hsl(30, 100%, 50%)");
271
- const adjusted = color.with({ h: (h) => h - 70 });
272
- expect(adjusted.toString({ units: true })).toBe("hsl(320deg 100% 50%)");
273
- });
274
- it("should adjust brightness correctly", () => {
275
- const color = Color.from("hsl(50, 100%, 30%)");
276
- const adjusted = color.with({ l: 50 });
277
- expect(adjusted.toString({ units: true })).toBe("hsl(50deg 100% 50%)");
278
- });
279
- it("should adjust contrast correctly", () => {
280
- const color = Color.from("rgb(30, 190, 250)");
281
- const amount = 2;
282
- const adjusted = color.with(({ r, g, b }) => [
283
- (r - 128) * amount + 128,
284
- (g - 128) * amount + 128,
285
- (b - 128) * amount + 128,
286
- ]);
287
- expect(adjusted.toString()).toBe("rgb(0 252 255)");
288
- });
289
- it("should apply sepia filter", () => {
290
- const color = Color.from("rgb(255, 50, 70)");
291
- const amount = 1;
292
- const adjusted = color.with(({ r, g, b }) => ({
293
- r: r + (0.393 * r + 0.769 * g + 0.189 * b - r) * amount,
294
- g: g + (0.349 * r + 0.686 * g + 0.168 * b - g) * amount,
295
- b: b + (0.272 * r + 0.534 * g + 0.131 * b - b) * amount,
296
- }));
297
- expect(adjusted.toString()).toBe("rgb(152 135 105)");
298
- });
299
- it("should return the same color-mix in different syntaxes", () => {
300
- const expected = Color.from("hsl(240, 50%, 50%)").in("lch").mix("#bf4040ff").to("rgb");
301
- const fromColorMix = Color.from("color-mix(in lch, hsl(240, 50%, 50%), #bf4040ff)").to("rgb");
302
- const fromRelative = Color.from("hsl(from color-mix(in lch, hsl(240, 50%, 50%), #bf4040ff) h s l)").to("rgb");
303
- expect(fromColorMix).toEqual(expected);
304
- expect(fromRelative).toEqual(expected);
305
- });
306
- it("should change config correctly", () => {
307
- const lightDark = "light-dark(red, blue)";
308
- const systemColor = "LinkText";
309
- expect(Color.from(lightDark).to("named-color")).toBe("red");
310
- expect(Color.from(systemColor).to("rgb")).toBe("rgb(0 0 255)");
311
- configure({ theme: "dark" });
312
- expect(Color.from(lightDark).to("named-color")).toBe("blue");
313
- expect(Color.from(systemColor).to("rgb")).toBe("rgb(0 128 255)");
314
- configure({
315
- systemColors: {
316
- LinkText: [
317
- [0, 0, 255],
318
- [50, 150, 250],
319
- ],
320
- },
321
- });
322
- expect(Color.from(systemColor).to("rgb")).toBe("rgb(50 150 250)");
323
- });
324
- it("parses color-mix() weights correctly", () => {
325
- const c1 = Color.from("color-mix(in hsl, hsl(0 100 50), hsl(120 100 50))");
326
- expect(c1.to("hsl")).toBe("hsl(60 100 50)");
327
- const c2 = Color.from("color-mix(in hsl, hsl(0 100 50) 50%, hsl(120 100 50))");
328
- expect(c2.to("hsl")).toBe("hsl(60 100 50)");
329
- const c3 = Color.from("color-mix(in hsl, hsl(0 100 50) 30%, hsl(120 100 50))");
330
- expect(c3.to("hsl")).toBe("hsl(84 100 50)");
331
- const c4 = Color.from("color-mix(in hsl, hsl(0 100 50), hsl(120 100 50) 50%)");
332
- expect(c4.to("hsl")).toBe("hsl(60 100 50)");
333
- const c5 = Color.from("color-mix(in hsl, hsl(0 100 50), hsl(120 100 50) 30%)");
334
- expect(c5.to("hsl")).toBe("hsl(36 100 50)");
335
- const c6 = Color.from("color-mix(in hsl, hsl(0 100 50) 70%, hsl(120 100 50) 30%)");
336
- expect(c6.to("hsl")).toBe("hsl(36 100 50)");
337
- const c7 = Color.from("color-mix(in hsl, hsl(0 100 50) 30%, hsl(120 100 50) 50%)");
338
- expect(c7.to("hsl")).toBe("hsl(75 100 50 / 0.8)");
339
- const c8 = Color.from("color-mix(in hsl, hsl(0 100 50) calc(30% + 20%), hsl(120 100 50))");
340
- expect(c8.to("hsl")).toBe("hsl(60 100 50)");
341
- const c9 = Color.from("color-mix(in hsl, hsl(0 100 50) 70%, hsl(120 100 50) calc(30% + 20%))");
342
- expect(c9.to("hsl")).toBe("hsl(36 100 50)");
343
- const c10 = Color.from("color-mix(in hsl, hsl(0 100 50) calc(10% + 20%), hsl(120 100 50) calc(30% + 20%))");
344
- expect(c10.to("hsl")).toBe("hsl(60 100 50)");
345
- const c11 = Color.from("color-mix(in hsl, hsl(0 100 50) 80%, hsl(120 100 50) 80%)");
346
- expect(c11.to("hsl")).toBe("hsl(60 100 50)");
347
- const c12 = Color.from("color-mix(in hsl, hsl(0 100 50) 20%, hsl(120 100 50) 80%)");
348
- expect(c12.to("hsl")).toBe("hsl(96 100 50)");
349
- const c13 = Color.from("color-mix(in hsl, hsl(0 100 50) 80%, hsl(120 100 50) 20%)");
350
- expect(c13.to("hsl")).toBe("hsl(24 100 50)");
351
- const c14 = Color.from("color-mix(in hsl, 20% hsl(0 100 50), 80% hsl(120 100 50))");
352
- expect(c14.to("hsl")).toBe("hsl(96 100 50)");
353
- const c15 = Color.from("color-mix(in hsl, 80% hsl(0 100 50), 20% hsl(120 100 50))");
354
- expect(c15.to("hsl")).toBe("hsl(24 100 50)");
355
- });
356
- it("should parse calc() expressions correctly", () => {
357
- const cases = [
358
- ["rgb(calc(50% + 10%) calc(20% * 3) calc(100% - 30%))", "rgb(153 153 178.5)"],
359
- ["hsl(calc(360deg / 2) calc(100% - 20%) calc((50% + 10%) * 2))", "hsl(180 80 100)"],
360
- ["hwb(calc(240deg - 30deg) calc(10% + 5%) calc(20% * 2))", "hwb(210 15 40)"],
361
- ["lab(calc(100% - 20%) calc(50% + 10%) calc(30% * 2))", "lab(80 75 75)"],
362
- ["lch(calc(100% - 20%) calc(50% + 10%) calc(180deg + 90deg))", "lch(80 90 270)"],
363
- ["oklab(calc(100% - 20%) calc(50% + 10%) calc(30% * -2))", "oklab(0.8 0.24 -0.24)"],
364
- ["oklch(calc(100% - 20%) calc(50% + 10%) calc(180deg + 90deg))", "oklch(0.8 0.24 270)"],
365
- [
366
- "color(srgb calc(50% + 10%) calc(20% * 3) calc(100% - 30%) / calc(1 - 0.3))",
367
- "color(srgb 0.6 0.6 0.7 / 0.7)",
368
- ],
369
- ["rgb(calc(min(80%, 90%) * 255) calc(max(10%, 20%) * 255) calc(round(50.6%) * 255))", "rgb(255 255 255)"],
370
- ["hsl(calc(sin(0.5 * pi) * 360deg) calc(sqrt(0.25) * 100%) calc(abs(-50%) * 100%))", "hsl(0 50 100)"],
371
- ["hwb(calc(180deg + cos(0) * 90deg) calc(min(30%, 40%) * 2) calc(max(10%, 5%) * 4))", "hwb(270 60 40)"],
372
- ["lab(calc(100% * exp(0)) calc(pow(2, 3) * 10) calc(floor(25.7% * 300)))", "lab(100 80 125)"],
373
- ["lch(calc(ceil(79.3%)) calc(hypot(30, 40)) calc(atan2(1, 1) * 180deg / pi))", "lch(80 50 45)"],
374
- [
375
- "color(srgb calc(pow(0.5, 2)) calc(log(100) / log(10) * 0.3) calc(round(0.756)) / calc(sign(0.8)))",
376
- "color(srgb 0.25 0.6 1)",
377
- ],
378
- ["rgb(from #ff0000 calc(r * 0.5) calc(g + 50) calc(b + 75))", "rgb(127.5 50 75)"],
379
- ["hsl(from #00ff00 calc(h * 2) calc(s - 20) calc(l / 2))", "hsl(240 80 25)"],
380
- [
381
- "color(from #0000ff srgb calc(r * 2) calc(g + 0.1) calc(b - 0.1) / calc(alpha * 0.5))",
382
- "color(srgb 0 0.1 0.9 / 0.5)",
383
- ],
384
- ["oklch(from oklch(0.8 0.2 120deg) calc(l * 0.9) calc(c * 1.5) calc(h + 60))", "oklch(0.72 0.3 180)"],
385
- ["lab(from lab(50 20 30) calc(l + 10) calc(a * min(2, 3)) calc(b * max(0.5, 1)))", "lab(60 40 30)"],
386
- ];
387
- cases.forEach(([input, expected]) => {
388
- expect(Color.from(input).toString({ precision: 4 })).toBe(expected);
389
- });
390
- });
391
- it("should gamut map out-of-gamut sRGB coords consistently", () => {
392
- const coords = [1.2, -0.3, 0.5];
393
- const model = "srgb";
394
- const epsilon = 1e-5;
395
- const clipCoords = new Color(model, coords).toArray({ fit: "clip" });
396
- const chromaCoords = new Color(model, coords).toArray({ fit: "chroma-reduction" });
397
- const cssCoords = new Color(model, coords).toArray({ fit: "css-gamut-map" });
398
- expect(clipCoords).toEqual([1, 0, 0.5, 1]);
399
- expect(chromaCoords.every((c) => c >= 0 - epsilon && c <= 1 + epsilon)).toBe(true);
400
- expect(cssCoords.every((c) => c >= 0 - epsilon && c <= 1 + epsilon)).toBe(true);
401
- expect(chromaCoords).not.toEqual(clipCoords);
402
- expect(cssCoords).not.toEqual(clipCoords);
403
- });
404
- it("should leave already in-gamut coords unchanged", () => {
405
- const fitMethods = ["clip", "chroma-reduction", "css-gamut-map"];
406
- const coords = [0.5, 0.5, 0.5];
407
- const model = "srgb";
408
- for (const fit of fitMethods) {
409
- const fitted = new Color(model, coords).toArray({ fit });
410
- expect(fitted).toEqual([...coords, 1]);
411
- }
412
- });
413
- it("should gamut map extreme coords differently per method", () => {
414
- const fitMethods = ["clip", "chroma-reduction", "css-gamut-map"];
415
- const coords = [2, 2, -1];
416
- const model = "srgb";
417
- const epsilon = 1e-5;
418
- const results = fitMethods.map((fit) => new Color(model, coords).toArray({ fit }));
419
- for (const res of results) {
420
- expect(res.every((c) => c >= 0 - epsilon && c <= 1 + epsilon)).toBe(true);
421
- }
422
- expect(new Set(results.map((r) => JSON.stringify(r))).size).toBeGreaterThan(1);
423
- });
424
- });
425
- describe("Color registration system", () => {
426
- it("should register a <named-color>", () => {
427
- registerNamedColor("Dusk Mint", [123, 167, 151]);
428
- expect(Color.from("rgb(123 167 151)").to("named-color")).toBe("duskmint");
429
- expect(Color.from("duskmint").equals("#7ba797")).toBe(true);
430
- });
431
- it("should register a color space for <color()> function", () => {
432
- /**
433
- * @see {@link https://www.w3.org/TR/css-color-hdr-1/|CSS Color HDR Module Level 1}
434
- */
435
- registerColorSpace("rec2100-hlg", {
436
- components: ["r", "g", "b"],
437
- bridge: "xyz-d65",
438
- toLinear: (c) => {
439
- const a = 0.17883277;
440
- const b = 1 - 4 * a;
441
- const c1 = 0.5 - a * Math.log(4 * a);
442
- if (c <= 0.5)
443
- return c ** 2 / 3;
444
- return (Math.exp((c - c1) / a) + b) / 12;
445
- },
446
- fromLinear: (E) => {
447
- const a = 0.17883277;
448
- const b = 1 - 4 * a;
449
- const c1 = 0.5 - a * Math.log(4 * a);
450
- const sign = E < 0 ? -1 : 1;
451
- const absE = Math.abs(E);
452
- if (absE <= 1 / 12)
453
- return sign * Math.sqrt(3 * absE);
454
- return sign * (a * Math.log(12 * absE - b) + c1);
455
- },
456
- toBridgeMatrix: MATRICES.REC2020_to_XYZD65,
457
- fromBridgeMatrix: MATRICES.XYZD65_to_REC2020,
458
- });
459
- const rec2100 = Color.from("color(rec2100-hlg none calc(-infinity) 100%)");
460
- expect(rec2100.toArray()).toEqual([0, 0, 1, 1]);
461
- expect(() => rec2100.with({ r: 0 }).to("xyz-d65")).not.toThrow();
462
- const instance = new Color("rec2100-hlg", [NaN, -Infinity, Infinity]);
463
- expect(instance.toArray()).toEqual([0, 0, 1, 1]);
464
- const relative = "color(from color(rec2100-hlg 0.7 0.3 0.1) rec2100-hlg r g b)";
465
- expect(Color.isValid(relative, "rec2100-hlg"));
466
- const outOfSrgb = Color.from("color(rec2100-hlg 0 1 0)");
467
- expect(outOfSrgb.inGamut("srgb")).toBe(false);
468
- expect(outOfSrgb.inGamut("rec2100-hlg")).toBe(true);
469
- });
470
- it("should register a <color-function>", () => {
471
- /**
472
- * @see {@link https://www.color.org/hdr/04-Timo_Kunkel.pdf|The Perceptual Quantizer}
473
- */
474
- registerColorSpace("rec2100-pq", {
475
- components: ["r", "g", "b"],
476
- bridge: "xyz-d65",
477
- toLinear: (c) => {
478
- const ninv = 2 ** 14 / 2610;
479
- const minv = 2 ** 5 / 2523;
480
- const c1 = 3424 / 2 ** 12;
481
- const c2 = 2413 / 2 ** 7;
482
- const c3 = 2392 / 2 ** 7;
483
- const x = Math.pow(Math.max(Math.pow(c, minv) - c1, 0) / (c2 - c3 * Math.pow(c, minv)), ninv);
484
- const Yw = 203;
485
- return (x * 10000) / Yw;
486
- },
487
- fromLinear: (c) => {
488
- const Yw = 203;
489
- const x = (c * Yw) / 10000;
490
- const n = 2610 / 2 ** 14;
491
- const m = 2523 / 2 ** 5;
492
- const c1 = 3424 / 2 ** 12;
493
- const c2 = 2413 / 2 ** 7;
494
- const c3 = 2392 / 2 ** 7;
495
- return Math.pow((c1 + c2 * Math.pow(x, n)) / (1 + c3 * Math.pow(x, n)), m);
496
- },
497
- toBridgeMatrix: MATRICES.REC2020_to_XYZD65,
498
- fromBridgeMatrix: MATRICES.XYZD65_to_REC2020,
499
- });
500
- const m1 = 2610 / 16384;
501
- const m2 = (2523 / 4096) * 128;
502
- const c1 = 3424 / 4096;
503
- const c2 = (2413 / 4096) * 32;
504
- const c3 = (2392 / 4096) * 32;
505
- /**
506
- * @see {@link https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2390-8-2020-PDF-E.pdf|High dynamic range television for production and international}
507
- */
508
- registerColorFunction("ictcp", {
509
- components: {
510
- i: { index: 0, value: [0, 1], precision: 5 },
511
- ct: { index: 1, value: [-1, 1], precision: 5 },
512
- cp: { index: 2, value: [-1, 1], precision: 5 },
513
- },
514
- bridge: "rec2100-pq",
515
- fromBridge: ([R, G, B]) => {
516
- const pqOETF = (x) => {
517
- const xp = Math.pow(x, m1);
518
- return Math.pow((c1 + c2 * xp) / (1 + c3 * xp), m2);
519
- };
520
- const RGB_to_LMS = [
521
- [1688 / 4096, 2146 / 4096, 262 / 4096],
522
- [683 / 4096, 2951 / 4096, 462 / 4096],
523
- [99 / 4096, 309 / 4096, 3688 / 4096],
524
- ];
525
- const LMSp_to_ICTCP = [
526
- [2048 / 4096, 2048 / 4096, 0 / 4096],
527
- [6610 / 4096, -13613 / 4096, 7003 / 4096],
528
- [17933 / 4096, -17390 / 4096, -543 / 4096],
529
- ];
530
- const LMS = multiplyMatrices(RGB_to_LMS, [R, G, B]);
531
- const LMSp = LMS.map(pqOETF);
532
- const ICTCP = multiplyMatrices(LMSp_to_ICTCP, LMSp);
533
- return ICTCP;
534
- },
535
- toBridge: ([I, Ct, Cp]) => {
536
- const pqEOTF = (y) => {
537
- const yp = Math.pow(y, 1 / m2);
538
- return Math.pow(Math.max(yp - c1, 0) / (c2 - c3 * yp), 1 / m1);
539
- };
540
- const ICTCP_to_LMSp = [
541
- [1.0, 0.00860903703704, 0.111029625],
542
- [1.0, -0.00860903703704, -0.111029625],
543
- [1.0, 0.56003133501049, -0.32062717499839],
544
- ];
545
- const LMS_to_RGB = [
546
- [3.43660668650384, -2.50645211965619, 0.06984543315235],
547
- [-0.791329556583, 1.98360045251401, -0.19227089593101],
548
- [0.02594989937188, -0.09891371469193, 1.07296381532005],
549
- ];
550
- const LMSp = multiplyMatrices(ICTCP_to_LMSp, [I, Ct, Cp]);
551
- const LMS = LMSp.map(pqEOTF);
552
- const RGB = multiplyMatrices(LMS_to_RGB, LMS);
553
- return RGB.map((v) => Math.min(1, Math.max(0, v)));
554
- },
555
- });
556
- const ictcp = Color.from("ictcp(none calc(-infinity) 100%)");
557
- expect(ictcp.toArray()).toEqual([0, -1, 1, 1]);
558
- expect(() => ictcp.with({ cp: 0 }).to("rgb")).not.toThrow();
559
- const instance = new Color("ictcp", [NaN, -Infinity, Infinity]);
560
- expect(instance.toArray()).toEqual([0, -1, 1, 1]);
561
- const relative = "ictcp(from ictcp(0.5 0.3 -0.2) i ct cp)";
562
- expect(Color.isValid(relative, "ictcp"));
563
- const outOfSrgb = Color.from("ictcp(0.8 -0.4 -0.1)");
564
- expect(outOfSrgb.inGamut("srgb")).toBe(false);
565
- expect(outOfSrgb.inGamut("rec2020")).toBe(true);
566
- });
567
- it("should register a new <color-base> syntax", () => {
568
- /**
569
- * @see {@link https://cie.co.at/datatable/cie-1931-colour-matching-functions-2-degree-observer|CIE 1931 colour-matching functions, 2 degree observer}
570
- */
571
- registerColorBase("wavelength", {
572
- isValid: (str) => str.slice(0, 11) === "wavelength(" && str[str.length - 1] === ")",
573
- bridge: "xyz-d65",
574
- toBridge: (coords) => coords,
575
- parse: (str) => {
576
- const cmf = [
577
- [360, 0.0001299, 0.000003917, 0.0006061],
578
- [361, 0.000145847, 0.000004393581, 0.0006808792],
579
- [362, 0.0001638021, 0.000004929604, 0.0007651456],
580
- [363, 0.0001840037, 0.000005532136, 0.0008600124],
581
- [364, 0.0002066902, 0.000006208245, 0.0009665928],
582
- [365, 0.0002321, 0.000006965, 0.001086],
583
- [366, 0.000260728, 0.000007813219, 0.001220586],
584
- [367, 0.000293075, 0.000008767336, 0.001372729],
585
- [368, 0.000329388, 0.000009839844, 0.001543579],
586
- [369, 0.000369914, 0.00001104323, 0.001734286],
587
- [370, 0.0004149, 0.00001239, 0.001946],
588
- [371, 0.0004641587, 0.00001388641, 0.002177777],
589
- [372, 0.000518986, 0.00001555728, 0.002435809],
590
- [373, 0.000581854, 0.00001744296, 0.002731953],
591
- [374, 0.0006552347, 0.00001958375, 0.003078064],
592
- [375, 0.0007416, 0.00002202, 0.003486],
593
- [376, 0.0008450296, 0.00002483965, 0.003975227],
594
- [377, 0.0009645268, 0.00002804126, 0.00454088],
595
- [378, 0.001094949, 0.00003153104, 0.00515832],
596
- [379, 0.001231154, 0.00003521521, 0.005802907],
597
- [380, 0.001368, 0.000039, 0.006450001],
598
- [381, 0.00150205, 0.0000428264, 0.007083216],
599
- [382, 0.001642328, 0.0000469146, 0.007745488],
600
- [383, 0.001802382, 0.0000515896, 0.008501152],
601
- [384, 0.001995757, 0.0000571764, 0.009414544],
602
- [385, 0.002236, 0.000064, 0.01054999],
603
- [386, 0.002535385, 0.00007234421, 0.0119658],
604
- [387, 0.002892603, 0.00008221224, 0.01365587],
605
- [388, 0.003300829, 0.00009350816, 0.01558805],
606
- [389, 0.003753236, 0.0001061361, 0.01773015],
607
- [390, 0.004243, 0.00012, 0.02005001],
608
- [391, 0.004762389, 0.000134984, 0.02251136],
609
- [392, 0.005330048, 0.000151492, 0.02520288],
610
- [393, 0.005978712, 0.000170208, 0.02827972],
611
- [394, 0.006741117, 0.000191816, 0.03189704],
612
- [395, 0.00765, 0.000217, 0.03621],
613
- [396, 0.008751373, 0.0002469067, 0.04143771],
614
- [397, 0.01002888, 0.00028124, 0.04750372],
615
- [398, 0.0114217, 0.00031852, 0.05411988],
616
- [399, 0.01286901, 0.0003572667, 0.06099803],
617
- [400, 0.01431, 0.000396, 0.06785001],
618
- [401, 0.01570443, 0.0004337147, 0.07448632],
619
- [402, 0.01714744, 0.000473024, 0.08136156],
620
- [403, 0.01878122, 0.000517876, 0.08915364],
621
- [404, 0.02074801, 0.0005722187, 0.09854048],
622
- [405, 0.02319, 0.00064, 0.1102],
623
- [406, 0.02620736, 0.00072456, 0.1246133],
624
- [407, 0.02978248, 0.0008255, 0.1417017],
625
- [408, 0.03388092, 0.00094116, 0.1613035],
626
- [409, 0.03846824, 0.00106988, 0.1832568],
627
- [410, 0.04351, 0.00121, 0.2074],
628
- [411, 0.0489956, 0.001362091, 0.2336921],
629
- [412, 0.0550226, 0.001530752, 0.2626114],
630
- [413, 0.0617188, 0.001720368, 0.2947746],
631
- [414, 0.069212, 0.001935323, 0.3307985],
632
- [415, 0.07763, 0.00218, 0.3713],
633
- [416, 0.08695811, 0.0024548, 0.4162091],
634
- [417, 0.09717672, 0.002764, 0.4654642],
635
- [418, 0.1084063, 0.0031178, 0.5196948],
636
- [419, 0.1207672, 0.0035264, 0.5795303],
637
- [420, 0.13438, 0.004, 0.6456],
638
- [421, 0.1493582, 0.00454624, 0.7184838],
639
- [422, 0.1653957, 0.00515932, 0.7967133],
640
- [423, 0.1819831, 0.00582928, 0.8778459],
641
- [424, 0.198611, 0.00654616, 0.959439],
642
- [425, 0.21477, 0.0073, 1.0390501],
643
- [426, 0.2301868, 0.008086507, 1.1153673],
644
- [427, 0.2448797, 0.00890872, 1.1884971],
645
- [428, 0.2587773, 0.00976768, 1.2581233],
646
- [429, 0.2718079, 0.01066443, 1.3239296],
647
- [430, 0.2839, 0.0116, 1.3856],
648
- [431, 0.2949438, 0.01257317, 1.4426352],
649
- [432, 0.3048965, 0.01358272, 1.4948035],
650
- [433, 0.3137873, 0.01462968, 1.5421903],
651
- [434, 0.3216454, 0.01571509, 1.5848807],
652
- [435, 0.3285, 0.01684, 1.62296],
653
- [436, 0.3343513, 0.01800736, 1.6564048],
654
- [437, 0.3392101, 0.01921448, 1.6852959],
655
- [438, 0.3431213, 0.02045392, 1.7098745],
656
- [439, 0.3461296, 0.02171824, 1.7303821],
657
- [440, 0.34828, 0.023, 1.74706],
658
- [441, 0.3495999, 0.02429461, 1.7600446],
659
- [442, 0.3501474, 0.02561024, 1.7696233],
660
- [443, 0.350013, 0.02695857, 1.7762637],
661
- [444, 0.349287, 0.02835125, 1.7804334],
662
- [445, 0.34806, 0.0298, 1.7826],
663
- [446, 0.3463733, 0.03131083, 1.7829682],
664
- [447, 0.3442624, 0.03288368, 1.7816998],
665
- [448, 0.3418088, 0.03452112, 1.7791982],
666
- [449, 0.3390941, 0.03622571, 1.7758671],
667
- [450, 0.3362, 0.038, 1.77211],
668
- [451, 0.3331977, 0.03984667, 1.7682589],
669
- [452, 0.3300411, 0.041768, 1.764039],
670
- [453, 0.3266357, 0.043766, 1.7589438],
671
- [454, 0.3228868, 0.04584267, 1.7524663],
672
- [455, 0.3187, 0.048, 1.7441],
673
- [456, 0.3140251, 0.05024368, 1.7335595],
674
- [457, 0.308884, 0.05257304, 1.7208581],
675
- [458, 0.3032904, 0.05498056, 1.7059369],
676
- [459, 0.2972579, 0.05745872, 1.6887372],
677
- [460, 0.2908, 0.06, 1.6692],
678
- [461, 0.2839701, 0.06260197, 1.6475287],
679
- [462, 0.2767214, 0.06527752, 1.6234127],
680
- [463, 0.2689178, 0.06804208, 1.5960223],
681
- [464, 0.2604227, 0.07091109, 1.564528],
682
- [465, 0.2511, 0.0739, 1.5281],
683
- [466, 0.2408475, 0.077016, 1.4861114],
684
- [467, 0.2298512, 0.0802664, 1.4395215],
685
- [468, 0.2184072, 0.0836668, 1.3898799],
686
- [469, 0.2068115, 0.0872328, 1.3387362],
687
- [470, 0.19536, 0.09098, 1.28764],
688
- [471, 0.1842136, 0.09491755, 1.2374223],
689
- [472, 0.1733273, 0.09904584, 1.1878243],
690
- [473, 0.1626881, 0.1033674, 1.1387611],
691
- [474, 0.1522833, 0.1078846, 1.090148],
692
- [475, 0.1421, 0.1126, 1.0419],
693
- [476, 0.1321786, 0.117532, 0.9941976],
694
- [477, 0.1225696, 0.1226744, 0.9473473],
695
- [478, 0.1132752, 0.1279928, 0.9014531],
696
- [479, 0.1042979, 0.1334528, 0.8566193],
697
- [480, 0.09564, 0.13902, 0.8129501],
698
- [481, 0.08729955, 0.1446764, 0.7705173],
699
- [482, 0.07930804, 0.1504693, 0.7294448],
700
- [483, 0.07171776, 0.1564619, 0.6899136],
701
- [484, 0.06458099, 0.1627177, 0.6521049],
702
- [485, 0.05795001, 0.1693, 0.6162],
703
- [486, 0.05186211, 0.1762431, 0.5823286],
704
- [487, 0.04628152, 0.1835581, 0.5504162],
705
- [488, 0.04115088, 0.1912735, 0.5203376],
706
- [489, 0.03641283, 0.199418, 0.4919673],
707
- [490, 0.03201, 0.20802, 0.46518],
708
- [491, 0.0279172, 0.2171199, 0.4399246],
709
- [492, 0.0241444, 0.2267345, 0.4161836],
710
- [493, 0.020687, 0.2368571, 0.3938822],
711
- [494, 0.0175404, 0.2474812, 0.3729459],
712
- [495, 0.0147, 0.2586, 0.3533],
713
- [496, 0.01216179, 0.2701849, 0.3348578],
714
- [497, 0.00991996, 0.2822939, 0.3175521],
715
- [498, 0.00796724, 0.2950505, 0.3013375],
716
- [499, 0.006296346, 0.308578, 0.2861686],
717
- [500, 0.0049, 0.323, 0.272],
718
- [501, 0.003777173, 0.3384021, 0.2588171],
719
- [502, 0.00294532, 0.3546858, 0.2464838],
720
- [503, 0.00242488, 0.3716986, 0.2347718],
721
- [504, 0.002236293, 0.3892875, 0.2234533],
722
- [505, 0.0024, 0.4073, 0.2123],
723
- [506, 0.00292552, 0.4256299, 0.2011692],
724
- [507, 0.00383656, 0.4443096, 0.1901196],
725
- [508, 0.00517484, 0.4633944, 0.1792254],
726
- [509, 0.00698208, 0.4829395, 0.1685608],
727
- [510, 0.0093, 0.503, 0.1582],
728
- [511, 0.01214949, 0.5235693, 0.1481383],
729
- [512, 0.01553588, 0.544512, 0.1383758],
730
- [513, 0.01947752, 0.56569, 0.1289942],
731
- [514, 0.02399277, 0.5869653, 0.1200751],
732
- [515, 0.0291, 0.6082, 0.1117],
733
- [516, 0.03481485, 0.6293456, 0.1039048],
734
- [517, 0.04112016, 0.6503068, 0.09666748],
735
- [518, 0.04798504, 0.6708752, 0.08998272],
736
- [519, 0.05537861, 0.6908424, 0.08384531],
737
- [520, 0.06327, 0.71, 0.07824999],
738
- [521, 0.07163501, 0.7281852, 0.07320899],
739
- [522, 0.08046224, 0.7454636, 0.06867816],
740
- [523, 0.08973996, 0.7619694, 0.06456784],
741
- [524, 0.09945645, 0.7778368, 0.06078835],
742
- [525, 0.1096, 0.7932, 0.05725001],
743
- [526, 0.1201674, 0.8081104, 0.05390435],
744
- [527, 0.1311145, 0.8224962, 0.05074664],
745
- [528, 0.1423679, 0.8363068, 0.04775276],
746
- [529, 0.1538542, 0.8494916, 0.04489859],
747
- [530, 0.1655, 0.862, 0.04216],
748
- [531, 0.1772571, 0.8738108, 0.03950728],
749
- [532, 0.18914, 0.8849624, 0.03693564],
750
- [533, 0.2011694, 0.8954936, 0.03445836],
751
- [534, 0.2133658, 0.9054432, 0.03208872],
752
- [535, 0.2257499, 0.9148501, 0.02984],
753
- [536, 0.2383209, 0.9237348, 0.02771181],
754
- [537, 0.2510668, 0.9320924, 0.02569444],
755
- [538, 0.2639922, 0.9399226, 0.02378716],
756
- [539, 0.2771017, 0.9472252, 0.02198925],
757
- [540, 0.2904, 0.954, 0.0203],
758
- [541, 0.3038912, 0.9602561, 0.01871805],
759
- [542, 0.3175726, 0.9660074, 0.01724036],
760
- [543, 0.3314384, 0.9712606, 0.01586364],
761
- [544, 0.3454828, 0.9760225, 0.01458461],
762
- [545, 0.3597, 0.9803, 0.0134],
763
- [546, 0.3740839, 0.9840924, 0.01230723],
764
- [547, 0.3886396, 0.9874182, 0.01130188],
765
- [548, 0.4033784, 0.9903128, 0.01037792],
766
- [549, 0.4183115, 0.9928116, 0.009529306],
767
- [550, 0.4334499, 0.9949501, 0.008749999],
768
- [551, 0.4487953, 0.9967108, 0.0080352],
769
- [552, 0.464336, 0.9980983, 0.0073816],
770
- [553, 0.480064, 0.999112, 0.0067854],
771
- [554, 0.4959713, 0.9997482, 0.0062428],
772
- [555, 0.5120501, 1.0, 0.005749999],
773
- [556, 0.5282959, 0.9998567, 0.0053036],
774
- [557, 0.5446916, 0.9993046, 0.0048998],
775
- [558, 0.5612094, 0.9983255, 0.0045342],
776
- [559, 0.5778215, 0.9968987, 0.0042024],
777
- [560, 0.5945, 0.995, 0.0039],
778
- [561, 0.6112209, 0.9926005, 0.0036232],
779
- [562, 0.6279758, 0.9897426, 0.0033706],
780
- [563, 0.6447602, 0.9864444, 0.0031414],
781
- [564, 0.6615697, 0.9827241, 0.0029348],
782
- [565, 0.6784, 0.9786, 0.002749999],
783
- [566, 0.6952392, 0.9740837, 0.0025852],
784
- [567, 0.7120586, 0.9691712, 0.0024386],
785
- [568, 0.7288284, 0.9638568, 0.0023094],
786
- [569, 0.7455188, 0.9581349, 0.0021968],
787
- [570, 0.7621, 0.952, 0.0021],
788
- [571, 0.7785432, 0.9454504, 0.002017733],
789
- [572, 0.7948256, 0.9384992, 0.0019482],
790
- [573, 0.8109264, 0.9311628, 0.0018898],
791
- [574, 0.8268248, 0.9234576, 0.001840933],
792
- [575, 0.8425, 0.9154, 0.0018],
793
- [576, 0.8579325, 0.9070064, 0.001766267],
794
- [577, 0.8730816, 0.8982772, 0.0017378],
795
- [578, 0.8878944, 0.8892048, 0.0017112],
796
- [579, 0.9023181, 0.8797816, 0.001683067],
797
- [580, 0.9163, 0.87, 0.001650001],
798
- [581, 0.9297995, 0.8598613, 0.001610133],
799
- [582, 0.9427984, 0.849392, 0.0015644],
800
- [583, 0.9552776, 0.838622, 0.0015136],
801
- [584, 0.9672179, 0.8275813, 0.001458533],
802
- [585, 0.9786, 0.8163, 0.0014],
803
- [586, 0.9893856, 0.8047947, 0.001336667],
804
- [587, 0.9995488, 0.793082, 0.00127],
805
- [588, 1.0090892, 0.781192, 0.001205],
806
- [589, 1.0180064, 0.7691547, 0.001146667],
807
- [590, 1.0263, 0.757, 0.0011],
808
- [591, 1.0339827, 0.7447541, 0.0010688],
809
- [592, 1.040986, 0.7324224, 0.0010494],
810
- [593, 1.047188, 0.7200036, 0.0010356],
811
- [594, 1.0524667, 0.7074965, 0.0010212],
812
- [595, 1.0567, 0.6949, 0.001],
813
- [596, 1.0597944, 0.6822192, 0.00096864],
814
- [597, 1.0617992, 0.6694716, 0.00092992],
815
- [598, 1.0628068, 0.6566744, 0.00088688],
816
- [599, 1.0629096, 0.6438448, 0.00084256],
817
- [600, 1.0622, 0.631, 0.0008],
818
- [601, 1.0607352, 0.6181555, 0.00076096],
819
- [602, 1.0584436, 0.6053144, 0.00072368],
820
- [603, 1.0552244, 0.5924756, 0.00068592],
821
- [604, 1.0509768, 0.5796379, 0.00064544],
822
- [605, 1.0456, 0.5668, 0.0006],
823
- [606, 1.0390369, 0.5539611, 0.0005478667],
824
- [607, 1.0313608, 0.5411372, 0.0004916],
825
- [608, 1.0226662, 0.5283528, 0.0004354],
826
- [609, 1.0130477, 0.5156323, 0.0003834667],
827
- [610, 1.0026, 0.503, 0.00034],
828
- [611, 0.9913675, 0.4904688, 0.0003072533],
829
- [612, 0.9793314, 0.4780304, 0.00028316],
830
- [613, 0.9664916, 0.4656776, 0.00026544],
831
- [614, 0.9528479, 0.4534032, 0.0002518133],
832
- [615, 0.9384, 0.4412, 0.00024],
833
- [616, 0.923194, 0.42908, 0.0002295467],
834
- [617, 0.907244, 0.417036, 0.00022064],
835
- [618, 0.890502, 0.405032, 0.00021196],
836
- [619, 0.87292, 0.393032, 0.0002021867],
837
- [620, 0.8544499, 0.381, 0.00019],
838
- [621, 0.835084, 0.3689184, 0.0001742133],
839
- [622, 0.814946, 0.3568272, 0.00015564],
840
- [623, 0.794186, 0.3447768, 0.00013596],
841
- [624, 0.772954, 0.3328176, 0.0001168533],
842
- [625, 0.7514, 0.321, 0.0001],
843
- [626, 0.7295836, 0.3093381, 0.00008613333],
844
- [627, 0.7075888, 0.2978504, 0.0000746],
845
- [628, 0.6856022, 0.2865936, 0.000065],
846
- [629, 0.6638104, 0.2756245, 0.00005693333],
847
- [630, 0.6424, 0.265, 0.00004999999],
848
- [631, 0.6215149, 0.2547632, 0.00004416],
849
- [632, 0.6011138, 0.2448896, 0.00003948],
850
- [633, 0.5811052, 0.2353344, 0.00003572],
851
- [634, 0.5613977, 0.2260528, 0.00003264],
852
- [635, 0.5419, 0.217, 0.00003],
853
- [636, 0.5225995, 0.2081616, 0.00002765333],
854
- [637, 0.5035464, 0.1995488, 0.00002556],
855
- [638, 0.4847436, 0.1911552, 0.00002364],
856
- [639, 0.4661939, 0.1829744, 0.00002181333],
857
- [640, 0.4479, 0.175, 0.00002],
858
- [641, 0.4298613, 0.1672235, 0.00001813333],
859
- [642, 0.412098, 0.1596464, 0.0000162],
860
- [643, 0.394644, 0.1522776, 0.0000142],
861
- [644, 0.3775333, 0.1451259, 0.00001213333],
862
- [645, 0.3608, 0.1382, 0.00001],
863
- [646, 0.3444563, 0.1315003, 0.000007733333],
864
- [647, 0.3285168, 0.1250248, 0.0000054],
865
- [648, 0.3130192, 0.1187792, 0.0000032],
866
- [649, 0.2980011, 0.1127691, 0.000001333333],
867
- [650, 0.2835, 0.107, 0.0],
868
- [651, 0.2695448, 0.1014762, 0.0],
869
- [652, 0.2561184, 0.09618864, 0.0],
870
- [653, 0.2431896, 0.09112296, 0.0],
871
- [654, 0.2307272, 0.08626485, 0.0],
872
- [655, 0.2187, 0.0816, 0.0],
873
- [656, 0.2070971, 0.07712064, 0.0],
874
- [657, 0.1959232, 0.07282552, 0.0],
875
- [658, 0.1851708, 0.06871008, 0.0],
876
- [659, 0.1748323, 0.06476976, 0.0],
877
- [660, 0.1649, 0.061, 0.0],
878
- [661, 0.1553667, 0.05739621, 0.0],
879
- [662, 0.14623, 0.05395504, 0.0],
880
- [663, 0.13749, 0.05067376, 0.0],
881
- [664, 0.1291467, 0.04754965, 0.0],
882
- [665, 0.1212, 0.04458, 0.0],
883
- [666, 0.1136397, 0.04175872, 0.0],
884
- [667, 0.106465, 0.03908496, 0.0],
885
- [668, 0.09969044, 0.03656384, 0.0],
886
- [669, 0.09333061, 0.03420048, 0.0],
887
- [670, 0.0874, 0.032, 0.0],
888
- [671, 0.08190096, 0.02996261, 0.0],
889
- [672, 0.07680428, 0.02807664, 0.0],
890
- [673, 0.07207712, 0.02632936, 0.0],
891
- [674, 0.06768664, 0.02470805, 0.0],
892
- [675, 0.0636, 0.0232, 0.0],
893
- [676, 0.05980685, 0.02180077, 0.0],
894
- [677, 0.05628216, 0.02050112, 0.0],
895
- [678, 0.05297104, 0.01928108, 0.0],
896
- [679, 0.04981861, 0.01812069, 0.0],
897
- [680, 0.04677, 0.017, 0.0],
898
- [681, 0.04378405, 0.01590379, 0.0],
899
- [682, 0.04087536, 0.01483718, 0.0],
900
- [683, 0.03807264, 0.01381068, 0.0],
901
- [684, 0.03540461, 0.01283478, 0.0],
902
- [685, 0.0329, 0.01192, 0.0],
903
- [686, 0.03056419, 0.01106831, 0.0],
904
- [687, 0.02838056, 0.01027339, 0.0],
905
- [688, 0.02634484, 0.009533311, 0.0],
906
- [689, 0.02445275, 0.008846157, 0.0],
907
- [690, 0.0227, 0.00821, 0.0],
908
- [691, 0.02108429, 0.007623781, 0.0],
909
- [692, 0.01959988, 0.007085424, 0.0],
910
- [693, 0.01823732, 0.006591476, 0.0],
911
- [694, 0.01698717, 0.006138485, 0.0],
912
- [695, 0.01584, 0.005723, 0.0],
913
- [696, 0.01479064, 0.005343059, 0.0],
914
- [697, 0.01383132, 0.004995796, 0.0],
915
- [698, 0.01294868, 0.004676404, 0.0],
916
- [699, 0.0121292, 0.004380075, 0.0],
917
- [700, 0.01135916, 0.004102, 0.0],
918
- [701, 0.01062935, 0.003838453, 0.0],
919
- [702, 0.009938846, 0.003589099, 0.0],
920
- [703, 0.009288422, 0.003354219, 0.0],
921
- [704, 0.008678854, 0.003134093, 0.0],
922
- [705, 0.008110916, 0.002929, 0.0],
923
- [706, 0.007582388, 0.002738139, 0.0],
924
- [707, 0.007088746, 0.002559876, 0.0],
925
- [708, 0.006627313, 0.002393244, 0.0],
926
- [709, 0.006195408, 0.002237275, 0.0],
927
- [710, 0.005790346, 0.002091, 0.0],
928
- [711, 0.005409826, 0.001953587, 0.0],
929
- [712, 0.005052583, 0.00182458, 0.0],
930
- [713, 0.004717512, 0.00170358, 0.0],
931
- [714, 0.004403507, 0.001590187, 0.0],
932
- [715, 0.004109457, 0.001484, 0.0],
933
- [716, 0.003833913, 0.001384496, 0.0],
934
- [717, 0.003575748, 0.001291268, 0.0],
935
- [718, 0.003334342, 0.001204092, 0.0],
936
- [719, 0.003109075, 0.001122744, 0.0],
937
- [720, 0.002899327, 0.001047, 0.0],
938
- [721, 0.002704348, 0.0009765896, 0.0],
939
- [722, 0.00252302, 0.0009111088, 0.0],
940
- [723, 0.002354168, 0.0008501332, 0.0],
941
- [724, 0.002196616, 0.0007932384, 0.0],
942
- [725, 0.00204919, 0.00074, 0.0],
943
- [726, 0.00191096, 0.0006900827, 0.0],
944
- [727, 0.001781438, 0.00064331, 0.0],
945
- [728, 0.00166011, 0.000599496, 0.0],
946
- [729, 0.001546459, 0.0005584547, 0.0],
947
- [730, 0.001439971, 0.00052, 0.0],
948
- [731, 0.001340042, 0.0004839136, 0.0],
949
- [732, 0.001246275, 0.0004500528, 0.0],
950
- [733, 0.001158471, 0.0004183452, 0.0],
951
- [734, 0.00107643, 0.0003887184, 0.0],
952
- [735, 0.0009999493, 0.0003611, 0.0],
953
- [736, 0.0009287358, 0.0003353835, 0.0],
954
- [737, 0.0008624332, 0.0003114404, 0.0],
955
- [738, 0.0008007503, 0.0002891656, 0.0],
956
- [739, 0.000743396, 0.0002684539, 0.0],
957
- [740, 0.0006900786, 0.0002492, 0.0],
958
- [741, 0.0006405156, 0.0002313019, 0.0],
959
- [742, 0.0005945021, 0.0002146856, 0.0],
960
- [743, 0.0005518646, 0.0001992884, 0.0],
961
- [744, 0.000512429, 0.0001850475, 0.0],
962
- [745, 0.0004760213, 0.0001719, 0.0],
963
- [746, 0.0004424536, 0.0001597781, 0.0],
964
- [747, 0.0004115117, 0.0001486044, 0.0],
965
- [748, 0.0003829814, 0.0001383016, 0.0],
966
- [749, 0.0003566491, 0.0001287925, 0.0],
967
- [750, 0.0003323011, 0.00012, 0.0],
968
- [751, 0.0003097586, 0.0001118595, 0.0],
969
- [752, 0.0002888871, 0.0001043224, 0.0],
970
- [753, 0.0002695394, 0.0000973356, 0.0],
971
- [754, 0.0002515682, 0.00009084587, 0.0],
972
- [755, 0.0002348261, 0.0000848, 0.0],
973
- [756, 0.000219171, 0.00007914667, 0.0],
974
- [757, 0.0002045258, 0.000073858, 0.0],
975
- [758, 0.0001908405, 0.000068916, 0.0],
976
- [759, 0.0001780654, 0.00006430267, 0.0],
977
- [760, 0.0001661505, 0.00006, 0.0],
978
- [761, 0.0001550236, 0.00005598187, 0.0],
979
- [762, 0.0001446219, 0.0000522256, 0.0],
980
- [763, 0.0001349098, 0.0000487184, 0.0],
981
- [764, 0.000125852, 0.00004544747, 0.0],
982
- [765, 0.000117413, 0.0000424, 0.0],
983
- [766, 0.0001095515, 0.00003956104, 0.0],
984
- [767, 0.0001022245, 0.00003691512, 0.0],
985
- [768, 0.00009539445, 0.00003444868, 0.0],
986
- [769, 0.0000890239, 0.00003214816, 0.0],
987
- [770, 0.00008307527, 0.00003, 0.0],
988
- [771, 0.00007751269, 0.00002799125, 0.0],
989
- [772, 0.00007231304, 0.00002611356, 0.0],
990
- [773, 0.00006745778, 0.00002436024, 0.0],
991
- [774, 0.00006292844, 0.00002272461, 0.0],
992
- [775, 0.00005870652, 0.0000212, 0.0],
993
- [776, 0.00005477028, 0.00001977855, 0.0],
994
- [777, 0.00005109918, 0.00001845285, 0.0],
995
- [778, 0.00004767654, 0.00001721687, 0.0],
996
- [779, 0.00004448567, 0.00001606459, 0.0],
997
- [780, 0.00004150994, 0.00001499, 0.0],
998
- [781, 0.00003873324, 0.00001398728, 0.0],
999
- [782, 0.00003614203, 0.00001305155, 0.0],
1000
- [783, 0.00003372352, 0.00001217818, 0.0],
1001
- [784, 0.00003146487, 0.00001136254, 0.0],
1002
- [785, 0.00002935326, 0.0000106, 0.0],
1003
- [786, 0.00002737573, 0.000009885877, 0.0],
1004
- [787, 0.00002552433, 0.000009217304, 0.0],
1005
- [788, 0.00002379376, 0.000008592362, 0.0],
1006
- [789, 0.0000221787, 0.000008009133, 0.0],
1007
- [790, 0.00002067383, 0.0000074657, 0.0],
1008
- [791, 0.00001927226, 0.000006959567, 0.0],
1009
- [792, 0.0000179664, 0.000006487995, 0.0],
1010
- [793, 0.00001674991, 0.000006048699, 0.0],
1011
- [794, 0.00001561648, 0.000005639396, 0.0],
1012
- [795, 0.00001455977, 0.0000052578, 0.0],
1013
- [796, 0.00001357387, 0.000004901771, 0.0],
1014
- [797, 0.00001265436, 0.00000456972, 0.0],
1015
- [798, 0.00001179723, 0.000004260194, 0.0],
1016
- [799, 0.00001099844, 0.000003971739, 0.0],
1017
- [800, 0.00001025398, 0.0000037029, 0.0],
1018
- [801, 0.000009559646, 0.000003452163, 0.0],
1019
- [802, 0.000008912044, 0.000003218302, 0.0],
1020
- [803, 0.000008308358, 0.0000030003, 0.0],
1021
- [804, 0.000007745769, 0.000002797139, 0.0],
1022
- [805, 0.000007221456, 0.0000026078, 0.0],
1023
- [806, 0.000006732475, 0.00000243122, 0.0],
1024
- [807, 0.000006276423, 0.000002266531, 0.0],
1025
- [808, 0.000005851304, 0.000002113013, 0.0],
1026
- [809, 0.000005455118, 0.000001969943, 0.0],
1027
- [810, 0.000005085868, 0.0000018366, 0.0],
1028
- [811, 0.000004741466, 0.00000171223, 0.0],
1029
- [812, 0.000004420236, 0.000001596228, 0.0],
1030
- [813, 0.000004120783, 0.00000148809, 0.0],
1031
- [814, 0.000003841716, 0.000001387314, 0.0],
1032
- [815, 0.000003581652, 0.0000012934, 0.0],
1033
- [816, 0.000003339127, 0.00000120582, 0.0],
1034
- [817, 0.000003112949, 0.000001124143, 0.0],
1035
- [818, 0.000002902121, 0.000001048009, 0.0],
1036
- [819, 0.000002705645, 0.0000009770578, 0.0],
1037
- [820, 0.000002522525, 0.00000091093, 0.0],
1038
- [821, 0.000002351726, 0.0000008492513, 0.0],
1039
- [822, 0.000002192415, 0.0000007917212, 0.0],
1040
- [823, 0.000002043902, 0.0000007380904, 0.0],
1041
- [824, 0.000001905497, 0.0000006881098, 0.0],
1042
- [825, 0.000001776509, 0.00000064153, 0.0],
1043
- [826, 0.000001656215, 0.0000005980895, 0.0],
1044
- [827, 0.000001544022, 0.0000005575746, 0.0],
1045
- [828, 0.00000143944, 0.000000519808, 0.0],
1046
- [829, 0.000001341977, 0.0000004846123, 0.0],
1047
- [830, 0.000001251141, 0.00000045181, 0.0],
1048
- ];
1049
- const inner = str.slice(11, -1);
1050
- const wavelength = parseFloat(inner);
1051
- let index = 0;
1052
- while (cmf[index + 1][0] < wavelength) {
1053
- index++;
1054
- }
1055
- const [lambda1, x1, y1, z1] = cmf[index];
1056
- const [lambda2, x2, y2, z2] = cmf[index + 1];
1057
- const fraction = (wavelength - lambda1) / (lambda2 - lambda1);
1058
- const x = x1 + fraction * (x2 - x1);
1059
- const y = y1 + fraction * (y2 - y1);
1060
- const z = z1 + fraction * (z2 - z1);
1061
- return [x, y, z];
1062
- },
1063
- });
1064
- const wavelength = Color.from("wavelength(360)");
1065
- expect(wavelength.toArray({ precision: null })).toEqual([0.0001299, 0.000003917, 0.0006061, 1]);
1066
- expect(() => wavelength.with({ x: 0 }).to("rgb")).not.toThrow();
1067
- const relative = "color(from wavelength(360) xyz-d65 x y z)";
1068
- expect(Color.isValid(relative, "xyz-d65"));
1069
- });
1070
- it("should register a new <color> syntax", () => {
1071
- registerColorType("color-at", {
1072
- isValid: (str) => str.slice(0, 9) === "color-at(" && str[str.length - 1] === ")",
1073
- bridge: "rgb",
1074
- toBridge: (coords) => coords,
1075
- parse: (str) => {
1076
- const timeToMinutes = (t) => {
1077
- const [h, m] = t.split(":").map(Number);
1078
- return h * 60 + m;
1079
- };
1080
- const extractTimeAndColor = (part) => {
1081
- const s = part.trim();
1082
- if (!s.startsWith("'")) {
1083
- throw new Error("Time must start with a single quote.");
1084
- }
1085
- let i = 1;
1086
- let timeStr = "";
1087
- while (i < s.length && s[i] !== "'") {
1088
- timeStr += s[i];
1089
- i++;
1090
- }
1091
- if (i >= s.length || s[i] !== "'") {
1092
- throw new Error("Unclosed time quote.");
1093
- }
1094
- i++;
1095
- while (i < s.length && /\s/.test(s[i])) {
1096
- i++;
1097
- }
1098
- const remaining = s.slice(i).trim();
1099
- let colorExpression = "";
1100
- if (remaining.startsWith("(") || /^[a-z]/i.test(remaining)) {
1101
- const { expression: expr, end: e } = extractBalancedExpression(remaining, 0);
1102
- if (expr) {
1103
- colorExpression = expr;
1104
- const rest = remaining.slice(e).trim();
1105
- if (rest.length > 0) {
1106
- throw new Error(`Unexpected extra tokens after color: '${rest}'.`);
1107
- }
1108
- }
1109
- else {
1110
- const m = remaining.match(/^([^\s]+)(.*)$/);
1111
- if (!m) {
1112
- throw new Error("Invalid color expression.");
1113
- }
1114
- colorExpression = m[1];
1115
- const rest = m[2].trim();
1116
- if (rest.length > 0) {
1117
- throw new Error(`Unexpected extra tokens after color: '${rest}'.`);
1118
- }
1119
- }
1120
- }
1121
- else {
1122
- const m = remaining.match(/^([^\s]+)(.*)$/);
1123
- if (!m) {
1124
- throw new Error("Invalid color expression.");
1125
- }
1126
- colorExpression = m[1];
1127
- const rest = m[2].trim();
1128
- if (rest.length > 0) {
1129
- throw new Error(`Unexpected extra tokens after color: '${rest}'.`);
1130
- }
1131
- }
1132
- return { minutes: timeToMinutes(timeStr), color: Color.from(colorExpression) };
1133
- };
1134
- const inner = str.slice(9, -1).trim();
1135
- const parts = [];
1136
- let i = 0;
1137
- let current = "";
1138
- while (i < inner.length) {
1139
- const char = inner[i];
1140
- if (char === ",") {
1141
- parts.push(current.trim());
1142
- current = "";
1143
- i++;
1144
- continue;
1145
- }
1146
- if (char === "(" || /[a-zA-Z]/.test(char)) {
1147
- const { expression: expr, end } = extractBalancedExpression(inner, i);
1148
- if (expr) {
1149
- current += expr;
1150
- i = end;
1151
- continue;
1152
- }
1153
- }
1154
- current += char;
1155
- i++;
1156
- }
1157
- parts.push(current.trim());
1158
- if (parts.length === 0) {
1159
- throw new Error("color-at must have at least one time-color pair.");
1160
- }
1161
- const pairs = parts.map(extractTimeAndColor);
1162
- pairs.sort((a, b) => a.minutes - b.minutes);
1163
- let currentMinutes;
1164
- try {
1165
- const now = new Date();
1166
- if (isNaN(now.getTime()))
1167
- throw new Error("Invalid date");
1168
- currentMinutes = now.getHours() * 60 + now.getMinutes();
1169
- }
1170
- catch {
1171
- return pairs[0].color.in("rgb").toArray();
1172
- }
1173
- for (let j = pairs.length - 1; j >= 0; j--) {
1174
- if (currentMinutes >= pairs[j].minutes) {
1175
- return pairs[j].color.in("rgb").toArray();
1176
- }
1177
- }
1178
- return pairs[0].color.in("rgb").toArray();
1179
- },
1180
- });
1181
- const timed = "color-at('06:00' skyblue, '12:00' gold, '18:00' orangered, '22:00' midnightblue)";
1182
- const value = Color.from(timed).to("named-color");
1183
- expect(["skyblue", "gold", "orangered", "midnightblue"].includes(value)).toBe(true);
1184
- const complex = `
1185
- color-at(
1186
- '06:00' rgb(135, 206, 235),
1187
- '08:00' hsl(195 53% 79%),
1188
- '10:00' hwb(203 53% 2%),
1189
- '12:00' lab(51.98 -8.36 -32.83),
1190
- '14:00' lch(58.36 64.78 270.78),
1191
- '16:00' oklab(0.79 0.05 0.16),
1192
- '18:00' oklch(0.69 0.19 32.32),
1193
- '20:00' color(srgb 0.09 0.09 0.43),
1194
- '22:00' color(display-p3 0.02 0.04 0.05)
1195
- )
1196
- `;
1197
- expect(Color.isValid(complex, "color-at"));
1198
- });
1199
- it("should register multiple custom named colors and convert correctly", () => {
1200
- const namedColors = [
1201
- { name: "Twilight Coral", value: [220, 128, 144] },
1202
- { name: "Moonstone Blue", value: [115, 166, 213] },
1203
- { name: "Sunset Amber", value: [255, 183, 77] },
1204
- { name: "Forest Whisper", value: [34, 139, 97] },
1205
- { name: "Slate Blue Gray", value: [115, 124, 161] },
1206
- ];
1207
- register("named-color", namedColors.map(({ name, value }) => ({ name, value })));
1208
- namedColors.forEach(({ value, name }) => {
1209
- const normalizedName = name.replace(/\s+/g, "").toLowerCase();
1210
- expect(Color.from(`rgb(${value.join(" ")})`).to("named-color")).toBe(normalizedName);
1211
- });
1212
- });
1213
- it("should unregister <color> types from the system", () => {
1214
- unregister("hwb", "prophoto-rgb", "lch");
1215
- expect(() => Color.from("hwb(120deg 0% 0%)")).toThrow();
1216
- expect(() => Color.from("red").in("prophoto-rgb").with({ b: 0 })).toThrow();
1217
- expect(() => new Color("lch", [90, 100, 280])).toThrow();
1218
- });
1219
- it("register a new fit method", () => {
1220
- const MATRIX_16 = [
1221
- [0.401288, 0.650173, -0.051461],
1222
- [-0.250268, 1.204414, 0.045854],
1223
- [-0.002079, 0.048952, 0.953127],
1224
- ];
1225
- const invert3x3 = (m) => {
1226
- const a = m[0][0], b = m[0][1], c = m[0][2];
1227
- const d = m[1][0], e = m[1][1], f = m[1][2];
1228
- const g = m[2][0], h = m[2][1], i = m[2][2];
1229
- const A = e * i - f * h;
1230
- const B = c * h - b * i;
1231
- const C = b * f - c * e;
1232
- const D = f * g - d * i;
1233
- const E = a * i - c * g;
1234
- const F = c * d - a * f;
1235
- const G = d * h - e * g;
1236
- const H = b * g - a * h;
1237
- const I = a * e - b * d;
1238
- const det = a * A + b * D + c * G;
1239
- if (Math.abs(det) < 1e-12)
1240
- throw new Error("Singular matrix");
1241
- const invDet = 1 / det;
1242
- return [
1243
- [A * invDet, B * invDet, C * invDet],
1244
- [D * invDet, E * invDet, F * invDet],
1245
- [G * invDet, H * invDet, I * invDet],
1246
- ];
1247
- };
1248
- const MATRIX_INVERSE_16 = invert3x3(MATRIX_16);
1249
- const sign = (x) => (x < 0 ? -1 : 1);
1250
- const luminanceLevelAdaptationFactor = (L_A) => {
1251
- const k = 1 / (5 * L_A + 1);
1252
- const k4 = Math.pow(k, 4);
1253
- return 0.2 * k4 * (5 * L_A) + 0.1 * Math.pow(1 - k4, 2) * Math.pow(5 * L_A, 1 / 3);
1254
- };
1255
- const chromaticInductionFactors = (n) => {
1256
- const N_bb = 0.725 * Math.pow(1 / n, 0.2);
1257
- return [N_bb, N_bb];
1258
- };
1259
- const baseExponentialNonLinearity = (n) => 1.48 + Math.sqrt(n);
1260
- const viewingConditionsDependentParameters = (Y_b, Y_w, L_A) => {
1261
- const n = Y_b / Y_w;
1262
- const F_L = luminanceLevelAdaptationFactor(L_A);
1263
- const [N_bb, N_cb] = chromaticInductionFactors(n);
1264
- const z = baseExponentialNonLinearity(n);
1265
- return { n, F_L, N_bb, N_cb, z };
1266
- };
1267
- const degreeOfAdaptation = (F, L_A) => F * (1 - (1 / 3.6) * Math.exp((-L_A - 42) / 92));
1268
- const postAdaptationNonLinearResponseCompressionForward = (RGB, F_L) => {
1269
- return RGB.map((comp) => {
1270
- const tmp = Math.pow((F_L * comp) / 100.0, 0.42);
1271
- return (400 * tmp) / (27.13 + tmp) + 0.1;
1272
- });
1273
- };
1274
- const postAdaptationNonLinearResponseCompressionInverse = (RGBc, F_L) => {
1275
- return RGBc.map((comp) => {
1276
- const v = comp - 0.1;
1277
- const s = sign(v);
1278
- const absV = Math.abs(v);
1279
- if (absV <= 1e-12)
1280
- return 0;
1281
- const inner = (27.13 * absV) / (400 - absV);
1282
- return s * (100 / F_L) * Math.pow(inner, 1 / 0.42);
1283
- });
1284
- };
1285
- const opponentColorDimensionsForward = (RGB) => {
1286
- const [R, G, B] = RGB;
1287
- const a = R - (12 * G) / 11 + B / 11;
1288
- const b = (R + G - 2 * B) / 9;
1289
- return [a, b];
1290
- };
1291
- const hueAngle = (a, b) => {
1292
- const rad = Math.atan2(b, a);
1293
- let deg = (rad * 180) / Math.PI;
1294
- if (deg < 0)
1295
- deg += 360;
1296
- return deg % 360;
1297
- };
1298
- const eccentricityFactor = (h) => 0.25 * (Math.cos(2 + (h * Math.PI) / 180) + 3.8);
1299
- const achromaticResponseForward = (RGB_a, N_bb) => {
1300
- const [R, G, B] = RGB_a;
1301
- return (2 * R + G + (1 / 20) * B - 0.305) * N_bb;
1302
- };
1303
- const achromaticResponseInverse = (A_w, J, c, z) => {
1304
- return A_w * Math.pow(J / 100, 1 / (c * z));
1305
- };
1306
- const lightnessCorrelate = (A, A_w, c, z) => {
1307
- return 100 * Math.pow(A / A_w, c * z);
1308
- };
1309
- const brightnessCorrelate = (c, J, A_w, F_L) => {
1310
- return (4 / c) * Math.sqrt(J / 100) * (A_w + 4) * Math.pow(F_L, 0.25);
1311
- };
1312
- const temporaryMagnitudeQuantityForward = (N_c, N_cb, e_t, a, b, RGB_a) => {
1313
- const [Ra, Ga, Ba] = RGB_a;
1314
- const denom = Ra + Ga + (21 * Ba) / 20;
1315
- if (denom === 0)
1316
- return 0;
1317
- return ((50000 / 13) * N_c * N_cb * e_t * Math.sqrt(a * a + b * b)) / denom;
1318
- };
1319
- const temporaryMagnitudeQuantityInverse = (C, J, n) => {
1320
- const base = Math.sqrt(J / 100) * Math.pow(1.64 - Math.pow(0.29, n), 0.73);
1321
- if (base === 0)
1322
- return 0;
1323
- return Math.pow(C / base, 1 / 0.9);
1324
- };
1325
- const chromaCorrelate = (J, n, N_c, N_cb, e_t, a, b, RGB_a) => {
1326
- const t = temporaryMagnitudeQuantityForward(N_c, N_cb, e_t, a, b, RGB_a);
1327
- return Math.pow(t, 0.9) * Math.sqrt(J / 100) * Math.pow(1.64 - Math.pow(0.29, n), 0.73);
1328
- };
1329
- const colorfulnessCorrelate = (C, F_L) => C * Math.pow(F_L, 0.25);
1330
- const saturationCorrelate = (M, Q) => 100 * Math.sqrt(M / Q);
1331
- const P = (N_c, N_cb, e_t, t, A, N_bb) => {
1332
- const P1 = ((50000 / 13) * N_c * N_cb * e_t) / t;
1333
- const P2 = A / N_bb + 0.305;
1334
- const P3 = 21 / 20;
1335
- return [P1, P2, P3];
1336
- };
1337
- const postAdaptationNonLinearResponseCompressionMatrix = (P_2, a, b) => {
1338
- const R_a = (460 * P_2 + 451 * a + 288 * b) / 1403;
1339
- const G_a = (460 * P_2 - 891 * a - 261 * b) / 1403;
1340
- const B_a = (460 * P_2 - 220 * a - 6300 * b) / 1403;
1341
- return [R_a, G_a, B_a];
1342
- };
1343
- const opponentColorDimensionsInverse = (Pn, hDeg) => {
1344
- const [P_1, , P_3] = Pn;
1345
- const hr = (hDeg * Math.PI) / 180;
1346
- const sin_hr = Math.sin(hr);
1347
- const cos_hr = Math.cos(hr);
1348
- const P_4 = P_1 / sin_hr;
1349
- const P_5 = P_1 / cos_hr;
1350
- const n = Pn[1] * (2 + P_3) * (460 / 1403);
1351
- if (Math.abs(sin_hr) >= Math.abs(cos_hr)) {
1352
- const b = n / (P_4 + (2 + P_3) * (220 / 1403) * (cos_hr / sin_hr) - 27 / 1403 + P_3 * (6300 / 1403));
1353
- const a = b * (cos_hr / sin_hr);
1354
- return [a, b];
1355
- }
1356
- else {
1357
- const a = n / (P_5 + (2 + P_3) * (220 / 1403) - (27 / 1403 - P_3 * (6300 / 1403)) * (sin_hr / cos_hr));
1358
- const b = a * (sin_hr / cos_hr);
1359
- return [a, b];
1360
- }
1361
- };
1362
- const XYZ_to_CAM16 = (XYZ, XYZ_w = [95.05, 100.0, 108.88], L_A = 318.31, Y_b = 20.0, surround = { F: 1.0, c: 0.69, N_c: 1.0 }, discountIlluminant = false, computeH = false) => {
1363
- const RGB_w = multiplyMatrices(MATRIX_16, XYZ_w);
1364
- const D = discountIlluminant ? 1.0 : Math.max(0, Math.min(1, degreeOfAdaptation(surround.F, L_A)));
1365
- const { n, F_L, N_bb, N_cb, z } = viewingConditionsDependentParameters(Y_b, XYZ_w[1], L_A);
1366
- const D_RGB = [
1367
- (D * XYZ_w[1]) / (RGB_w[0] || 1e-12) + 1 - D,
1368
- (D * XYZ_w[1]) / (RGB_w[1] || 1e-12) + 1 - D,
1369
- (D * XYZ_w[1]) / (RGB_w[2] || 1e-12) + 1 - D,
1370
- ];
1371
- const RGB_wc = [D_RGB[0] * RGB_w[0], D_RGB[1] * RGB_w[1], D_RGB[2] * RGB_w[2]];
1372
- const RGB_aw = postAdaptationNonLinearResponseCompressionForward(RGB_wc, F_L);
1373
- const A_w = achromaticResponseForward(RGB_aw, N_bb);
1374
- const RGB = multiplyMatrices(MATRIX_16, XYZ);
1375
- const RGB_c = [D_RGB[0] * RGB[0], D_RGB[1] * RGB[1], D_RGB[2] * RGB[2]];
1376
- const RGB_a = postAdaptationNonLinearResponseCompressionForward(RGB_c, F_L);
1377
- const [a, b] = opponentColorDimensionsForward(RGB_a);
1378
- const h = hueAngle(a, b);
1379
- const e_t = eccentricityFactor(h);
1380
- const H = computeH ? NaN : NaN;
1381
- const A = achromaticResponseForward(RGB_a, N_bb);
1382
- const J = lightnessCorrelate(A, A_w, surround.c, z);
1383
- const Q = brightnessCorrelate(surround.c, J, A_w, F_L);
1384
- const C = chromaCorrelate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a);
1385
- const M = colorfulnessCorrelate(C, F_L);
1386
- const s = saturationCorrelate(M, Q);
1387
- return {
1388
- J,
1389
- C,
1390
- h,
1391
- s,
1392
- Q,
1393
- M,
1394
- H,
1395
- };
1396
- };
1397
- const CAM16_to_XYZ = (specification, XYZ_w = [95.05, 100.0, 108.88], L_A = 318.31, Y_b = 20.0, surround = { F: 1.0, c: 0.69, N_c: 1.0 }, discountIlluminant = false) => {
1398
- const J = specification.J ?? NaN;
1399
- let C = specification.C ?? NaN;
1400
- const M = specification.M ?? NaN;
1401
- const h = specification.h;
1402
- const RGB_w = multiplyMatrices(MATRIX_16, XYZ_w);
1403
- const D = discountIlluminant ? 1.0 : Math.max(0, Math.min(1, degreeOfAdaptation(surround.F, L_A)));
1404
- const { n, F_L, N_bb, N_cb, z } = viewingConditionsDependentParameters(Y_b, XYZ_w[1], L_A);
1405
- const D_RGB = [
1406
- (D * XYZ_w[1]) / (RGB_w[0] || 1e-12) + 1 - D,
1407
- (D * XYZ_w[1]) / (RGB_w[1] || 1e-12) + 1 - D,
1408
- (D * XYZ_w[1]) / (RGB_w[2] || 1e-12) + 1 - D,
1409
- ];
1410
- const RGB_wc = [D_RGB[0] * RGB_w[0], D_RGB[1] * RGB_w[1], D_RGB[2] * RGB_w[2]];
1411
- const RGB_aw = postAdaptationNonLinearResponseCompressionForward(RGB_wc, F_L);
1412
- const A_w = achromaticResponseForward(RGB_aw, N_bb);
1413
- if (Number.isNaN(C) && !Number.isNaN(M)) {
1414
- C = M / Math.pow(F_L, 0.25);
1415
- }
1416
- if (Number.isNaN(C)) {
1417
- throw new Error('Either "C" or "M" must be provided in specification.');
1418
- }
1419
- const t = temporaryMagnitudeQuantityInverse(C, J, n);
1420
- const e_t = eccentricityFactor(h);
1421
- const A = achromaticResponseInverse(A_w, J, surround.c, z);
1422
- const Pn = P(surround.N_c, N_cb, e_t, t, A, N_bb);
1423
- const [, P_2] = Pn;
1424
- let [a, b] = opponentColorDimensionsInverse(Pn, h);
1425
- if (t === 0) {
1426
- a = 0;
1427
- b = 0;
1428
- }
1429
- const RGB_a = postAdaptationNonLinearResponseCompressionMatrix(P_2, a, b);
1430
- const RGB_c = postAdaptationNonLinearResponseCompressionInverse(RGB_a, F_L);
1431
- const RGB = [RGB_c[0] / D_RGB[0], RGB_c[1] / D_RGB[1], RGB_c[2] / D_RGB[2]];
1432
- const XYZ = multiplyMatrices(MATRIX_INVERSE_16, RGB);
1433
- return XYZ;
1434
- };
1435
- /**
1436
- * @see {@link https://colour.readthedocs.io/en/develop/_modules/colour/appearance/cam16.html|Colour Science CAM16-UCS documentation}
1437
- */
1438
- registerFitMethod("cam16-ucs", (coords, model) => {
1439
- const { targetGamut } = colorModels[model];
1440
- if (targetGamut === null)
1441
- return coords;
1442
- const color = new Color(model, coords);
1443
- if (color.inGamut(targetGamut, 1e-5))
1444
- return coords;
1445
- const XYZ = color.in("xyz-d65").toArray();
1446
- const cam = XYZ_to_CAM16(XYZ);
1447
- const c1 = 0.007, c2 = 0.0228;
1448
- const Jp = ((1 + 100 * c1) * cam.J) / (1 + c1 * cam.J);
1449
- const Mp = Math.log(1 + c2 * cam.M) / c2;
1450
- const ap = Mp * Math.cos((cam.h * Math.PI) / 180);
1451
- const bp = Mp * Math.sin((cam.h * Math.PI) / 180);
1452
- const epsilon = 1e-5;
1453
- let scale = 1.0;
1454
- let clippedCoords = [];
1455
- while (scale > 0) {
1456
- const ap_s = ap * scale;
1457
- const bp_s = bp * scale;
1458
- const Mp_s = Math.sqrt(ap_s * ap_s + bp_s * bp_s);
1459
- const h_s = (Math.atan2(bp_s, ap_s) * 180) / Math.PI;
1460
- const M_s = (Math.exp(c2 * Mp_s) - 1) / c2;
1461
- const J_s = Jp / (1 - c1 * Jp);
1462
- const XYZ_s = CAM16_to_XYZ({ J: J_s, M: M_s, h: h_s });
1463
- const candidateCoords = new Color("xyz-d65", XYZ_s).in(model).toArray();
1464
- const candidate = new Color(model, candidateCoords);
1465
- if (candidate.inGamut(targetGamut, epsilon)) {
1466
- clippedCoords = candidate.toArray();
1467
- break;
1468
- }
1469
- scale -= 0.05;
1470
- }
1471
- if (!clippedCoords.length) {
1472
- clippedCoords = fit(color.toArray().slice(0, 3), model);
1473
- }
1474
- return clippedCoords;
1475
- });
1476
- const coords = [1.2, -0.3, 0.5];
1477
- const model = "srgb";
1478
- const epsilon = 1e-5;
1479
- const cam16Coords = new Color(model, coords).toArray({ fit: "cam16-ucs" });
1480
- const chromaCoords = new Color(model, coords).toArray({ fit: "chroma-reduction" });
1481
- const cssCoords = new Color(model, coords).toArray({ fit: "css-gamut-map" });
1482
- expect(cam16Coords.every((c) => c >= 0 - epsilon && c <= 1 + epsilon)).toBe(true);
1483
- expect(chromaCoords.every((c) => c >= 0 - epsilon && c <= 1 + epsilon)).toBe(true);
1484
- expect(cssCoords.every((c) => c >= 0 - epsilon && c <= 1 + epsilon)).toBe(true);
1485
- expect(chromaCoords).not.toEqual(cam16Coords);
1486
- expect(cssCoords).not.toEqual(cam16Coords);
1487
- const withinGamut = [0.5, 0.5, 0.5];
1488
- const fitted = new Color(model, withinGamut).toArray({ fit: "cam16-ucs" });
1489
- expect(fitted).toEqual([...withinGamut, 1]);
1490
- });
1491
- });
1492
- describe("use()", () => {
1493
- it("should register methods to the class", () => {
1494
- const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
1495
- const lightenPlugin = (ColorClass) => {
1496
- ColorClass.prototype.lighten = function (amount) {
1497
- return this.in("hsl").with({
1498
- l: (l) => clamp(l + amount, 0, 100),
1499
- });
1500
- };
1501
- };
1502
- const darkenPlugin = (ColorClass) => {
1503
- ColorClass.prototype.darken = function (amount) {
1504
- return this.in("hsl").with({
1505
- l: (l) => clamp(l - amount, 0, 100),
1506
- });
1507
- };
1508
- };
1509
- use(lightenPlugin, darkenPlugin);
1510
- const color = Color.from("hsl(50 50 50)");
1511
- expect(color.lighten(10).toString()).toBe("hsl(50 50 60)");
1512
- expect(color.darken(20).toString()).toBe("hsl(50 50 30)");
1513
- });
1514
- it("should throw if called with no arguments", () => {
1515
- expect(() => use()).toThrow();
1516
- });
1517
- it("should throw if a non-function plugin is passed", () => {
1518
- expect(() => use("notAFunction")).toThrow();
1519
- });
1520
- it("should warn and skip duplicate plugins", () => {
1521
- const plugin = jest.fn();
1522
- const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
1523
- use(plugin);
1524
- use(plugin);
1525
- expect(warnSpy).toHaveBeenCalledWith("Plugin at index 0 is already registered. Skipping.");
1526
- warnSpy.mockRestore();
1527
- });
1528
- it("should log an error if a plugin throws", () => {
1529
- const error = new Error("plugin fail");
1530
- const badPlugin = () => {
1531
- throw error;
1532
- };
1533
- const errorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
1534
- use(badPlugin);
1535
- expect(errorSpy).toHaveBeenCalledWith("Error running plugin at index 0:", error);
1536
- errorSpy.mockRestore();
1537
- });
1538
- it("should allow multiple plugins in a single call", () => {
1539
- const pluginA = jest.fn();
1540
- const pluginB = jest.fn();
1541
- use(pluginA, pluginB);
1542
- expect(pluginA).toHaveBeenCalledWith(Color);
1543
- expect(pluginB).toHaveBeenCalledWith(Color);
1544
- });
1545
- });