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/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)
@@ -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;
@@ -223,7 +248,7 @@ export class Color {
223
248
  * @returns The formatted color string.
224
249
  */
225
250
  toString(options = {}) {
226
- const { format } = colorTypes[this.model];
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 (and alpha) to its numeric value.
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(fullComponents))
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 and alpha.
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 defs = {
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 normalize - Whether to normalize component values to their valid ranges. Defaults to `true`.
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, normalize = true) {
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 defs = {
353
- ...colorModels[model].components, // eslint-disable-line @typescript-eslint/no-explicit-any
354
- alpha: { index: 3, value: [0, 1], precision: 3 },
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 = defs[k];
360
- return [k, normalizeComponent(coords[def.index], def.value)];
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 = Object.values(defs).find((d) => d.index === i);
369
- return normalizeComponent(incoming, def.value);
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 } = defs[name];
369
+ const { index, value } = components[name];
378
370
  const raw = newValues[name];
379
- const prev = normalizeComponent(coords[index], value);
371
+ const prev = normalize(coords[index], value);
380
372
  const val = typeof raw === "function"
381
- ? normalizeComponent(raw(prev), value)
373
+ ? normalize(raw(prev), value)
382
374
  : typeof raw === "number"
383
- ? normalizeComponent(raw, value)
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
- * Mixes this color with another by a specified amount.
382
+ * Creates a new Color instance with values fitted to the color model's gamut.
391
383
  *
392
- * @param other - The color to mix with. Can be a string or a `Color` instance.
393
- * @param options - Options for how the colors are mixed.
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
- mix(other, options = {}) {
398
- const { model } = this;
399
- const A = this.coords.slice();
400
- const B = (typeof other === "string" ? Color.from(other) : other).in(model).coords.slice();
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 color (instance or string).
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] = o.in("xyz-d65").toArray({ fit: "none", precision: null });
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("oklab").toArray(coordsOptions);
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("lab").toArray(coordsOptions);
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("lab").toArray(coordsOptions);
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 color to compare against (as a Color instance or string).
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] = (typeof other === "string" ? Color.from(other) : other).in("lab").toArray(coordsOptions);
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 = 0;
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 or string to compare.
672
- * @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"`).
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 = 1e-5) {
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 = o.toArray({ fit: "none", precision: null });
690
- if (o.model === this.model)
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 = o.in("xyz-d65").toArray({ fit: "none", precision: null });
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 (default: 1e-5).
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 = 1e-5) {
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}'.`);