saturon 0.2.2 → 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 +58 -46
- package/dist/Color.js +144 -221
- package/dist/converters.d.ts +39 -145
- package/dist/converters.js +12 -432
- package/dist/engine.d.ts +148 -0
- package/dist/engine.js +1900 -0
- package/dist/index.umd.js +1800 -1312
- package/dist/index.umd.min.js +143 -1
- package/dist/math.d.ts +3 -12
- package/dist/math.js +12 -21
- package/dist/tests/Color.test.d.ts +17 -0
- package/dist/tests/Color.test.js +1062 -0
- package/dist/tests/wpt.test.d.ts +1 -0
- package/dist/tests/wpt.test.js +2129 -0
- package/dist/types.d.ts +66 -76
- package/dist/utils.d.ts +10 -141
- package/dist/utils.js +111 -1059
- package/package.json +10 -11
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)
|
|
@@ -115,7 +83,7 @@ export class Color {
|
|
|
115
83
|
value = base + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * dev;
|
|
116
84
|
}
|
|
117
85
|
else {
|
|
118
|
-
let [min, max] = comp.value === "
|
|
86
|
+
let [min, max] = comp.value === "hue" ? [0, 360] : comp.value === "percentage" ? [0, 100] : comp.value;
|
|
119
87
|
const limits = options.limits?.[name];
|
|
120
88
|
if (limits) {
|
|
121
89
|
const [lMin, lMax] = limits;
|
|
@@ -128,7 +96,7 @@ export class Color {
|
|
|
128
96
|
r = biasFn(r);
|
|
129
97
|
value = min + r * (max - min);
|
|
130
98
|
}
|
|
131
|
-
if (comp.value === "
|
|
99
|
+
if (comp.value === "hue")
|
|
132
100
|
value = ((value % 360) + 360) % 360;
|
|
133
101
|
else if (comp.value === "percentage")
|
|
134
102
|
value = Math.min(100, Math.max(0, value));
|
|
@@ -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;
|
|
@@ -179,7 +204,6 @@ export class Color {
|
|
|
179
204
|
cache.set("paths", paths);
|
|
180
205
|
}
|
|
181
206
|
const key = `${from}-${to}`;
|
|
182
|
-
const coords = this.coords.slice(0, 3);
|
|
183
207
|
if (!paths.has(key)) {
|
|
184
208
|
const queue = [from], parent = { [from]: null };
|
|
185
209
|
for (let i = 0; i < queue.length; i++) {
|
|
@@ -202,7 +226,7 @@ export class Color {
|
|
|
202
226
|
}
|
|
203
227
|
paths.set(key, path);
|
|
204
228
|
}
|
|
205
|
-
let value = [...coords];
|
|
229
|
+
let value = [...this.coords.map((v) => (isNaN(v) ? 0 : v))];
|
|
206
230
|
const path = paths.get(key);
|
|
207
231
|
for (let i = 0; i < path.length - 1; i++) {
|
|
208
232
|
const a = path[i], b = path[i + 1];
|
|
@@ -224,7 +248,7 @@ export class Color {
|
|
|
224
248
|
* @returns The formatted color string.
|
|
225
249
|
*/
|
|
226
250
|
toString(options = {}) {
|
|
227
|
-
const { format } =
|
|
251
|
+
const { format } = formatters[this.model];
|
|
228
252
|
const { legacy = false, fit = config.defaults.fit, precision, units = false } = options;
|
|
229
253
|
return format?.(this.coords, { legacy, fit, precision, units });
|
|
230
254
|
}
|
|
@@ -232,20 +256,17 @@ export class Color {
|
|
|
232
256
|
* Returns the color as an object of component values.
|
|
233
257
|
*
|
|
234
258
|
* @param options - Optional retrieval options.
|
|
235
|
-
* @returns An object mapping each component
|
|
259
|
+
* @returns An object mapping each component to its numeric value.
|
|
236
260
|
* @throws If the model has no defined components.
|
|
237
261
|
*/
|
|
262
|
+
// eslint-disable-next-line no-unused-vars
|
|
238
263
|
toObject(options = {}) {
|
|
239
264
|
const coords = this.toArray(options);
|
|
240
265
|
const { components } = colorModels[this.model];
|
|
241
266
|
if (!components)
|
|
242
267
|
throw new Error(`Model ${this.model} does not have defined components.`);
|
|
243
|
-
const fullComponents = {
|
|
244
|
-
...components,
|
|
245
|
-
alpha: { index: 3, value: [0, 1], precision: 3 },
|
|
246
|
-
};
|
|
247
268
|
const result = {}; // eslint-disable-line no-unused-vars
|
|
248
|
-
for (const [name, { index }] of Object.entries(
|
|
269
|
+
for (const [name, { index }] of Object.entries(components))
|
|
249
270
|
result[name] = coords[index];
|
|
250
271
|
return result;
|
|
251
272
|
}
|
|
@@ -253,7 +274,7 @@ export class Color {
|
|
|
253
274
|
* Returns the color as an array of component values, optionally normalized and fitted.
|
|
254
275
|
*
|
|
255
276
|
* @param options - Conversion configuration.
|
|
256
|
-
* @returns An array of normalized color components
|
|
277
|
+
* @returns An array of normalized color components.
|
|
257
278
|
* @throws If the model has no defined components.
|
|
258
279
|
*/
|
|
259
280
|
toArray(options = {}) {
|
|
@@ -262,30 +283,15 @@ export class Color {
|
|
|
262
283
|
const { components } = colorModels[model];
|
|
263
284
|
if (!components)
|
|
264
285
|
throw new Error(`Model ${model} does not have defined components.`);
|
|
265
|
-
const
|
|
266
|
-
...components,
|
|
267
|
-
alpha: { index: 3, value: [0, 1], precision: 3 },
|
|
268
|
-
};
|
|
269
|
-
const normalize = (c, i) => {
|
|
270
|
-
const v = Object.values(defs)[i]?.value;
|
|
271
|
-
const [min, max] = Array.isArray(v) ? v : v === "angle" ? [0, 360] : [0, 100];
|
|
272
|
-
if (Number.isNaN(c))
|
|
273
|
-
return 0;
|
|
274
|
-
if (c === Infinity)
|
|
275
|
-
return max;
|
|
276
|
-
if (c === -Infinity)
|
|
277
|
-
return min;
|
|
278
|
-
return typeof c === "number" ? c : 0;
|
|
279
|
-
};
|
|
280
|
-
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));
|
|
281
287
|
const fitted = fit(norm, model, {
|
|
282
288
|
method: method,
|
|
283
289
|
precision,
|
|
284
290
|
});
|
|
285
|
-
return [...fitted
|
|
291
|
+
return [...fitted, coords[3]];
|
|
286
292
|
}
|
|
287
293
|
/**
|
|
288
|
-
*
|
|
294
|
+
* Creates a new Color instance with modified component values.
|
|
289
295
|
*
|
|
290
296
|
* This method supports several flexible update styles:
|
|
291
297
|
*
|
|
@@ -324,147 +330,64 @@ export class Color {
|
|
|
324
330
|
* color.with(({ r, g, b }) => [r * 0.5, g * 0.5, b * 0.5, 1]);
|
|
325
331
|
* ```
|
|
326
332
|
*
|
|
327
|
-
* @
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* @
|
|
333
|
+
* @template M - The color model type
|
|
334
|
+
* @param values - The new component values to apply. Can be:
|
|
335
|
+
* - A partial object mapping component names to numbers or update functions
|
|
336
|
+
* - A function that receives current components and returns partial updates or an array of values
|
|
337
|
+
* - An array of new values corresponding to component indices
|
|
338
|
+
* @param normalized - Whether to normalize component values to their valid ranges. Defaults to `true`.
|
|
339
|
+
* When `false`, values are not clamped or validated against their ranges.
|
|
340
|
+
* @returns A new Color instance with the updated component values
|
|
333
341
|
*/
|
|
334
342
|
/* eslint-disable no-unused-vars */
|
|
335
343
|
with(values) {
|
|
336
|
-
/* eslint-enable no-unused-vars */
|
|
337
344
|
const { model } = this;
|
|
338
|
-
const coords = this.
|
|
345
|
+
const coords = this.coords.slice();
|
|
339
346
|
const { components } = colorModels[model];
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (typeof values === "function") {
|
|
349
|
-
const result = values(Object.fromEntries(names.map((c) => [c, coords[defs[c].index]])));
|
|
350
|
-
newValues = result;
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
newValues = values;
|
|
354
|
-
}
|
|
347
|
+
const names = Object.keys(components);
|
|
348
|
+
const componentDefs = Object.values(components);
|
|
349
|
+
const newValues = typeof values === "function"
|
|
350
|
+
? values(Object.fromEntries(names.map((k) => {
|
|
351
|
+
const def = components[k];
|
|
352
|
+
return [k, normalize(coords[def.index], def.value)];
|
|
353
|
+
})))
|
|
354
|
+
: values;
|
|
355
355
|
if (Array.isArray(newValues)) {
|
|
356
356
|
const adjusted = coords.map((curr, i) => {
|
|
357
357
|
const incoming = newValues[i];
|
|
358
|
-
const { value } = Object.values(defs).find((d) => d.index === i);
|
|
359
358
|
if (typeof incoming !== "number")
|
|
360
359
|
return curr;
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
return 0;
|
|
364
|
-
if (incoming === Infinity)
|
|
365
|
-
return max;
|
|
366
|
-
if (incoming === -Infinity)
|
|
367
|
-
return min;
|
|
368
|
-
return incoming;
|
|
360
|
+
const def = componentDefs.find((d) => d.index === i);
|
|
361
|
+
return normalize(incoming, def.value);
|
|
369
362
|
});
|
|
370
|
-
return new Color(model, [...adjusted.slice(0, 3), coords[3]]);
|
|
363
|
+
return new Color(model, [...adjusted.slice(0, 3), coords[3] ?? 1]);
|
|
371
364
|
}
|
|
372
365
|
const next = [...coords];
|
|
373
366
|
for (const name of names) {
|
|
374
367
|
if (!(name in newValues))
|
|
375
368
|
continue;
|
|
376
|
-
const { index, value } =
|
|
377
|
-
const current = coords[index];
|
|
369
|
+
const { index, value } = components[name];
|
|
378
370
|
const raw = newValues[name];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
val = max;
|
|
386
|
-
else if (val === -Infinity)
|
|
387
|
-
val = min;
|
|
388
|
-
}
|
|
371
|
+
const prev = normalize(coords[index], value);
|
|
372
|
+
const val = typeof raw === "function"
|
|
373
|
+
? normalize(raw(prev), value)
|
|
374
|
+
: typeof raw === "number"
|
|
375
|
+
? normalize(raw, value)
|
|
376
|
+
: coords[index];
|
|
389
377
|
next[index] = val;
|
|
390
378
|
}
|
|
391
379
|
return new Color(model, [...next.slice(0, 3), next[3] ?? coords[3]]);
|
|
392
380
|
}
|
|
393
381
|
/**
|
|
394
|
-
*
|
|
382
|
+
* Creates a new Color instance with values fitted to the color model's gamut.
|
|
395
383
|
*
|
|
396
|
-
* @param
|
|
397
|
-
* @
|
|
398
|
-
* @returns A new `Color` instance representing the mixed color.
|
|
399
|
-
* @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
|
|
400
386
|
*/
|
|
401
|
-
|
|
402
|
-
const {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
if (!components)
|
|
406
|
-
throw new Error(`Model ${model} does not have defined components.`);
|
|
407
|
-
const { hue = "shorter", amount = 0.5, easing = "linear", gamma = 1.0 } = options;
|
|
408
|
-
const defs = {
|
|
409
|
-
...components,
|
|
410
|
-
alpha: { index: 3, value: [0, 1], precision: 3 },
|
|
411
|
-
};
|
|
412
|
-
const hueDelta = (a, b) => {
|
|
413
|
-
const d = (((b - a) % 360) + 360) % 360;
|
|
414
|
-
return d > 180 ? d - 360 : d;
|
|
415
|
-
};
|
|
416
|
-
const hueDeltaLong = (a, b) => hueDelta(a, b) >= 0 ? hueDelta(a, b) - 360 : hueDelta(a, b) + 360;
|
|
417
|
-
const interpolateHue = (a, b, t, method) => {
|
|
418
|
-
const wrapped = (v) => ((v % 360) + 360) % 360;
|
|
419
|
-
switch (method) {
|
|
420
|
-
case "shorter":
|
|
421
|
-
return wrapped(a + t * hueDelta(a, b));
|
|
422
|
-
case "longer":
|
|
423
|
-
return wrapped(a + t * hueDeltaLong(a, b));
|
|
424
|
-
case "increasing":
|
|
425
|
-
return wrapped(a * (1 - t) + (b < a ? b + 360 : b) * t);
|
|
426
|
-
case "decreasing":
|
|
427
|
-
return wrapped(a * (1 - t) + (b > a ? b - 360 : b) * t);
|
|
428
|
-
default:
|
|
429
|
-
throw new Error(`Invalid hue interpolation method: ${method}`);
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
const t = Math.max(0, Math.min(1, amount));
|
|
433
|
-
const ease = typeof easing === "function" ? easing : EASINGS[easing];
|
|
434
|
-
const tt = Math.pow(ease(t), 1 / gamma);
|
|
435
|
-
const otherColor = (typeof other === "string" ? Color.from(other) : other);
|
|
436
|
-
const thisCoords = coords.slice(0, 3);
|
|
437
|
-
const otherCoords = otherColor.in(model).toArray({ fit: "none", precision: null }).slice(0, 3);
|
|
438
|
-
const thisAlpha = coords[3];
|
|
439
|
-
const otherAlpha = otherColor.coords[3];
|
|
440
|
-
const hueIndex = Object.entries(defs).find(([k]) => k === "h")?.[1].index;
|
|
441
|
-
if (t === 0)
|
|
442
|
-
return new Color(model, [...thisCoords, thisAlpha]);
|
|
443
|
-
if (t === 1)
|
|
444
|
-
return new Color(model, [...otherCoords, otherAlpha]);
|
|
445
|
-
if (thisAlpha < 1 || otherAlpha < 1) {
|
|
446
|
-
const premixed = thisCoords.map((start, i) => {
|
|
447
|
-
const end = otherCoords[i];
|
|
448
|
-
if (i === hueIndex)
|
|
449
|
-
return interpolateHue(start, end, tt, hue);
|
|
450
|
-
const a = start * thisAlpha;
|
|
451
|
-
const b = end * otherAlpha;
|
|
452
|
-
return a * (1 - tt) + b * tt;
|
|
453
|
-
});
|
|
454
|
-
const mixedAlpha = thisAlpha * (1 - tt) + otherAlpha * tt;
|
|
455
|
-
const mixed = mixedAlpha > 0
|
|
456
|
-
? premixed.map((c, i) => (i === hueIndex ? c : c / mixedAlpha))
|
|
457
|
-
: thisCoords.map((_, i) => (i === hueIndex ? premixed[i] : 0));
|
|
458
|
-
return new Color(model, [...mixed, mixedAlpha]);
|
|
459
|
-
}
|
|
460
|
-
const mixedCoords = thisCoords.map((start, i) => {
|
|
461
|
-
const entry = Object.values(defs).find((d) => d.index === i);
|
|
462
|
-
if (!entry)
|
|
463
|
-
return start;
|
|
464
|
-
const end = otherCoords[i];
|
|
465
|
-
return entry.value === "angle" ? interpolateHue(start, end, tt, hue) : start + (end - start) * tt;
|
|
466
|
-
});
|
|
467
|
-
return new Color(model, [...mixedCoords, 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);
|
|
468
391
|
}
|
|
469
392
|
/**
|
|
470
393
|
* Fits this color within the specified gamut using a given method.
|
|
@@ -484,7 +407,7 @@ export class Color {
|
|
|
484
407
|
/**
|
|
485
408
|
* Calculates the WCAG 2.1 contrast ratio between this color and another.
|
|
486
409
|
*
|
|
487
|
-
* @param other - The comparison
|
|
410
|
+
* @param other - The comparison Color instance.
|
|
488
411
|
* @returns Contrast ratio from 1 to 21.
|
|
489
412
|
*
|
|
490
413
|
* @remarks
|
|
@@ -492,15 +415,14 @@ export class Color {
|
|
|
492
415
|
* - For perceptual accuracy, consider using APCA instead.
|
|
493
416
|
*/
|
|
494
417
|
contrast(other) {
|
|
495
|
-
const o = typeof other === "string" ? Color.from(other) : other;
|
|
496
418
|
const [, L1] = this.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
497
|
-
const [, L2] =
|
|
419
|
+
const [, L2] = other.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
498
420
|
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
|
|
499
421
|
}
|
|
500
422
|
/**
|
|
501
423
|
* Calculates the color difference (ΔEOK) between the current color and another color using the OKLAB color space.
|
|
502
424
|
*
|
|
503
|
-
* @param other - The other
|
|
425
|
+
* @param other - The other Color instance to compare against.
|
|
504
426
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
505
427
|
*
|
|
506
428
|
* @remarks
|
|
@@ -511,7 +433,7 @@ export class Color {
|
|
|
511
433
|
deltaEOK(other) {
|
|
512
434
|
const coordsOptions = { fit: "none", precision: null };
|
|
513
435
|
const [L1, a1, b1] = this.in("oklab").toArray(coordsOptions);
|
|
514
|
-
const [L2, a2, b2] =
|
|
436
|
+
const [L2, a2, b2] = other.in("oklab").toArray(coordsOptions);
|
|
515
437
|
const ΔL = L1 - L2;
|
|
516
438
|
const ΔA = a1 - a2;
|
|
517
439
|
const ΔB = b1 - b2;
|
|
@@ -522,13 +444,13 @@ export class Color {
|
|
|
522
444
|
* Calculates the color difference (ΔE) between two colors using the CIE76 formula.
|
|
523
445
|
* This is a simple Euclidean distance in LAB color space.
|
|
524
446
|
*
|
|
525
|
-
* @param other - The other
|
|
447
|
+
* @param other - The other Color instance to compare against.
|
|
526
448
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
527
449
|
*/
|
|
528
450
|
deltaE76(other) {
|
|
529
451
|
const coordsOptions = { fit: "none", precision: null };
|
|
530
452
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
531
|
-
const [L2, a2, b2] =
|
|
453
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
532
454
|
const ΔL = L1 - L2;
|
|
533
455
|
const ΔA = a1 - a2;
|
|
534
456
|
const ΔB = b1 - b2;
|
|
@@ -538,13 +460,13 @@ export class Color {
|
|
|
538
460
|
* Calculates the color difference (ΔE) between two colors using the CIE94 formula.
|
|
539
461
|
* This method improves perceptual accuracy over CIE76 by applying weighting factors.
|
|
540
462
|
*
|
|
541
|
-
* @param other - The other
|
|
463
|
+
* @param other - The other Color instance to compare against.
|
|
542
464
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
543
465
|
*/
|
|
544
466
|
deltaE94(other) {
|
|
545
467
|
const coordsOptions = { fit: "none", precision: null };
|
|
546
468
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
547
|
-
const [L2, a2, b2] =
|
|
469
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
548
470
|
const ΔL = L1 - L2;
|
|
549
471
|
const ΔA = a1 - a2;
|
|
550
472
|
const ΔB = b1 - b2;
|
|
@@ -562,13 +484,13 @@ export class Color {
|
|
|
562
484
|
* Calculates the color difference (ΔE) between two colors using the CIEDE2000 formula.
|
|
563
485
|
* This is the most perceptually accurate method, accounting for interactions between hue, chroma, and lightness.
|
|
564
486
|
*
|
|
565
|
-
* @param other - The other
|
|
487
|
+
* @param other - The other Color instance to compare against.
|
|
566
488
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
567
489
|
*/
|
|
568
490
|
deltaE2000(other) {
|
|
569
491
|
const coordsOptions = { fit: "none", precision: null };
|
|
570
492
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
571
|
-
const [L2, a2, b2] =
|
|
493
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
572
494
|
const π = Math.PI, d2r = π / 180, r2d = 180 / π;
|
|
573
495
|
const C1 = Math.sqrt(a1 ** 2 + b1 ** 2);
|
|
574
496
|
const C2 = Math.sqrt(a2 ** 2 + b2 ** 2);
|
|
@@ -606,7 +528,7 @@ export class Color {
|
|
|
606
528
|
const Cdash = (Cdash1 + Cdash2) / 2;
|
|
607
529
|
const Cdash7 = Math.pow(Cdash, 7);
|
|
608
530
|
const hsum = h1 + h2;
|
|
609
|
-
let hdash
|
|
531
|
+
let hdash;
|
|
610
532
|
if (Cdash1 === 0 && Cdash2 === 0) {
|
|
611
533
|
hdash = hsum;
|
|
612
534
|
}
|
|
@@ -640,8 +562,8 @@ export class Color {
|
|
|
640
562
|
/**
|
|
641
563
|
* Checks numeric equality with another color within a tolerance.
|
|
642
564
|
*
|
|
643
|
-
* @param other - Color
|
|
644
|
-
* @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"`).
|
|
645
567
|
* @returns `true` if equal within tolerance.
|
|
646
568
|
*
|
|
647
569
|
* @remarks
|
|
@@ -655,22 +577,23 @@ export class Color {
|
|
|
655
577
|
* - {@link deltaE94} (weighted improvements over LAB)
|
|
656
578
|
* - {@link deltaE2000} (most accurate, accounts for perceptual interactions)
|
|
657
579
|
*/
|
|
658
|
-
equals(other, epsilon =
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
580
|
+
equals(other, epsilon = EPSILON) {
|
|
581
|
+
const thisCoords = this.toArray({ fit: "none", precision: null });
|
|
582
|
+
const otherCoords = other.toArray({ fit: "none", precision: null });
|
|
583
|
+
if (other.model === this.model)
|
|
584
|
+
return thisCoords.every((v, i) => Math.abs(v - otherCoords[i]) <= epsilon);
|
|
662
585
|
const a = this.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
663
|
-
const b =
|
|
586
|
+
const b = other.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
664
587
|
return a.every((v, i) => Math.abs(v - b[i]) <= epsilon);
|
|
665
588
|
}
|
|
666
589
|
/**
|
|
667
590
|
* Determines whether this color lies within a given gamut.
|
|
668
591
|
*
|
|
669
592
|
* @param gamut - Target color space.
|
|
670
|
-
* @param epsilon - Floating-point tolerance (
|
|
593
|
+
* @param epsilon - Floating-point tolerance (defaults to the value of `EPSILON` in `"saturon/math"`).
|
|
671
594
|
* @returns `true` if inside gamut, else `false`.
|
|
672
595
|
*/
|
|
673
|
-
inGamut(gamut, epsilon =
|
|
596
|
+
inGamut(gamut, epsilon = EPSILON) {
|
|
674
597
|
const g = gamut.trim().toLowerCase();
|
|
675
598
|
if (!(g in colorSpaces))
|
|
676
599
|
throw new Error(`Unsupported color gamut: '${g}'.`);
|
|
@@ -680,7 +603,7 @@ export class Color {
|
|
|
680
603
|
const coords = this.in(g).toArray({ fit: "none", precision: null });
|
|
681
604
|
return Object.values(components).every(({ index, value }) => {
|
|
682
605
|
const v = coords[index];
|
|
683
|
-
const [min, max] = Array.isArray(value) ? value : value === "
|
|
606
|
+
const [min, max] = Array.isArray(value) ? value : value === "hue" ? [0, 360] : [0, 100];
|
|
684
607
|
return v >= min - epsilon && v <= max + epsilon;
|
|
685
608
|
});
|
|
686
609
|
}
|