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/dist/Color.js CHANGED
@@ -1,7 +1,8 @@
1
- import { colorModels, colorTypes, colorSpaces } from "./converters.js";
2
- import { cache, clean, fit } from "./utils.js";
3
- import { EASINGS } from "./math.js";
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 c = clean(color);
26
- for (const type in colorTypes) {
27
- const t = type;
28
- const { parse, bridge, toBridge, isValid } = colorTypes[t];
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
- throw new Error(`Unsupported or invalid color format: '${color}'.`);
32
+ catch (error) {
33
+ throw new Error(`Failed to parse color string '${color}'`, { cause: error });
34
+ }
37
35
  }
38
36
  /**
39
- * Returns the detected color type, or `undefined` if unrecognized.
37
+ * Validates a color string.
40
38
  *
41
- * @param color - Color string to analyze.
42
- * @param strict - Whether to validate full round-trip conversion.
39
+ * @param color - Color string to check.
40
+ * @param options - Validation options.
41
+ * @returns `true` if valid, otherwise `false`.
43
42
  */
44
- static type(color, strict = false) {
45
- const c = clean(color);
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
- if (type) {
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([...Object.keys(components), "alpha"]);
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 === "angle" ? [0, 360] : comp.value === "percentage" ? [0, 100] : 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 === "angle")
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 = colorTypes[t];
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 } = colorTypes[this.model];
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 (and alpha) to its numeric value.
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(fullComponents))
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 and alpha.
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 defs = {
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.slice(0, 3), coords[3]];
291
+ return [...fitted, coords[3]];
286
292
  }
287
293
  /**
288
- * Returns a new `Color` instance with updated or replaced component values.
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
- * @param values - Either:
328
- * - a partial object of component values,
329
- * - an updater function returning an object or array,
330
- * - or an array of new coordinates.
331
- * @returns A new `Color` instance with updated values.
332
- * @throws If the color model has no defined components.
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.toArray({ fit: "none", precision: null });
345
+ const coords = this.coords.slice();
339
346
  const { components } = colorModels[model];
340
- if (!components)
341
- throw new Error(`Model ${model} does not have defined components.`);
342
- const defs = {
343
- ...components,
344
- alpha: { index: 3, value: [0, 1], precision: 3 },
345
- };
346
- const names = Object.keys(defs);
347
- let newValues;
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 [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
362
- if (Number.isNaN(incoming))
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 } = defs[name];
377
- const current = coords[index];
369
+ const { index, value } = components[name];
378
370
  const raw = newValues[name];
379
- let val = typeof raw === "function" ? raw(current) : raw;
380
- if (typeof val === "number") {
381
- const [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
382
- if (Number.isNaN(val))
383
- val = 0;
384
- else if (val === Infinity)
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
- * Mixes this color with another by a specified amount.
382
+ * Creates a new Color instance with values fitted to the color model's gamut.
395
383
  *
396
- * @param other - The color to mix with. Can be a string or a `Color` instance.
397
- * @param options - Options for how the colors are mixed.
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
- mix(other, options = {}) {
402
- const { model } = this;
403
- const coords = this.toArray({ fit: "none", precision: null });
404
- const { components } = colorModels[model];
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 color (instance or string).
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] = o.in("xyz-d65").toArray({ fit: "none", precision: null });
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("oklab").toArray(coordsOptions);
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("lab").toArray(coordsOptions);
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("lab").toArray(coordsOptions);
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("lab").toArray(coordsOptions);
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 = 0;
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 or string to compare.
644
- * @param epsilon - Allowed floating-point difference (default: 1e-5).
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 = 1e-5) {
659
- const o = typeof other === "string" ? Color.from(other) : other;
660
- if (o.model === this.model)
661
- return this.coords.every((v, i) => Math.abs(v - o.coords[i]) <= epsilon);
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 = o.in("xyz-d65").toArray({ fit: "none", precision: null });
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 (default: 1e-5).
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 = 1e-5) {
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 === "angle" ? [0, 360] : [0, 100];
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
  }