saturon 0.2.1 → 0.2.3

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/index.umd.js CHANGED
@@ -165,11 +165,11 @@ var Saturon = (() => {
165
165
  if (method === "none") {
166
166
  clipped = coords;
167
167
  } else if (method === "clip") {
168
- clipped = coords.slice(0, 3).map((v, i) => {
168
+ clipped = coords.map((v, i) => {
169
169
  const prop = componentProps[i];
170
170
  if (!prop)
171
171
  throw new Error(`Missing component properties for index ${i}.`);
172
- if (prop.value === "angle")
172
+ if (prop.value === "hue")
173
173
  return (v % 360 + 360) % 360;
174
174
  const [min, max] = Array.isArray(prop.value) ? prop.value : [0, 100];
175
175
  return Math.min(max, Math.max(min, v));
@@ -181,7 +181,7 @@ var Saturon = (() => {
181
181
  }
182
182
  clipped = fn(coords, model);
183
183
  }
184
- return clipped.slice(0, 3).map((v, i) => {
184
+ return clipped.map((v, i) => {
185
185
  let p;
186
186
  if (typeof precision === "number" || precision === null)
187
187
  p = precision;
@@ -204,10 +204,10 @@ var Saturon = (() => {
204
204
  return percent / 100 * (max2 - min2) / 2;
205
205
  return percent / 100 * (max2 - min2) + min2;
206
206
  };
207
- const parseAngle = (token2) => {
207
+ const parseHue = (token2) => {
208
208
  const value2 = parseFloat(token2);
209
209
  if (isNaN(value2))
210
- throw new Error(`Invalid angle value: '${token2}'.`);
210
+ throw new Error(`Invalid hue value: '${token2}'.`);
211
211
  if (token2.slice(-3) === "deg")
212
212
  return value2;
213
213
  if (token2.slice(-3) === "rad")
@@ -400,21 +400,21 @@ var Saturon = (() => {
400
400
  };
401
401
  let inner = token2.slice(5, -1).trim();
402
402
  if (inner === "infinity")
403
- return _max;
403
+ return Infinity;
404
404
  if (inner === "-infinity")
405
- return _min;
405
+ return -Infinity;
406
406
  if (inner === "NaN")
407
- return 0;
407
+ return NaN;
408
408
  inner = inner.replace(/(\d+(\.\d+)?)%/g, (m) => {
409
409
  if (relative === true)
410
- throw new Error("<angle> and <percentage> values are converted to <number> in relative syntax.");
410
+ throw new Error("<hue> and <percentage> values are converted to <number> in relative syntax.");
411
411
  const r = parsePercent(m, _min, _max);
412
412
  return r !== void 0 ? String(r) : "0";
413
413
  });
414
414
  inner = inner.replace(/(\d+(\.\d+)?)(deg|rad|grad|turn)/g, (_, num, __, unit) => {
415
415
  if (relative === true)
416
- throw new Error("<angle> and <percentage> values are converted to <number> in relative syntax.");
417
- return String(parseAngle(`${parseFloat(num)}${unit}`));
416
+ throw new Error("<hue> and <percentage> values are converted to <number> in relative syntax.");
417
+ return String(parseHue(`${parseFloat(num)}${unit}`));
418
418
  });
419
419
  const caclEnv = {
420
420
  ...base,
@@ -454,27 +454,18 @@ var Saturon = (() => {
454
454
  throw new Error(`Evaluation error: ${err}`);
455
455
  }
456
456
  };
457
- const evaluateAngle = () => {
457
+ const evaluateHue = () => {
458
458
  if (/^-?(?:\d+|\d*\.\d+)(?:deg|rad|grad|turn)$/.test(token)) {
459
- return parseAngle(token);
459
+ return parseHue(token);
460
460
  }
461
461
  if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
462
462
  return parseFloat(token);
463
463
  }
464
464
  const [min2, max2] = [0, 360];
465
- if (token[token.length - 1] === "%") {
466
- if (commaSeparated && supportsLegacy === true) {
467
- throw new Error("The legacy color syntax does not allow percentages for <angle> components.");
468
- }
469
- if (relative === true) {
470
- throw new Error("The relative color syntax doesn't allow percentages for <angle> components.");
471
- }
472
- return parsePercent(token, min2, max2);
473
- }
474
465
  if (token.slice(0, 5) === "calc(") {
475
466
  return parseCalc(token, min2, max2);
476
467
  }
477
- throw new Error(`Invalid angle value: '${token}'. Must be a number, a number with a unit (deg, rad, grad, turn), or a percentage.`);
468
+ throw new Error(`Invalid hue value: '${token}'. Must be a number, or a number with a unit (deg, rad, grad, turn).`);
478
469
  };
479
470
  const evaluatePercent = () => {
480
471
  if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
@@ -506,11 +497,11 @@ var Saturon = (() => {
506
497
  throw new Error(`Invalid number value: '${token}'. Must be a number${relative === false ? " or a percentage" : ""}.`);
507
498
  };
508
499
  if (token === "none")
509
- return 0;
500
+ return NaN;
510
501
  if (token in base)
511
502
  return base[token];
512
- if (value === "angle")
513
- return evaluateAngle();
503
+ if (value === "hue")
504
+ return evaluateHue();
514
505
  if (value === "percentage")
515
506
  return evaluatePercent();
516
507
  if (Array.isArray(value))
@@ -555,7 +546,7 @@ var Saturon = (() => {
555
546
  if (commaSeparated && token === "none") {
556
547
  throw new Error(`${fn}() cannot use "none" in comma-separated syntax.`);
557
548
  }
558
- if (meta.index !== 3 && meta.value !== "angle" && meta.value !== "percentage" && token.slice(0, 5) !== "calc(") {
549
+ if (meta.index !== 3 && meta.value !== "hue" && meta.value !== "percentage" && token.slice(0, 5) !== "calc(") {
559
550
  percentFlags.push(token.trim()[token.length - 1] === "%");
560
551
  }
561
552
  if (token) {
@@ -772,8 +763,8 @@ var Saturon = (() => {
772
763
  parse: (str) => {
773
764
  const tokens = tokenize(str);
774
765
  const ast = getAST(tokens);
775
- const components2 = parseAST(ast);
776
- return [...components2.slice(0, 3), components2[3] ?? 1];
766
+ const coords = parseAST(ast);
767
+ return [...coords.slice(0, 3), coords[3] ?? 1];
777
768
  },
778
769
  fromBridge: (coords) => [...fromBridge(coords), coords[3] ?? 1],
779
770
  format: ([c1, c2, c3, a = 1], options = {}) => {
@@ -781,11 +772,12 @@ var Saturon = (() => {
781
772
  const clipped = fit([c1, c2, c3], name, { method: fitMethod, precision });
782
773
  const alpha = Number(min(max(a, 0), 1).toFixed(3)).toString();
783
774
  const formatted = clipped.map((v, index) => {
775
+ v = isNaN(v) ? 0 : v;
784
776
  if ((units || legacy) && components) {
785
777
  const def = Object.values(components).find((comp) => comp.index === index);
786
778
  if (def?.value === "percentage")
787
779
  return `${v}%`;
788
- if (def?.value === "angle" && units)
780
+ if (def?.value === "hue" && units)
789
781
  return `${v}deg`;
790
782
  }
791
783
  return v.toString();
@@ -815,6 +807,7 @@ var Saturon = (() => {
815
807
  }
816
808
 
817
809
  // dist/math.js
810
+ var EPSILON = 1e-5;
818
811
  var MATRICES = {
819
812
  D50_to_D65: [
820
813
  [0.955473421488075, -0.02309845494876471, 0.06325924320057072],
@@ -1027,14 +1020,16 @@ var Saturon = (() => {
1027
1020
  const [r, g, b] = rgb.map((v) => v / 255);
1028
1021
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
1029
1022
  const L = (max + min) / 2;
1030
- let H = 0, S = 0;
1031
1023
  const d = max - min;
1032
- if (d !== 0) {
1024
+ let H = NaN, S = 0;
1025
+ if (d > 0) {
1033
1026
  S = d / (1 - Math.abs(2 * L - 1));
1034
1027
  H = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
1035
- H *= 60;
1028
+ H = H * 60 % 360;
1036
1029
  }
1037
- return [H % 360, S * 100, L * 100];
1030
+ const S_out = S * 100, L_out = L * 100;
1031
+ const invalid = L_out <= EPSILON || 100 - L_out <= EPSILON || S_out <= EPSILON;
1032
+ return [invalid ? NaN : H, S_out, L_out];
1038
1033
  }
1039
1034
  function HWB_to_RGB([h, w, b]) {
1040
1035
  w /= 100;
@@ -1049,11 +1044,9 @@ var Saturon = (() => {
1049
1044
  function RGB_to_HWB(rgb) {
1050
1045
  const [r, g, b] = rgb.map((v) => v / 255);
1051
1046
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
1052
- let H = 0;
1053
1047
  const d = max - min;
1054
- if (d === 0)
1055
- H = 0;
1056
- else {
1048
+ let H = NaN;
1049
+ if (d > EPSILON) {
1057
1050
  H = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
1058
1051
  H = H * 60 % 360;
1059
1052
  }
@@ -1085,7 +1078,7 @@ var Saturon = (() => {
1085
1078
  let H = Math.atan2(b, a) * 180 / Math.PI;
1086
1079
  if (H < 0)
1087
1080
  H += 360;
1088
- return [L, C, H];
1081
+ return [L, C, C <= EPSILON ? NaN : H];
1089
1082
  }
1090
1083
  function OKLAB_to_XYZD65(oklab) {
1091
1084
  const { LMS_to_XYZD65, OKLAB_to_LMS } = MATRICES;
@@ -1105,7 +1098,7 @@ var Saturon = (() => {
1105
1098
  let H = Math.atan2(b, a) * 180 / Math.PI;
1106
1099
  if (H < 0)
1107
1100
  H += 360;
1108
- return [L, C, H];
1101
+ return [L, C, C <= EPSILON ? NaN : H];
1109
1102
  }
1110
1103
 
1111
1104
  // dist/converters.js
@@ -1423,7 +1416,7 @@ var Saturon = (() => {
1423
1416
  supportsLegacy: true,
1424
1417
  alphaVariant: "hsla",
1425
1418
  components: {
1426
- h: { index: 0, value: "angle", precision: 0 },
1419
+ h: { index: 0, value: "hue", precision: 0 },
1427
1420
  s: { index: 1, value: "percentage", precision: 0 },
1428
1421
  l: { index: 2, value: "percentage", precision: 0 }
1429
1422
  },
@@ -1433,7 +1426,7 @@ var Saturon = (() => {
1433
1426
  },
1434
1427
  hwb: {
1435
1428
  components: {
1436
- h: { index: 0, value: "angle", precision: 0 },
1429
+ h: { index: 0, value: "hue", precision: 0 },
1437
1430
  w: { index: 1, value: "percentage", precision: 0 },
1438
1431
  b: { index: 2, value: "percentage", precision: 0 }
1439
1432
  },
@@ -1457,7 +1450,7 @@ var Saturon = (() => {
1457
1450
  components: {
1458
1451
  l: { index: 0, value: "percentage", precision: 5 },
1459
1452
  c: { index: 1, value: [0, 150], precision: 5 },
1460
- h: { index: 2, value: "angle", precision: 5 }
1453
+ h: { index: 2, value: "hue", precision: 5 }
1461
1454
  },
1462
1455
  bridge: "lab",
1463
1456
  toBridge: LCH_to_LAB,
@@ -1479,7 +1472,7 @@ var Saturon = (() => {
1479
1472
  components: {
1480
1473
  l: { index: 0, value: [0, 1], precision: 5 },
1481
1474
  c: { index: 1, value: [0, 0.4], precision: 5 },
1482
- h: { index: 2, value: "angle", precision: 5 }
1475
+ h: { index: 2, value: "hue", precision: 5 }
1483
1476
  },
1484
1477
  bridge: "oklab",
1485
1478
  toBridge: OKLCH_to_OKLAB,
@@ -1500,14 +1493,9 @@ var Saturon = (() => {
1500
1493
  const HEX = str.slice(1);
1501
1494
  if (![3, 4, 6, 8].includes(HEX.length))
1502
1495
  throw new Error("Invalid hex color length.");
1503
- for (const ch of HEX) {
1504
- const code = ch.charCodeAt(0);
1505
- const isDigit = code >= 48 && code <= 57;
1506
- const isLower = code >= 97 && code <= 102;
1507
- const isUpper = code >= 65 && code <= 70;
1508
- if (!(isDigit || isLower || isUpper))
1496
+ for (const ch of HEX)
1497
+ if (!/^[0-9a-fA-F]$/.test(ch))
1509
1498
  throw new Error("Invalid hex color character.");
1510
- }
1511
1499
  const expand = (c) => parseInt(c.length === 1 ? c + c : c, 16);
1512
1500
  const [r, g, b, a = 255] = HEX.length <= 4 ? HEX.split("").map(expand) : [HEX.slice(0, 2), HEX.slice(2, 4), HEX.slice(4, 6), HEX.slice(6, 8)].map((c) => parseInt(c || "ff", 16));
1513
1501
  return [r, g, b, a / 255];
@@ -1667,7 +1655,7 @@ var Saturon = (() => {
1667
1655
  if (!comps) {
1668
1656
  throw new Error(`Unknown color model: ${model}.`);
1669
1657
  }
1670
- const hasHue = Object.values(comps).some((c) => c.value === "angle");
1658
+ const hasHue = Object.values(comps).some((c) => c.value === "hue");
1671
1659
  if (!hasHue) {
1672
1660
  throw new Error(`Hue interpolation not supported in ${model} space.`);
1673
1661
  }
@@ -1677,14 +1665,14 @@ var Saturon = (() => {
1677
1665
  const { color: color1, weight: weight1 } = extractColorAndWeight(parts[1]);
1678
1666
  const { color: color2, weight: weight2 } = extractColorAndWeight(parts[2]);
1679
1667
  const { amount, alphaMultiplier = 1 } = getWeight2Prime(weight1, weight2);
1680
- return color1.with({ alpha: (a) => a * alphaMultiplier }).in(model).mix(color2.with({ alpha: (a) => a * alphaMultiplier }), { amount, hue }).in("rgb").toArray({ fit: "none", precision: null });
1668
+ return color1.with({ alpha: (a) => isNaN(a) ? a : a * alphaMultiplier }, false).in(model).mix(color2.with({ alpha: (a) => isNaN(a) ? a : a * alphaMultiplier }, false), { amount, hue }).in("rgb").coords;
1681
1669
  }
1682
1670
  },
1683
1671
  transparent: {
1684
1672
  isValid: (str) => str === "transparent",
1685
1673
  bridge: "rgb",
1686
1674
  toBridge: (coords) => coords,
1687
- parse: (str) => [0, 0, 0, 0]
1675
+ parse: (str) => [NaN, NaN, NaN, 0]
1688
1676
  // eslint-disable-line no-unused-vars
1689
1677
  }
1690
1678
  };
@@ -1714,7 +1702,7 @@ var Saturon = (() => {
1714
1702
  toBridge: (coords) => coords,
1715
1703
  parse: (str) => {
1716
1704
  const inner = str.slice(15, -1);
1717
- const [, luminance] = Color.from(inner).in("xyz-d65").toArray({ fit: "none", precision: null });
1705
+ const [, luminance] = Color.from(inner).in("xyz-d65").coords;
1718
1706
  return luminance > 0.5 ? [0, 0, 0, 1] : [255, 255, 255, 1];
1719
1707
  }
1720
1708
  },
@@ -1807,7 +1795,7 @@ var Saturon = (() => {
1807
1795
  if (idx < tokens.length && tokens[idx] === ",") {
1808
1796
  idx++;
1809
1797
  const fallbackStr = tokens.slice(idx).join(" ");
1810
- return Color.from(fallbackStr).in("rgb").toArray({ fit: "none", precision: null });
1798
+ return Color.from(fallbackStr).in("rgb").coords;
1811
1799
  }
1812
1800
  const red = 1 - Math.min(1, c * (1 - k) + k);
1813
1801
  const green = 1 - Math.min(1, m * (1 - k) + k);
@@ -1878,7 +1866,7 @@ var Saturon = (() => {
1878
1866
  }
1879
1867
  const [color1, color2] = parts;
1880
1868
  const { theme } = config;
1881
- return Color.from(theme === "light" ? color1 : color2).toArray({ fit: "none", precision: null });
1869
+ return Color.from(theme === "light" ? color1 : color2).coords;
1882
1870
  }
1883
1871
  }
1884
1872
  };
@@ -1989,7 +1977,7 @@ var Saturon = (() => {
1989
1977
  const v = Math.random() || 1e-9;
1990
1978
  value = base + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * dev;
1991
1979
  } else {
1992
- let [min, max] = comp.value === "angle" ? [0, 360] : comp.value === "percentage" ? [0, 100] : comp.value;
1980
+ let [min, max] = comp.value === "hue" ? [0, 360] : comp.value === "percentage" ? [0, 100] : comp.value;
1993
1981
  const limits = options.limits?.[name];
1994
1982
  if (limits) {
1995
1983
  const [lMin, lMax] = limits;
@@ -2002,7 +1990,7 @@ var Saturon = (() => {
2002
1990
  r = biasFn(r);
2003
1991
  value = min + r * (max - min);
2004
1992
  }
2005
- if (comp.value === "angle")
1993
+ if (comp.value === "hue")
2006
1994
  value = (value % 360 + 360) % 360;
2007
1995
  else if (comp.value === "percentage")
2008
1996
  value = Math.min(100, Math.max(0, value));
@@ -2052,7 +2040,6 @@ var Saturon = (() => {
2052
2040
  cache.set("paths", paths);
2053
2041
  }
2054
2042
  const key = `${from}-${to}`;
2055
- const coords = this.coords.slice(0, 3);
2056
2043
  if (!paths.has(key)) {
2057
2044
  const queue = [from], parent = { [from]: null };
2058
2045
  for (let i = 0; i < queue.length; i++) {
@@ -2075,7 +2062,7 @@ var Saturon = (() => {
2075
2062
  }
2076
2063
  paths.set(key, path2);
2077
2064
  }
2078
- let value = [...coords];
2065
+ let value = [...this.coords.map((v) => isNaN(v) ? 0 : v)];
2079
2066
  const path = paths.get(key);
2080
2067
  for (let i = 0; i < path.length - 1; i++) {
2081
2068
  const a = path[i], b = path[i + 1];
@@ -2141,7 +2128,7 @@ var Saturon = (() => {
2141
2128
  };
2142
2129
  const normalize = (c, i) => {
2143
2130
  const v = Object.values(defs)[i]?.value;
2144
- const [min, max] = Array.isArray(v) ? v : v === "angle" ? [0, 360] : [0, 100];
2131
+ const [min, max] = Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
2145
2132
  if (Number.isNaN(c))
2146
2133
  return 0;
2147
2134
  if (c === Infinity)
@@ -2155,10 +2142,10 @@ var Saturon = (() => {
2155
2142
  method,
2156
2143
  precision
2157
2144
  });
2158
- return [...fitted.slice(0, 3), coords[3]];
2145
+ return [...fitted, coords[3]];
2159
2146
  }
2160
2147
  /**
2161
- * Returns a new `Color` instance with updated or replaced component values.
2148
+ * Creates a new Color instance with modified component values.
2162
2149
  *
2163
2150
  * This method supports several flexible update styles:
2164
2151
  *
@@ -2197,66 +2184,60 @@ var Saturon = (() => {
2197
2184
  * color.with(({ r, g, b }) => [r * 0.5, g * 0.5, b * 0.5, 1]);
2198
2185
  * ```
2199
2186
  *
2200
- * @param values - Either:
2201
- * - a partial object of component values,
2202
- * - an updater function returning an object or array,
2203
- * - or an array of new coordinates.
2204
- * @returns A new `Color` instance with updated values.
2205
- * @throws If the color model has no defined components.
2187
+ * @template M - The color model type
2188
+ * @param values - The new component values to apply. Can be:
2189
+ * - A partial object mapping component names to numbers or update functions
2190
+ * - A function that receives current components and returns partial updates or an array of values
2191
+ * - An array of new values corresponding to component indices
2192
+ * @param normalize - Whether to normalize component values to their valid ranges. Defaults to `true`.
2193
+ * When `false`, values are not clamped or validated against their ranges.
2194
+ * @returns A new Color instance with the updated component values
2206
2195
  */
2207
2196
  /* eslint-disable no-unused-vars */
2208
- with(values) {
2197
+ with(values, normalize = true) {
2198
+ const normRange = (defValue) => Array.isArray(defValue) ? defValue : defValue === "hue" ? [0, 360] : [0, 100];
2199
+ const normalizeComponent = (c, defValue) => {
2200
+ if (normalize === false)
2201
+ return c;
2202
+ const [min, max] = normRange(defValue);
2203
+ if (Number.isNaN(c))
2204
+ return 0;
2205
+ if (c === Infinity)
2206
+ return max;
2207
+ if (c === -Infinity)
2208
+ return min;
2209
+ return c;
2210
+ };
2209
2211
  const { model } = this;
2210
- const coords = this.toArray({ fit: "none", precision: null });
2211
- const { components } = colorModels[model];
2212
- if (!components)
2213
- throw new Error(`Model ${model} does not have defined components.`);
2212
+ const coords = this.coords.slice();
2214
2213
  const defs = {
2215
- ...components,
2214
+ ...colorModels[model].components,
2215
+ // eslint-disable-line @typescript-eslint/no-explicit-any
2216
2216
  alpha: { index: 3, value: [0, 1], precision: 3 }
2217
2217
  };
2218
2218
  const names = Object.keys(defs);
2219
- let newValues;
2220
- if (typeof values === "function") {
2221
- const result = values(Object.fromEntries(names.map((c) => [c, coords[defs[c].index]])));
2222
- newValues = result;
2223
- } else {
2224
- newValues = values;
2225
- }
2219
+ const newValues = typeof values === "function" ? values(Object.fromEntries(names.map((k) => {
2220
+ const def = defs[k];
2221
+ return [k, normalizeComponent(coords[def.index], def.value)];
2222
+ }))) : values;
2226
2223
  if (Array.isArray(newValues)) {
2227
2224
  const adjusted = coords.map((curr, i) => {
2228
2225
  const incoming = newValues[i];
2229
- const { value } = Object.values(defs).find((d) => d.index === i);
2230
2226
  if (typeof incoming !== "number")
2231
2227
  return curr;
2232
- const [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
2233
- if (Number.isNaN(incoming))
2234
- return 0;
2235
- if (incoming === Infinity)
2236
- return max;
2237
- if (incoming === -Infinity)
2238
- return min;
2239
- return incoming;
2228
+ const def = Object.values(defs).find((d) => d.index === i);
2229
+ return normalizeComponent(incoming, def.value);
2240
2230
  });
2241
- return new _Color(model, [...adjusted.slice(0, 3), coords[3]]);
2231
+ return new _Color(model, [...adjusted.slice(0, 3), coords[3] ?? 1]);
2242
2232
  }
2243
2233
  const next = [...coords];
2244
2234
  for (const name of names) {
2245
2235
  if (!(name in newValues))
2246
2236
  continue;
2247
2237
  const { index, value } = defs[name];
2248
- const current = coords[index];
2249
2238
  const raw = newValues[name];
2250
- let val = typeof raw === "function" ? raw(current) : raw;
2251
- if (typeof val === "number") {
2252
- const [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
2253
- if (Number.isNaN(val))
2254
- val = 0;
2255
- else if (val === Infinity)
2256
- val = max;
2257
- else if (val === -Infinity)
2258
- val = min;
2259
- }
2239
+ const prev = normalizeComponent(coords[index], value);
2240
+ const val = typeof raw === "function" ? normalizeComponent(raw(prev), value) : typeof raw === "number" ? normalizeComponent(raw, value) : coords[index];
2260
2241
  next[index] = val;
2261
2242
  }
2262
2243
  return new _Color(model, [...next.slice(0, 3), next[3] ?? coords[3]]);
@@ -2271,69 +2252,89 @@ var Saturon = (() => {
2271
2252
  */
2272
2253
  mix(other, options = {}) {
2273
2254
  const { model } = this;
2274
- const coords = this.toArray({ fit: "none", precision: null });
2255
+ const A = this.coords.slice();
2256
+ const B = (typeof other === "string" ? _Color.from(other) : other).in(model).coords.slice();
2275
2257
  const { components } = colorModels[model];
2276
2258
  if (!components)
2277
2259
  throw new Error(`Model ${model} does not have defined components.`);
2278
- const { hue = "shorter", amount = 0.5, easing = "linear", gamma = 1 } = options;
2279
2260
  const defs = {
2280
2261
  ...components,
2281
2262
  alpha: { index: 3, value: [0, 1], precision: 3 }
2282
2263
  };
2264
+ const normRange = (v) => Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
2265
+ const repairPair = (a, b, v) => {
2266
+ const [min, max] = normRange(v);
2267
+ const fixInf = (x) => x === Infinity ? max : x === -Infinity ? min : x;
2268
+ a = fixInf(a);
2269
+ b = fixInf(b);
2270
+ const aNaN = Number.isNaN(a);
2271
+ const bNaN = Number.isNaN(b);
2272
+ if (aNaN && bNaN)
2273
+ return { a: 0, b: 0 };
2274
+ if (aNaN)
2275
+ return { a: b, b };
2276
+ if (bNaN)
2277
+ return { a, b: a };
2278
+ return { a, b, ok: true };
2279
+ };
2280
+ const hueIndex = defs.h?.index ?? -1;
2281
+ for (const key in defs) {
2282
+ const { index, value } = defs[key];
2283
+ if (index > 3)
2284
+ continue;
2285
+ const r = repairPair(A[index], B[index], value);
2286
+ if (r.ok) {
2287
+ const [min, max] = normRange(value);
2288
+ const fixInf = (x) => x === Infinity ? max : x === -Infinity ? min : x;
2289
+ A[index] = fixInf(A[index]);
2290
+ B[index] = fixInf(B[index]);
2291
+ } else {
2292
+ A[index] = r.a;
2293
+ B[index] = r.b;
2294
+ }
2295
+ }
2296
+ const { hue = "shorter", amount = 0.5, easing = "linear", gamma = 1 } = options;
2297
+ const ease = typeof easing === "function" ? easing : EASINGS[easing];
2298
+ const t = ease(Math.min(1, Math.max(0, amount)));
2299
+ const tt = Math.pow(t, 1 / gamma);
2300
+ const wrap = (v) => (v % 360 + 360) % 360;
2283
2301
  const hueDelta = (a, b) => {
2284
- const d = ((b - a) % 360 + 360) % 360;
2302
+ const d = wrap(b - a);
2285
2303
  return d > 180 ? d - 360 : d;
2286
2304
  };
2287
2305
  const hueDeltaLong = (a, b) => hueDelta(a, b) >= 0 ? hueDelta(a, b) - 360 : hueDelta(a, b) + 360;
2288
- const interpolateHue = (a, b, t2, method) => {
2289
- const wrapped = (v) => (v % 360 + 360) % 360;
2306
+ const interpHue = (a, b, t2, method) => {
2290
2307
  switch (method) {
2291
2308
  case "shorter":
2292
- return wrapped(a + t2 * hueDelta(a, b));
2309
+ return wrap(a + t2 * hueDelta(a, b));
2293
2310
  case "longer":
2294
- return wrapped(a + t2 * hueDeltaLong(a, b));
2311
+ return wrap(a + t2 * hueDeltaLong(a, b));
2295
2312
  case "increasing":
2296
- return wrapped(a * (1 - t2) + (b < a ? b + 360 : b) * t2);
2313
+ return wrap(a * (1 - t2) + (b < a ? b + 360 : b) * t2);
2297
2314
  case "decreasing":
2298
- return wrapped(a * (1 - t2) + (b > a ? b - 360 : b) * t2);
2299
- default:
2300
- throw new Error(`Invalid hue interpolation method: ${method}`);
2315
+ return wrap(a * (1 - t2) + (b > a ? b - 360 : b) * t2);
2301
2316
  }
2317
+ throw new Error(`Invalid hue interpolation: ${method}`);
2302
2318
  };
2303
- const t = Math.max(0, Math.min(1, amount));
2304
- const ease = typeof easing === "function" ? easing : EASINGS[easing];
2305
- const tt = Math.pow(ease(t), 1 / gamma);
2306
- const otherColor = typeof other === "string" ? _Color.from(other) : other;
2307
- const thisCoords = coords.slice(0, 3);
2308
- const otherCoords = otherColor.in(model).toArray({ fit: "none", precision: null }).slice(0, 3);
2309
- const thisAlpha = coords[3];
2310
- const otherAlpha = otherColor.coords[3];
2311
- const hueIndex = Object.entries(defs).find(([k]) => k === "h")?.[1].index;
2312
- if (t === 0)
2313
- return new _Color(model, [...thisCoords, thisAlpha]);
2314
- if (t === 1)
2315
- return new _Color(model, [...otherCoords, otherAlpha]);
2316
- if (thisAlpha < 1 || otherAlpha < 1) {
2317
- const premixed = thisCoords.map((start, i) => {
2318
- const end = otherCoords[i];
2319
- if (i === hueIndex)
2320
- return interpolateHue(start, end, tt, hue);
2321
- const a = start * thisAlpha;
2322
- const b = end * otherAlpha;
2323
- return a * (1 - tt) + b * tt;
2319
+ if (tt === 0)
2320
+ return new _Color(model, [...A]);
2321
+ if (tt === 1)
2322
+ return new _Color(model, [...B]);
2323
+ const aA = A[3];
2324
+ const aB = B[3];
2325
+ const resolvedA = A.slice(0, 3);
2326
+ const resolvedB = B.slice(0, 3);
2327
+ if (aA < 1 || aB < 1) {
2328
+ const premixed = resolvedA.map((a2, i) => {
2329
+ const b = resolvedB[i];
2330
+ return i === hueIndex ? interpHue(a2, b, tt, hue) : a2 * aA * (1 - tt) + b * aB * tt;
2324
2331
  });
2325
- const mixedAlpha = thisAlpha * (1 - tt) + otherAlpha * tt;
2326
- const mixed = mixedAlpha > 0 ? premixed.map((c, i) => i === hueIndex ? c : c / mixedAlpha) : thisCoords.map((_, i) => i === hueIndex ? premixed[i] : 0);
2327
- return new _Color(model, [...mixed, mixedAlpha]);
2332
+ const a = aA * (1 - tt) + aB * tt;
2333
+ const out = a > 0 ? premixed.map((v, i) => i === hueIndex ? v : v / a) : resolvedA.map((_, i) => i === hueIndex ? premixed[i] : 0);
2334
+ return new _Color(model, [...out, a]);
2328
2335
  }
2329
- const mixedCoords = thisCoords.map((start, i) => {
2330
- const entry = Object.values(defs).find((d) => d.index === i);
2331
- if (!entry)
2332
- return start;
2333
- const end = otherCoords[i];
2334
- return entry.value === "angle" ? interpolateHue(start, end, tt, hue) : start + (end - start) * tt;
2335
- });
2336
- return new _Color(model, [...mixedCoords, 1]);
2336
+ const mixed = resolvedA.map((a, i) => i === hueIndex ? interpHue(a, resolvedB[i], tt, hue) : a + (resolvedB[i] - a) * tt);
2337
+ return new _Color(model, [...mixed, 1]);
2337
2338
  }
2338
2339
  /**
2339
2340
  * Fits this color within the specified gamut using a given method.
@@ -2350,6 +2351,17 @@ var Saturon = (() => {
2350
2351
  const fitted = this.in(g).toArray({ fit: method, precision: null });
2351
2352
  return new _Color(g, fitted).in(this.model);
2352
2353
  }
2354
+ /**
2355
+ * Creates a new Color instance with values fitted to the color model's gamut.
2356
+ *
2357
+ * @param options - Configuration options for fitting
2358
+ * @returns A new Color instance with fitted values
2359
+ */
2360
+ fit(options = {}) {
2361
+ const { fit: method = config.defaults.fit, precision } = options;
2362
+ const fitted = this.toArray({ fit: method, precision });
2363
+ return new _Color(this.model, fitted);
2364
+ }
2353
2365
  /**
2354
2366
  * Calculates the WCAG 2.1 contrast ratio between this color and another.
2355
2367
  *
@@ -2523,8 +2535,10 @@ var Saturon = (() => {
2523
2535
  */
2524
2536
  equals(other, epsilon = 1e-5) {
2525
2537
  const o = typeof other === "string" ? _Color.from(other) : other;
2538
+ const thisCoords = this.toArray({ fit: "none", precision: null });
2539
+ const otherCoords = o.toArray({ fit: "none", precision: null });
2526
2540
  if (o.model === this.model)
2527
- return this.coords.every((v, i) => Math.abs(v - o.coords[i]) <= epsilon);
2541
+ return thisCoords.every((v, i) => Math.abs(v - otherCoords[i]) <= epsilon);
2528
2542
  const a = this.in("xyz-d65").toArray({ fit: "none", precision: null });
2529
2543
  const b = o.in("xyz-d65").toArray({ fit: "none", precision: null });
2530
2544
  return a.every((v, i) => Math.abs(v - b[i]) <= epsilon);
@@ -2546,7 +2560,7 @@ var Saturon = (() => {
2546
2560
  const coords = this.in(g).toArray({ fit: "none", precision: null });
2547
2561
  return Object.values(components).every(({ index, value }) => {
2548
2562
  const v = coords[index];
2549
- const [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
2563
+ const [min, max] = Array.isArray(value) ? value : value === "hue" ? [0, 360] : [0, 100];
2550
2564
  return v >= min - epsilon && v <= max + epsilon;
2551
2565
  });
2552
2566
  }