saturon 0.2.3 → 0.3.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 +1 -1
- package/README.md +5 -4
- package/dist/Color.d.ts +50 -47
- package/dist/Color.js +122 -229
- package/dist/converters.d.ts +35 -141
- package/dist/converters.js +8 -422
- package/dist/engine.d.ts +148 -0
- package/dist/engine.js +1900 -0
- package/dist/index.umd.js +1774 -1300
- package/dist/index.umd.min.js +143 -1
- package/dist/math.d.ts +2 -12
- package/dist/math.js +1 -11
- package/dist/tests/Color.test.d.ts +17 -0
- package/dist/tests/Color.test.js +1062 -0
- package/dist/tests/wpt.test.js +2129 -0
- package/dist/types.d.ts +62 -72
- package/dist/utils.d.ts +10 -141
- package/dist/utils.js +108 -1052
- package/package.json +10 -11
- package/dist/temp.js +0 -3
- /package/dist/{temp.d.ts → tests/wpt.test.d.ts} +0 -0
package/dist/Color.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { colorModels,
|
|
2
|
-
import { cache,
|
|
3
|
-
import {
|
|
1
|
+
import { colorModels, colorSpaces } from "./converters.js";
|
|
2
|
+
import { cache, fit, mixTwo, normalize } from "./utils.js";
|
|
3
|
+
import { EPSILON } from "./math.js";
|
|
4
4
|
import { config } from "./config.js";
|
|
5
|
+
import { formatters, isValid, parseNode, tokenize, validateTokens } from "./engine.js";
|
|
5
6
|
/**
|
|
6
7
|
* The `Color` class represents a dynamic CSS color object, allowing for the manipulation
|
|
7
8
|
* and retrieval of colors in various formats (e.g., RGB, HEX, HSL).
|
|
@@ -22,60 +23,27 @@ export class Color {
|
|
|
22
23
|
}
|
|
23
24
|
static from(color) {
|
|
24
25
|
/* eslint-enable no-unused-vars, @typescript-eslint/no-explicit-any */
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const {
|
|
29
|
-
if (!isValid(c))
|
|
30
|
-
continue;
|
|
31
|
-
const parsed = parse(c);
|
|
32
|
-
const coords = t in colorModels ? parsed : toBridge(parsed);
|
|
33
|
-
const model = (t in colorModels ? t : bridge);
|
|
26
|
+
const tokens = tokenize(color);
|
|
27
|
+
const tree = validateTokens(tokens);
|
|
28
|
+
try {
|
|
29
|
+
const { model, coords } = parseNode(tree);
|
|
34
30
|
return new Color(model, coords);
|
|
35
31
|
}
|
|
36
|
-
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`Failed to parse color string '${color}'`, { cause: error });
|
|
34
|
+
}
|
|
37
35
|
}
|
|
38
36
|
/**
|
|
39
|
-
*
|
|
37
|
+
* Validates a color string.
|
|
40
38
|
*
|
|
41
|
-
* @param color - Color string to
|
|
42
|
-
* @param
|
|
39
|
+
* @param color - Color string to check.
|
|
40
|
+
* @param options - Validation options.
|
|
41
|
+
* @returns `true` if valid, otherwise `false`.
|
|
43
42
|
*/
|
|
44
|
-
static
|
|
45
|
-
const
|
|
46
|
-
for (const type in colorTypes) {
|
|
47
|
-
const t = type;
|
|
48
|
-
const { isValid, bridge, parse, toBridge } = colorTypes[t];
|
|
49
|
-
if (!isValid(c))
|
|
50
|
-
continue;
|
|
51
|
-
if (!strict)
|
|
52
|
-
return t;
|
|
53
|
-
try {
|
|
54
|
-
const parsed = parse(c);
|
|
55
|
-
const coords = t in colorModels ? parsed : toBridge(parsed);
|
|
56
|
-
const model = (type in colorModels ? type : bridge);
|
|
57
|
-
return typeof new Color(model, coords) === "object" ? t : undefined;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
static isValid(color, type) {
|
|
43
|
+
static isValid(color, options = {}) {
|
|
44
|
+
const { rule } = options;
|
|
66
45
|
try {
|
|
67
|
-
|
|
68
|
-
const t = type?.trim().toLowerCase();
|
|
69
|
-
const c = clean(color);
|
|
70
|
-
const { isValid, bridge, parse, toBridge } = colorTypes[t];
|
|
71
|
-
if (!isValid(c))
|
|
72
|
-
return false;
|
|
73
|
-
const parsed = parse(c);
|
|
74
|
-
const coords = t in colorModels ? parsed : toBridge(parsed);
|
|
75
|
-
const model = (t in colorModels ? t : bridge);
|
|
76
|
-
return !!new Color(model, coords);
|
|
77
|
-
}
|
|
78
|
-
return !!Color.from(color);
|
|
46
|
+
return isValid(color, rule) && !!Color.from(color);
|
|
79
47
|
}
|
|
80
48
|
catch {
|
|
81
49
|
return false;
|
|
@@ -93,7 +61,7 @@ export class Color {
|
|
|
93
61
|
const models = Object.keys(colorModels);
|
|
94
62
|
const model = options.model ?? models[Math.floor(Math.random() * models.length)];
|
|
95
63
|
const { components } = colorModels[model];
|
|
96
|
-
const valid = new Set(
|
|
64
|
+
const valid = new Set(Object.keys(components));
|
|
97
65
|
for (const section of ["limits", "bias", "base", "deviation"]) {
|
|
98
66
|
const record = options[section];
|
|
99
67
|
if (!record)
|
|
@@ -142,10 +110,67 @@ export class Color {
|
|
|
142
110
|
}
|
|
143
111
|
return new Color(model, coords);
|
|
144
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Statically mixes multiple colors together by specified percentages.
|
|
115
|
+
* Fully compliant with CSS Color Module Level 5 specification constraints.
|
|
116
|
+
*
|
|
117
|
+
* @param colors - Array of MixItems containing colors and optional percentages.
|
|
118
|
+
* @param options - Options containing the interpolation space and hue policy.
|
|
119
|
+
* @returns A new `Color` instance representing the mixed color.
|
|
120
|
+
*/
|
|
121
|
+
static mix(colors, options) {
|
|
122
|
+
const { in: model = "oklab", hue = "shorter" } = options;
|
|
123
|
+
if (!colors || colors.length === 0) {
|
|
124
|
+
throw new Error("At least one color must be provided in the colors array.");
|
|
125
|
+
}
|
|
126
|
+
if (colors.length === 1)
|
|
127
|
+
return colors[0].color.in(model);
|
|
128
|
+
const weights = colors.map((c) => (c.percentage !== undefined ? c.percentage / 100 : undefined));
|
|
129
|
+
const definedSum = weights.reduce((sum, w) => sum + (w || 0), 0);
|
|
130
|
+
const missingCount = weights.filter((w) => w === undefined).length;
|
|
131
|
+
let alphaMult = 1;
|
|
132
|
+
if (weights.length > 0 && weights.every((w) => w === 0)) {
|
|
133
|
+
alphaMult = 0;
|
|
134
|
+
for (let i = 0; i < weights.length; i++)
|
|
135
|
+
weights[i] = 1 / weights.length;
|
|
136
|
+
}
|
|
137
|
+
else if (missingCount === 0 && definedSum < 1) {
|
|
138
|
+
alphaMult = definedSum;
|
|
139
|
+
}
|
|
140
|
+
if (alphaMult > 0) {
|
|
141
|
+
if (missingCount > 0) {
|
|
142
|
+
const remaining = Math.max(0, 1 - definedSum);
|
|
143
|
+
const share = remaining / missingCount;
|
|
144
|
+
for (let i = 0; i < weights.length; i++) {
|
|
145
|
+
if (weights[i] === undefined)
|
|
146
|
+
weights[i] = share;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (definedSum > 0 && definedSum !== 1) {
|
|
150
|
+
for (let i = 0; i < weights.length; i++) {
|
|
151
|
+
weights[i] = weights[i] / definedSum;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let accColor = colors[0].color.in(model);
|
|
156
|
+
let accWeight = weights[0];
|
|
157
|
+
for (let i = 1; i < colors.length; i++) {
|
|
158
|
+
const currentWeight = weights[i];
|
|
159
|
+
accWeight += currentWeight;
|
|
160
|
+
const t = accWeight === 0 ? 0 : currentWeight / accWeight;
|
|
161
|
+
accColor = mixTwo(accColor, colors[i].color.in(model), t, model, hue);
|
|
162
|
+
}
|
|
163
|
+
if (alphaMult !== 1) {
|
|
164
|
+
const finalCoords = [...accColor.coords];
|
|
165
|
+
finalCoords[3] = (finalCoords[3] ?? 1) * alphaMult;
|
|
166
|
+
return new Color(model, finalCoords);
|
|
167
|
+
}
|
|
168
|
+
return accColor;
|
|
169
|
+
}
|
|
145
170
|
to(type, options = {}) {
|
|
146
171
|
const t = type.toLowerCase();
|
|
147
172
|
const { legacy = false, fit = config.defaults.fit, precision, units = false } = options;
|
|
148
|
-
const conv =
|
|
173
|
+
const conv = formatters[t];
|
|
149
174
|
if (!conv)
|
|
150
175
|
throw new Error(`Unsupported color type: '${t}'.`);
|
|
151
176
|
const { fromBridge, bridge, format } = conv;
|
|
@@ -223,7 +248,7 @@ export class Color {
|
|
|
223
248
|
* @returns The formatted color string.
|
|
224
249
|
*/
|
|
225
250
|
toString(options = {}) {
|
|
226
|
-
const { format } =
|
|
251
|
+
const { format } = formatters[this.model];
|
|
227
252
|
const { legacy = false, fit = config.defaults.fit, precision, units = false } = options;
|
|
228
253
|
return format?.(this.coords, { legacy, fit, precision, units });
|
|
229
254
|
}
|
|
@@ -231,20 +256,17 @@ export class Color {
|
|
|
231
256
|
* Returns the color as an object of component values.
|
|
232
257
|
*
|
|
233
258
|
* @param options - Optional retrieval options.
|
|
234
|
-
* @returns An object mapping each component
|
|
259
|
+
* @returns An object mapping each component to its numeric value.
|
|
235
260
|
* @throws If the model has no defined components.
|
|
236
261
|
*/
|
|
262
|
+
// eslint-disable-next-line no-unused-vars
|
|
237
263
|
toObject(options = {}) {
|
|
238
264
|
const coords = this.toArray(options);
|
|
239
265
|
const { components } = colorModels[this.model];
|
|
240
266
|
if (!components)
|
|
241
267
|
throw new Error(`Model ${this.model} does not have defined components.`);
|
|
242
|
-
const fullComponents = {
|
|
243
|
-
...components,
|
|
244
|
-
alpha: { index: 3, value: [0, 1], precision: 3 },
|
|
245
|
-
};
|
|
246
268
|
const result = {}; // eslint-disable-line no-unused-vars
|
|
247
|
-
for (const [name, { index }] of Object.entries(
|
|
269
|
+
for (const [name, { index }] of Object.entries(components))
|
|
248
270
|
result[name] = coords[index];
|
|
249
271
|
return result;
|
|
250
272
|
}
|
|
@@ -252,7 +274,7 @@ export class Color {
|
|
|
252
274
|
* Returns the color as an array of component values, optionally normalized and fitted.
|
|
253
275
|
*
|
|
254
276
|
* @param options - Conversion configuration.
|
|
255
|
-
* @returns An array of normalized color components
|
|
277
|
+
* @returns An array of normalized color components.
|
|
256
278
|
* @throws If the model has no defined components.
|
|
257
279
|
*/
|
|
258
280
|
toArray(options = {}) {
|
|
@@ -261,22 +283,7 @@ export class Color {
|
|
|
261
283
|
const { components } = colorModels[model];
|
|
262
284
|
if (!components)
|
|
263
285
|
throw new Error(`Model ${model} does not have defined components.`);
|
|
264
|
-
const
|
|
265
|
-
...components,
|
|
266
|
-
alpha: { index: 3, value: [0, 1], precision: 3 },
|
|
267
|
-
};
|
|
268
|
-
const normalize = (c, i) => {
|
|
269
|
-
const v = Object.values(defs)[i]?.value;
|
|
270
|
-
const [min, max] = Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
|
|
271
|
-
if (Number.isNaN(c))
|
|
272
|
-
return 0;
|
|
273
|
-
if (c === Infinity)
|
|
274
|
-
return max;
|
|
275
|
-
if (c === -Infinity)
|
|
276
|
-
return min;
|
|
277
|
-
return typeof c === "number" ? c : 0;
|
|
278
|
-
};
|
|
279
|
-
const norm = coords.slice(0, 3).map(normalize);
|
|
286
|
+
const norm = coords.slice(0, 3).map((c, i) => normalize(c, Object.values(components)[i].value));
|
|
280
287
|
const fitted = fit(norm, model, {
|
|
281
288
|
method: method,
|
|
282
289
|
precision,
|
|
@@ -328,36 +335,21 @@ export class Color {
|
|
|
328
335
|
* - A partial object mapping component names to numbers or update functions
|
|
329
336
|
* - A function that receives current components and returns partial updates or an array of values
|
|
330
337
|
* - An array of new values corresponding to component indices
|
|
331
|
-
* @param
|
|
338
|
+
* @param normalized - Whether to normalize component values to their valid ranges. Defaults to `true`.
|
|
332
339
|
* When `false`, values are not clamped or validated against their ranges.
|
|
333
340
|
* @returns A new Color instance with the updated component values
|
|
334
341
|
*/
|
|
335
342
|
/* eslint-disable no-unused-vars */
|
|
336
|
-
with(values
|
|
337
|
-
const normRange = (defValue) => Array.isArray(defValue) ? defValue : defValue === "hue" ? [0, 360] : [0, 100];
|
|
338
|
-
const normalizeComponent = (c, defValue) => {
|
|
339
|
-
if (normalize === false)
|
|
340
|
-
return c;
|
|
341
|
-
const [min, max] = normRange(defValue);
|
|
342
|
-
if (Number.isNaN(c))
|
|
343
|
-
return 0;
|
|
344
|
-
if (c === Infinity)
|
|
345
|
-
return max;
|
|
346
|
-
if (c === -Infinity)
|
|
347
|
-
return min;
|
|
348
|
-
return c;
|
|
349
|
-
};
|
|
343
|
+
with(values) {
|
|
350
344
|
const { model } = this;
|
|
351
345
|
const coords = this.coords.slice();
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
};
|
|
356
|
-
const names = Object.keys(defs);
|
|
346
|
+
const { components } = colorModels[model];
|
|
347
|
+
const names = Object.keys(components);
|
|
348
|
+
const componentDefs = Object.values(components);
|
|
357
349
|
const newValues = typeof values === "function"
|
|
358
350
|
? values(Object.fromEntries(names.map((k) => {
|
|
359
|
-
const def =
|
|
360
|
-
return [k,
|
|
351
|
+
const def = components[k];
|
|
352
|
+
return [k, normalize(coords[def.index], def.value)];
|
|
361
353
|
})))
|
|
362
354
|
: values;
|
|
363
355
|
if (Array.isArray(newValues)) {
|
|
@@ -365,8 +357,8 @@ export class Color {
|
|
|
365
357
|
const incoming = newValues[i];
|
|
366
358
|
if (typeof incoming !== "number")
|
|
367
359
|
return curr;
|
|
368
|
-
const def =
|
|
369
|
-
return
|
|
360
|
+
const def = componentDefs.find((d) => d.index === i);
|
|
361
|
+
return normalize(incoming, def.value);
|
|
370
362
|
});
|
|
371
363
|
return new Color(model, [...adjusted.slice(0, 3), coords[3] ?? 1]);
|
|
372
364
|
}
|
|
@@ -374,114 +366,28 @@ export class Color {
|
|
|
374
366
|
for (const name of names) {
|
|
375
367
|
if (!(name in newValues))
|
|
376
368
|
continue;
|
|
377
|
-
const { index, value } =
|
|
369
|
+
const { index, value } = components[name];
|
|
378
370
|
const raw = newValues[name];
|
|
379
|
-
const prev =
|
|
371
|
+
const prev = normalize(coords[index], value);
|
|
380
372
|
const val = typeof raw === "function"
|
|
381
|
-
?
|
|
373
|
+
? normalize(raw(prev), value)
|
|
382
374
|
: typeof raw === "number"
|
|
383
|
-
?
|
|
375
|
+
? normalize(raw, value)
|
|
384
376
|
: coords[index];
|
|
385
377
|
next[index] = val;
|
|
386
378
|
}
|
|
387
379
|
return new Color(model, [...next.slice(0, 3), next[3] ?? coords[3]]);
|
|
388
380
|
}
|
|
389
381
|
/**
|
|
390
|
-
*
|
|
382
|
+
* Creates a new Color instance with values fitted to the color model's gamut.
|
|
391
383
|
*
|
|
392
|
-
* @param
|
|
393
|
-
* @
|
|
394
|
-
* @returns A new `Color` instance representing the mixed color.
|
|
395
|
-
* @throws If the color model does not have defined components.
|
|
384
|
+
* @param options - Configuration options for fitting
|
|
385
|
+
* @returns A new Color instance with fitted values
|
|
396
386
|
*/
|
|
397
|
-
|
|
398
|
-
const {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
const { components } = colorModels[model]; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
402
|
-
if (!components)
|
|
403
|
-
throw new Error(`Model ${model} does not have defined components.`);
|
|
404
|
-
const defs = {
|
|
405
|
-
...components,
|
|
406
|
-
alpha: { index: 3, value: [0, 1], precision: 3 },
|
|
407
|
-
};
|
|
408
|
-
const normRange = (v) => Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
|
|
409
|
-
const repairPair = (a, b, v) => {
|
|
410
|
-
const [min, max] = normRange(v);
|
|
411
|
-
const fixInf = (x) => (x === Infinity ? max : x === -Infinity ? min : x);
|
|
412
|
-
a = fixInf(a);
|
|
413
|
-
b = fixInf(b);
|
|
414
|
-
const aNaN = Number.isNaN(a);
|
|
415
|
-
const bNaN = Number.isNaN(b);
|
|
416
|
-
if (aNaN && bNaN)
|
|
417
|
-
return { a: 0, b: 0 };
|
|
418
|
-
if (aNaN)
|
|
419
|
-
return { a: b, b };
|
|
420
|
-
if (bNaN)
|
|
421
|
-
return { a, b: a };
|
|
422
|
-
return { a, b, ok: true };
|
|
423
|
-
};
|
|
424
|
-
const hueIndex = defs.h?.index ?? -1;
|
|
425
|
-
for (const key in defs) {
|
|
426
|
-
const { index, value } = defs[key];
|
|
427
|
-
if (index > 3)
|
|
428
|
-
continue;
|
|
429
|
-
const r = repairPair(A[index], B[index], value);
|
|
430
|
-
if (r.ok) {
|
|
431
|
-
const [min, max] = normRange(value);
|
|
432
|
-
const fixInf = (x) => (x === Infinity ? max : x === -Infinity ? min : x);
|
|
433
|
-
A[index] = fixInf(A[index]);
|
|
434
|
-
B[index] = fixInf(B[index]);
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
A[index] = r.a;
|
|
438
|
-
B[index] = r.b;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
const { hue = "shorter", amount = 0.5, easing = "linear", gamma = 1.0 } = options;
|
|
442
|
-
const ease = typeof easing === "function" ? easing : EASINGS[easing];
|
|
443
|
-
const t = ease(Math.min(1, Math.max(0, amount)));
|
|
444
|
-
const tt = Math.pow(t, 1 / gamma);
|
|
445
|
-
const wrap = (v) => ((v % 360) + 360) % 360;
|
|
446
|
-
const hueDelta = (a, b) => {
|
|
447
|
-
const d = wrap(b - a);
|
|
448
|
-
return d > 180 ? d - 360 : d;
|
|
449
|
-
};
|
|
450
|
-
const hueDeltaLong = (a, b) => hueDelta(a, b) >= 0 ? hueDelta(a, b) - 360 : hueDelta(a, b) + 360;
|
|
451
|
-
const interpHue = (a, b, t, method) => {
|
|
452
|
-
switch (method) {
|
|
453
|
-
case "shorter":
|
|
454
|
-
return wrap(a + t * hueDelta(a, b));
|
|
455
|
-
case "longer":
|
|
456
|
-
return wrap(a + t * hueDeltaLong(a, b));
|
|
457
|
-
case "increasing":
|
|
458
|
-
return wrap(a * (1 - t) + (b < a ? b + 360 : b) * t);
|
|
459
|
-
case "decreasing":
|
|
460
|
-
return wrap(a * (1 - t) + (b > a ? b - 360 : b) * t);
|
|
461
|
-
}
|
|
462
|
-
throw new Error(`Invalid hue interpolation: ${method}`);
|
|
463
|
-
};
|
|
464
|
-
if (tt === 0)
|
|
465
|
-
return new Color(model, [...A]);
|
|
466
|
-
if (tt === 1)
|
|
467
|
-
return new Color(model, [...B]);
|
|
468
|
-
const aA = A[3];
|
|
469
|
-
const aB = B[3];
|
|
470
|
-
const resolvedA = A.slice(0, 3);
|
|
471
|
-
const resolvedB = B.slice(0, 3);
|
|
472
|
-
if (aA < 1 || aB < 1) {
|
|
473
|
-
const premixed = resolvedA.map((a, i) => {
|
|
474
|
-
const b = resolvedB[i];
|
|
475
|
-
return i === hueIndex ? interpHue(a, b, tt, hue) : a * aA * (1 - tt) + b * aB * tt;
|
|
476
|
-
});
|
|
477
|
-
const a = aA * (1 - tt) + aB * tt;
|
|
478
|
-
const out = a > 0
|
|
479
|
-
? premixed.map((v, i) => (i === hueIndex ? v : v / a))
|
|
480
|
-
: resolvedA.map((_, i) => (i === hueIndex ? premixed[i] : 0));
|
|
481
|
-
return new Color(model, [...out, a]);
|
|
482
|
-
}
|
|
483
|
-
const mixed = resolvedA.map((a, i) => i === hueIndex ? interpHue(a, resolvedB[i], tt, hue) : a + (resolvedB[i] - a) * tt);
|
|
484
|
-
return new Color(model, [...mixed, 1]);
|
|
387
|
+
fit(options = {}) {
|
|
388
|
+
const { fit: method = config.defaults.fit, precision } = options;
|
|
389
|
+
const fitted = this.toArray({ fit: method, precision });
|
|
390
|
+
return new Color(this.model, fitted);
|
|
485
391
|
}
|
|
486
392
|
/**
|
|
487
393
|
* Fits this color within the specified gamut using a given method.
|
|
@@ -498,21 +404,10 @@ export class Color {
|
|
|
498
404
|
const fitted = this.in(g).toArray({ fit: method, precision: null });
|
|
499
405
|
return new Color(g, fitted).in(this.model);
|
|
500
406
|
}
|
|
501
|
-
/**
|
|
502
|
-
* Creates a new Color instance with values fitted to the color model's gamut.
|
|
503
|
-
*
|
|
504
|
-
* @param options - Configuration options for fitting
|
|
505
|
-
* @returns A new Color instance with fitted values
|
|
506
|
-
*/
|
|
507
|
-
fit(options = {}) {
|
|
508
|
-
const { fit: method = config.defaults.fit, precision } = options;
|
|
509
|
-
const fitted = this.toArray({ fit: method, precision });
|
|
510
|
-
return new Color(this.model, fitted);
|
|
511
|
-
}
|
|
512
407
|
/**
|
|
513
408
|
* Calculates the WCAG 2.1 contrast ratio between this color and another.
|
|
514
409
|
*
|
|
515
|
-
* @param other - The comparison
|
|
410
|
+
* @param other - The comparison Color instance.
|
|
516
411
|
* @returns Contrast ratio from 1 to 21.
|
|
517
412
|
*
|
|
518
413
|
* @remarks
|
|
@@ -520,15 +415,14 @@ export class Color {
|
|
|
520
415
|
* - For perceptual accuracy, consider using APCA instead.
|
|
521
416
|
*/
|
|
522
417
|
contrast(other) {
|
|
523
|
-
const o = typeof other === "string" ? Color.from(other) : other;
|
|
524
418
|
const [, L1] = this.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
525
|
-
const [, L2] =
|
|
419
|
+
const [, L2] = other.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
526
420
|
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
|
|
527
421
|
}
|
|
528
422
|
/**
|
|
529
423
|
* Calculates the color difference (ΔEOK) between the current color and another color using the OKLAB color space.
|
|
530
424
|
*
|
|
531
|
-
* @param other - The other
|
|
425
|
+
* @param other - The other Color instance to compare against.
|
|
532
426
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
533
427
|
*
|
|
534
428
|
* @remarks
|
|
@@ -539,7 +433,7 @@ export class Color {
|
|
|
539
433
|
deltaEOK(other) {
|
|
540
434
|
const coordsOptions = { fit: "none", precision: null };
|
|
541
435
|
const [L1, a1, b1] = this.in("oklab").toArray(coordsOptions);
|
|
542
|
-
const [L2, a2, b2] =
|
|
436
|
+
const [L2, a2, b2] = other.in("oklab").toArray(coordsOptions);
|
|
543
437
|
const ΔL = L1 - L2;
|
|
544
438
|
const ΔA = a1 - a2;
|
|
545
439
|
const ΔB = b1 - b2;
|
|
@@ -550,13 +444,13 @@ export class Color {
|
|
|
550
444
|
* Calculates the color difference (ΔE) between two colors using the CIE76 formula.
|
|
551
445
|
* This is a simple Euclidean distance in LAB color space.
|
|
552
446
|
*
|
|
553
|
-
* @param other - The other
|
|
447
|
+
* @param other - The other Color instance to compare against.
|
|
554
448
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
555
449
|
*/
|
|
556
450
|
deltaE76(other) {
|
|
557
451
|
const coordsOptions = { fit: "none", precision: null };
|
|
558
452
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
559
|
-
const [L2, a2, b2] =
|
|
453
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
560
454
|
const ΔL = L1 - L2;
|
|
561
455
|
const ΔA = a1 - a2;
|
|
562
456
|
const ΔB = b1 - b2;
|
|
@@ -566,13 +460,13 @@ export class Color {
|
|
|
566
460
|
* Calculates the color difference (ΔE) between two colors using the CIE94 formula.
|
|
567
461
|
* This method improves perceptual accuracy over CIE76 by applying weighting factors.
|
|
568
462
|
*
|
|
569
|
-
* @param other - The other
|
|
463
|
+
* @param other - The other Color instance to compare against.
|
|
570
464
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
571
465
|
*/
|
|
572
466
|
deltaE94(other) {
|
|
573
467
|
const coordsOptions = { fit: "none", precision: null };
|
|
574
468
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
575
|
-
const [L2, a2, b2] =
|
|
469
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
576
470
|
const ΔL = L1 - L2;
|
|
577
471
|
const ΔA = a1 - a2;
|
|
578
472
|
const ΔB = b1 - b2;
|
|
@@ -590,13 +484,13 @@ export class Color {
|
|
|
590
484
|
* Calculates the color difference (ΔE) between two colors using the CIEDE2000 formula.
|
|
591
485
|
* This is the most perceptually accurate method, accounting for interactions between hue, chroma, and lightness.
|
|
592
486
|
*
|
|
593
|
-
* @param other - The other
|
|
487
|
+
* @param other - The other Color instance to compare against.
|
|
594
488
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
595
489
|
*/
|
|
596
490
|
deltaE2000(other) {
|
|
597
491
|
const coordsOptions = { fit: "none", precision: null };
|
|
598
492
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
599
|
-
const [L2, a2, b2] =
|
|
493
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
600
494
|
const π = Math.PI, d2r = π / 180, r2d = 180 / π;
|
|
601
495
|
const C1 = Math.sqrt(a1 ** 2 + b1 ** 2);
|
|
602
496
|
const C2 = Math.sqrt(a2 ** 2 + b2 ** 2);
|
|
@@ -634,7 +528,7 @@ export class Color {
|
|
|
634
528
|
const Cdash = (Cdash1 + Cdash2) / 2;
|
|
635
529
|
const Cdash7 = Math.pow(Cdash, 7);
|
|
636
530
|
const hsum = h1 + h2;
|
|
637
|
-
let hdash
|
|
531
|
+
let hdash;
|
|
638
532
|
if (Cdash1 === 0 && Cdash2 === 0) {
|
|
639
533
|
hdash = hsum;
|
|
640
534
|
}
|
|
@@ -668,8 +562,8 @@ export class Color {
|
|
|
668
562
|
/**
|
|
669
563
|
* Checks numeric equality with another color within a tolerance.
|
|
670
564
|
*
|
|
671
|
-
* @param other - Color
|
|
672
|
-
* @param epsilon - Allowed floating-point difference (
|
|
565
|
+
* @param other - Color instnace to compare.
|
|
566
|
+
* @param epsilon - Allowed floating-point difference (defaults to the value of `EPSILON` in `"saturon/math"`).
|
|
673
567
|
* @returns `true` if equal within tolerance.
|
|
674
568
|
*
|
|
675
569
|
* @remarks
|
|
@@ -683,24 +577,23 @@ export class Color {
|
|
|
683
577
|
* - {@link deltaE94} (weighted improvements over LAB)
|
|
684
578
|
* - {@link deltaE2000} (most accurate, accounts for perceptual interactions)
|
|
685
579
|
*/
|
|
686
|
-
equals(other, epsilon =
|
|
687
|
-
const o = typeof other === "string" ? Color.from(other) : other;
|
|
580
|
+
equals(other, epsilon = EPSILON) {
|
|
688
581
|
const thisCoords = this.toArray({ fit: "none", precision: null });
|
|
689
|
-
const otherCoords =
|
|
690
|
-
if (
|
|
582
|
+
const otherCoords = other.toArray({ fit: "none", precision: null });
|
|
583
|
+
if (other.model === this.model)
|
|
691
584
|
return thisCoords.every((v, i) => Math.abs(v - otherCoords[i]) <= epsilon);
|
|
692
585
|
const a = this.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
693
|
-
const b =
|
|
586
|
+
const b = other.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
694
587
|
return a.every((v, i) => Math.abs(v - b[i]) <= epsilon);
|
|
695
588
|
}
|
|
696
589
|
/**
|
|
697
590
|
* Determines whether this color lies within a given gamut.
|
|
698
591
|
*
|
|
699
592
|
* @param gamut - Target color space.
|
|
700
|
-
* @param epsilon - Floating-point tolerance (
|
|
593
|
+
* @param epsilon - Floating-point tolerance (defaults to the value of `EPSILON` in `"saturon/math"`).
|
|
701
594
|
* @returns `true` if inside gamut, else `false`.
|
|
702
595
|
*/
|
|
703
|
-
inGamut(gamut, epsilon =
|
|
596
|
+
inGamut(gamut, epsilon = EPSILON) {
|
|
704
597
|
const g = gamut.trim().toLowerCase();
|
|
705
598
|
if (!(g in colorSpaces))
|
|
706
599
|
throw new Error(`Unsupported color gamut: '${g}'.`);
|