@tenphi/glaze 0.14.0 → 0.15.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">Glaze</h1>
6
6
 
7
7
  <p align="center">
8
- OKHSL-based color theme generator with WCAG contrast solving
8
+ OKHST-based color theme generator with WCAG contrast solving
9
9
  </p>
10
10
 
11
11
  <p align="center">
package/dist/index.cjs CHANGED
@@ -258,10 +258,48 @@ const getCs = (L, a, b, cusp) => {
258
258
  cMax
259
259
  ];
260
260
  };
261
+ const CYAN_A = Math.cos(199.8 * Math.PI / 180);
262
+ const CYAN_B = Math.sin(199.8 * Math.PI / 180);
263
+ const BLUE_A = Math.cos(267.4 * Math.PI / 180);
264
+ const BLUE_B = Math.sin(267.4 * Math.PI / 180);
265
+ let cyanCusp;
266
+ let blueCusp;
267
+ /**
268
+ * Computes the maximum safe OKLCH chroma that fits inside the sRGB gamut
269
+ * for all possible hues at a given OKLab lightness `L`.
270
+ */
271
+ function computeSafeChromaOKLCH(L) {
272
+ if (!cyanCusp) cyanCusp = findCuspOKLCH(CYAN_A, CYAN_B);
273
+ if (!blueCusp) blueCusp = findCuspOKLCH(BLUE_A, BLUE_B);
274
+ const c1 = findGamutIntersectionOKLCH(CYAN_A, CYAN_B, L, 1, L, cyanCusp);
275
+ const c2 = findGamutIntersectionOKLCH(BLUE_A, BLUE_B, L, 1, L, blueCusp);
276
+ return Math.min(c1, c2);
277
+ }
278
+ /** Per-hue cusp-lightness cache. The cusp is mode-independent, so keying on
279
+ * a rounded hue is safe and keeps the cache small. */
280
+ const cuspLightnessCache = /* @__PURE__ */ new Map();
281
+ /**
282
+ * OKHSL lightness of the gamut cusp for a hue — the lightness where the
283
+ * realizable chroma peaks. Reuses the same `find_cusp` OKHSL already runs for
284
+ * its `s` normalization (no new color math); the OKLab cusp lightness is run
285
+ * through the OKHSL `toe` and clamped to `[0.001, 0.999]` so divisions that
286
+ * key off it stay safe. Cached per (rounded) hue.
287
+ *
288
+ * @param h Hue, 0–360.
289
+ */
290
+ function cuspLightness(h) {
291
+ const key = Math.round(constrainAngle(h) * 100) / 100;
292
+ const cached = cuspLightnessCache.get(key);
293
+ if (cached !== void 0) return cached;
294
+ const hNorm = key / 360;
295
+ const lc = clampVal(toe(findCuspOKLCH(Math.cos(TAU * hNorm), Math.sin(TAU * hNorm))[0]), .001, .999);
296
+ cuspLightnessCache.set(key, lc);
297
+ return lc;
298
+ }
261
299
  /**
262
300
  * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
263
301
  */
264
- function okhslToOklab(h, s, l) {
302
+ function okhslToOklab(h, s, l, pastel = false) {
265
303
  const L = toeInv(l);
266
304
  let a = 0;
267
305
  let b = 0;
@@ -269,24 +307,30 @@ function okhslToOklab(h, s, l) {
269
307
  if (L !== 0 && L !== 1 && s !== 0) {
270
308
  const a_ = Math.cos(TAU * hNorm);
271
309
  const b_ = Math.sin(TAU * hNorm);
272
- const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
273
- const mid = .8;
274
- const midInv = 1.25;
275
- let t, k0, k1, k2;
276
- if (s < mid) {
277
- t = midInv * s;
278
- k0 = 0;
279
- k1 = mid * c0;
280
- k2 = 1 - k1 / cMid;
310
+ if (pastel) {
311
+ const c = s * computeSafeChromaOKLCH(L);
312
+ a = c * a_;
313
+ b = c * b_;
281
314
  } else {
282
- t = 5 * (s - .8);
283
- k0 = cMid;
284
- k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
285
- k2 = 1 - k1 / (cMax - cMid);
315
+ const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
316
+ const mid = .8;
317
+ const midInv = 1.25;
318
+ let t, k0, k1, k2;
319
+ if (s < mid) {
320
+ t = midInv * s;
321
+ k0 = 0;
322
+ k1 = mid * c0;
323
+ k2 = 1 - k1 / cMid;
324
+ } else {
325
+ t = 5 * (s - .8);
326
+ k0 = cMid;
327
+ k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
328
+ k2 = 1 - k1 / (cMax - cMid);
329
+ }
330
+ const c = k0 + t * k1 / (1 - k2 * t);
331
+ a = c * a_;
332
+ b = c * b_;
286
333
  }
287
- const c = k0 + t * k1 / (1 - k2 * t);
288
- a = c * a_;
289
- b = c * b_;
290
334
  }
291
335
  return [
292
336
  L,
@@ -298,8 +342,8 @@ function okhslToOklab(h, s, l) {
298
342
  * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
299
343
  * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
300
344
  */
301
- function okhslToLinearSrgb(h, s, l) {
302
- return OKLabToLinearSRGB(okhslToOklab(h, s, l));
345
+ function okhslToLinearSrgb(h, s, l, pastel = false) {
346
+ return OKLabToLinearSRGB(okhslToOklab(h, s, l, pastel));
303
347
  }
304
348
  /**
305
349
  * Compute relative luminance Y from linear sRGB channels.
@@ -329,8 +373,8 @@ const sRGBGammaToLinear = (val) => {
329
373
  /**
330
374
  * Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
331
375
  */
332
- function okhslToSrgb(h, s, l) {
333
- const lin = okhslToLinearSrgb(h, s, l);
376
+ function okhslToSrgb(h, s, l, pastel = false) {
377
+ const lin = okhslToLinearSrgb(h, s, l, pastel);
334
378
  return [
335
379
  Math.max(0, Math.min(1, sRGBLinearToGamma(lin[0]))),
336
380
  Math.max(0, Math.min(1, sRGBLinearToGamma(lin[1]))),
@@ -372,7 +416,7 @@ const linearSrgbToOklab = (rgb) => {
372
416
  * Input: [L, a, b] where L: 0–1, a/b: roughly -0.5 to 0.5.
373
417
  * Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
374
418
  */
375
- const oklabToOkhsl = (lab) => {
419
+ const oklabToOkhsl = (lab, pastel = false) => {
376
420
  const L = lab[0];
377
421
  const a = lab[1];
378
422
  const b = lab[2];
@@ -392,19 +436,22 @@ const oklabToOkhsl = (lab) => {
392
436
  const b_ = b / C;
393
437
  let h = Math.atan2(b, a) * (180 / Math.PI);
394
438
  h = constrainAngle(h);
395
- const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
396
- const mid = .8;
397
- const midInv = 1.25;
398
439
  let s;
399
- if (C < cMid) {
400
- const k1 = mid * c0;
401
- s = C / (k1 + C * (1 - k1 / cMid)) / midInv;
402
- } else {
403
- const k0 = cMid;
404
- const k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
405
- const k2 = 1 - k1 / (cMax - cMid);
406
- const cDiff = C - k0;
407
- s = mid + cDiff / (k1 + cDiff * k2) / 5;
440
+ if (pastel) s = C / computeSafeChromaOKLCH(L);
441
+ else {
442
+ const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
443
+ const mid = .8;
444
+ const midInv = 1.25;
445
+ if (C < cMid) {
446
+ const k1 = mid * c0;
447
+ s = C / (k1 + C * (1 - k1 / cMid)) / midInv;
448
+ } else {
449
+ const k0 = cMid;
450
+ const k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
451
+ const k2 = 1 - k1 / (cMax - cMid);
452
+ const cDiff = C - k0;
453
+ s = mid + cDiff / (k1 + cDiff * k2) / 5;
454
+ }
408
455
  }
409
456
  const l = toe(L);
410
457
  return [
@@ -417,12 +464,12 @@ const oklabToOkhsl = (lab) => {
417
464
  * Convert gamma-encoded sRGB (0–1 per channel) to OKHSL.
418
465
  * Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
419
466
  */
420
- function srgbToOkhsl(rgb) {
467
+ function srgbToOkhsl(rgb, pastel = false) {
421
468
  return oklabToOkhsl(linearSrgbToOklab([
422
469
  sRGBGammaToLinear(rgb[0]),
423
470
  sRGBGammaToLinear(rgb[1]),
424
471
  sRGBGammaToLinear(rgb[2])
425
- ]));
472
+ ]), pastel);
426
473
  }
427
474
  /**
428
475
  * Convert CSS HSL (sRGB-based) to gamma-encoded sRGB [r, g, b] in 0–1 range.
@@ -537,24 +584,26 @@ function fmt$1(value, decimals) {
537
584
  * Format OKHSL values as a CSS `okhsl(H S% L%)` string.
538
585
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
539
586
  */
540
- function formatOkhsl(h, s, l) {
541
- return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
587
+ function formatOkhsl(h, s, l, pastel = false) {
588
+ let outS = s;
589
+ if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s / 100, l / 100, true), false)[1] * 100;
590
+ return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(l, 2)}%)`;
542
591
  }
543
592
  /**
544
593
  * Format OKHSL values as a CSS `rgb(R G B)` string.
545
594
  * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
546
595
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
547
596
  */
548
- function formatRgb(h, s, l) {
549
- const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
597
+ function formatRgb(h, s, l, pastel = false) {
598
+ const [r, g, b] = okhslToSrgb(h, s / 100, l / 100, pastel);
550
599
  return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
551
600
  }
552
601
  /**
553
602
  * Format OKHSL values as a CSS `hsl(H S% L%)` string.
554
603
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
555
604
  */
556
- function formatHsl(h, s, l) {
557
- const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
605
+ function formatHsl(h, s, l, pastel = false) {
606
+ const [r, g, b] = okhslToSrgb(h, s / 100, l / 100, pastel);
558
607
  const max = Math.max(r, g, b);
559
608
  const min = Math.min(r, g, b);
560
609
  const delta = max - min;
@@ -573,8 +622,8 @@ function formatHsl(h, s, l) {
573
622
  * Format OKHSL values as a CSS `oklch(L C H)` string.
574
623
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
575
624
  */
576
- function formatOklch(h, s, l) {
577
- const [L, a, b] = okhslToOklab(h, s / 100, l / 100);
625
+ function formatOklch(h, s, l, pastel = false) {
626
+ const [L, a, b] = okhslToOklab(h, s / 100, l / 100, pastel);
578
627
  const C = Math.sqrt(a * a + b * b);
579
628
  let hh = Math.atan2(b, a) * (180 / Math.PI);
580
629
  hh = constrainAngle(hh);
@@ -600,7 +649,6 @@ function defaultConfig() {
600
649
  eps: .05
601
650
  },
602
651
  darkDesaturation: .1,
603
- saturationTaper: .15,
604
652
  states: {
605
653
  dark: "@dark",
606
654
  highContrast: "@high-contrast"
@@ -609,7 +657,8 @@ function defaultConfig() {
609
657
  dark: true,
610
658
  highContrast: false
611
659
  },
612
- autoFlip: true
660
+ autoFlip: true,
661
+ pastel: false
613
662
  };
614
663
  }
615
664
  let globalConfig = defaultConfig();
@@ -639,7 +688,6 @@ function configure(config) {
639
688
  lightTone: config.lightTone ?? globalConfig.lightTone,
640
689
  darkTone: config.darkTone ?? globalConfig.darkTone,
641
690
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
642
- saturationTaper: config.saturationTaper ?? globalConfig.saturationTaper,
643
691
  states: {
644
692
  dark: config.states?.dark ?? globalConfig.states.dark,
645
693
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -649,7 +697,8 @@ function configure(config) {
649
697
  highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
650
698
  },
651
699
  shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
652
- autoFlip: config.autoFlip ?? globalConfig.autoFlip
700
+ autoFlip: config.autoFlip ?? globalConfig.autoFlip,
701
+ pastel: config.pastel ?? globalConfig.pastel
653
702
  };
654
703
  }
655
704
  function resetConfig() {
@@ -668,11 +717,11 @@ function mergeConfig(base, override) {
668
717
  lightTone: override.lightTone !== void 0 ? override.lightTone : base.lightTone,
669
718
  darkTone: override.darkTone !== void 0 ? override.darkTone : base.darkTone,
670
719
  darkDesaturation: override.darkDesaturation ?? base.darkDesaturation,
671
- saturationTaper: override.saturationTaper ?? base.saturationTaper,
672
720
  states: base.states,
673
721
  modes: base.modes,
674
722
  shadowTuning: override.shadowTuning ?? base.shadowTuning,
675
- autoFlip: override.autoFlip ?? base.autoFlip
723
+ autoFlip: override.autoFlip ?? base.autoFlip,
724
+ pastel: override.pastel ?? base.pastel
676
725
  };
677
726
  }
678
727
 
@@ -765,8 +814,8 @@ function isAbsoluteTone(tone) {
765
814
  * - the `{ h, s, t }` <-> `{ h, s, l }` color-space converters,
766
815
  * - the resolved-variant edge adapter (`variantToOkhsl`),
767
816
  * - the per-scheme tone mapping that replaced the Möbius dark curve
768
- * (`mapToneForScheme`), the saturation reducers, and the solver's
769
- * scheme tone range.
817
+ * (`mapToneForScheme`), the dark desaturation reducer, and the solver's scheme
818
+ * tone range.
770
819
  *
771
820
  * See `docs/okhst.md` for the full specification and the calibrated
772
821
  * default constants.
@@ -909,33 +958,6 @@ function mapSaturationDark(s, mode, config) {
909
958
  if (mode === "static") return s;
910
959
  return s * (1 - config.darkDesaturation);
911
960
  }
912
- /** Smoothstep `0..1`. */
913
- function smoothstep(x) {
914
- const t = clamp(x, 0, 1);
915
- return t * t * (3 - 2 * t);
916
- }
917
- /** Fraction of the tone range over which the taper ramps in, per end. */
918
- const TAPER_REGION = .15;
919
- /**
920
- * Gently taper saturation toward the tone extremes, where in-gamut chroma
921
- * collapses and high saturation reads as noise. `taper` is the *strength*
922
- * (0–1): the maximum fraction of saturation removed at the very edges. The
923
- * rolloff ramps in smoothly over the outer {@link TAPER_REGION} of tone on
924
- * each end, so mid-tones are untouched and high-tone surfaces keep most of
925
- * their color. `taper = 0` disables the effect.
926
- *
927
- * @param s Saturation (0–1).
928
- * @param toneFinal Stored canonical tone (0–1).
929
- * @param taper Strength (0–1); default config is a gentle 0.15.
930
- */
931
- function saturationEnvelope(s, toneFinal, taper) {
932
- if (taper <= 0) return s;
933
- const t = clamp(toneFinal, 0, 1);
934
- const strength = clamp(taper, 0, 1);
935
- const edge = Math.min(t, 1 - t);
936
- if (edge >= TAPER_REGION) return s;
937
- return s * (1 - strength * (1 - smoothstep(edge / TAPER_REGION)));
938
- }
939
961
  /**
940
962
  * Tone search range (0–1) for the contrast solver in a given scheme.
941
963
  * `static` searches the full range; otherwise the scheme window's tone
@@ -1040,12 +1062,12 @@ const cacheOrder = [];
1040
1062
  * the metric's luminance basis. The metric is part of the cache key because
1041
1063
  * WCAG and APCA derive different luminances from the same color.
1042
1064
  */
1043
- function cachedLuminance(metric, h, s, t) {
1065
+ function cachedLuminance(metric, h, s, t, pastel) {
1044
1066
  const tRounded = Math.round(t * 1e4) / 1e4;
1045
- const key = `${metric}|${h}|${s}|${tRounded}`;
1067
+ const key = `${metric}|${h}|${s}|${tRounded}|${pastel}`;
1046
1068
  const cached = luminanceCache.get(key);
1047
1069
  if (cached !== void 0) return cached;
1048
- const y = metricLuminance(metric, okhslToLinearSrgb(h, s, fromTone(tRounded * 100, REF_EPS)));
1070
+ const y = metricLuminance(metric, okhslToLinearSrgb(h, s, fromTone(tRounded * 100, REF_EPS), pastel));
1049
1071
  if (luminanceCache.size >= CACHE_SIZE) {
1050
1072
  const evict = cacheOrder.shift();
1051
1073
  luminanceCache.delete(evict);
@@ -1176,14 +1198,11 @@ function solveNearestContrast(opts) {
1176
1198
  * staying as close to `preferredTone` as possible.
1177
1199
  */
1178
1200
  function findToneForContrast(options) {
1179
- const { hue, saturation, preferredTone, baseLinearRgb, contrast, toneRange = [0, 1], epsilon = 1e-4, maxIterations = 18 } = options;
1201
+ const { hue, saturation, preferredTone, baseLinearRgb, contrast, toneRange = [0, 1], epsilon = 1e-4, maxIterations = 18, pastel = false } = options;
1180
1202
  const { metric, target } = contrast;
1181
1203
  const searchTarget = metric === "wcag" ? target * 1.01 : target + .5;
1182
1204
  const yBase = metricLuminance(metric, baseLinearRgb);
1183
- const taper = options.saturationTaper ?? 0;
1184
- const lum = taper > 0 ? (t) => {
1185
- return metricLuminance(metric, okhslToLinearSrgb(hue, saturationEnvelope(saturation, t, taper), fromTone(t * 100, REF_EPS)));
1186
- } : (t) => cachedLuminance(metric, hue, saturation, t);
1205
+ const lum = (t) => cachedLuminance(metric, hue, saturation, t, pastel);
1187
1206
  const scorePref = metricScore(metric, lum(preferredTone), yBase);
1188
1207
  if (scorePref >= searchTarget) return {
1189
1208
  tone: preferredTone,
@@ -1587,7 +1606,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1587
1606
  const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast);
1588
1607
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
1589
1608
  const baseOkhsl = toOkhslVariant(baseVariant);
1590
- const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l);
1609
+ const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, ctx.config.pastel);
1591
1610
  const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
1592
1611
  let initialDirection;
1593
1612
  if (preferredTone < baseTone) initialDirection = "darker";
@@ -1601,7 +1620,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1601
1620
  toneRange: [0, 1],
1602
1621
  initialDirection,
1603
1622
  flip,
1604
- saturationTaper: ctx.config.saturationTaper
1623
+ pastel: ctx.config.pastel
1605
1624
  });
1606
1625
  if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
1607
1626
  return {
@@ -1633,9 +1652,8 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1633
1652
  satFactor = dep.satFactor;
1634
1653
  }
1635
1654
  const baseSat = satFactor * ctx.saturation / 100;
1636
- let finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
1655
+ const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
1637
1656
  const toneFraction = clamp(finalTone / 100, 0, 1);
1638
- finalSat = saturationEnvelope(finalSat, toneFraction, ctx.config.saturationTaper);
1639
1657
  return {
1640
1658
  h: effectiveHue,
1641
1659
  s: clamp(finalSat, 0, 1),
@@ -1651,8 +1669,8 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
1651
1669
  const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
1652
1670
  return toToneVariant(computeShadow(bgVariant, fgVariant, intensity, tuning));
1653
1671
  }
1654
- function okhslVariantToLinearRgb(v) {
1655
- return okhslToLinearSrgb(v.h, v.s, v.l);
1672
+ function okhslVariantToLinearRgb(v, pastel) {
1673
+ return okhslToLinearSrgb(v.h, v.s, v.l, pastel);
1656
1674
  }
1657
1675
  /**
1658
1676
  * Resolve hue for OKHSL mixing, handling achromatic colors.
@@ -1675,12 +1693,12 @@ function linearSrgbLerp(base, target, t) {
1675
1693
  base[2] + (target[2] - base[2]) * t
1676
1694
  ];
1677
1695
  }
1678
- function linearRgbToToneVariant(rgb) {
1696
+ function linearRgbToToneVariant(rgb, pastel) {
1679
1697
  const [h, s, l] = srgbToOkhsl([
1680
1698
  Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
1681
1699
  Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
1682
1700
  Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
1683
- ]);
1701
+ ], pastel);
1684
1702
  return toToneVariant({
1685
1703
  h,
1686
1704
  s,
@@ -1696,15 +1714,15 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1696
1714
  let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
1697
1715
  const blend = def.blend ?? "opaque";
1698
1716
  const space = def.space ?? "okhsl";
1699
- const baseLinear = okhslVariantToLinearRgb(baseVariant);
1700
- const targetLinear = okhslVariantToLinearRgb(targetVariant);
1717
+ const baseLinear = okhslVariantToLinearRgb(baseVariant, ctx.config.pastel);
1718
+ const targetLinear = okhslVariantToLinearRgb(targetVariant, ctx.config.pastel);
1701
1719
  if (def.contrast !== void 0) {
1702
1720
  const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast);
1703
1721
  const metric = resolvedContrast.metric;
1704
1722
  let luminanceAt;
1705
1723
  if (blend === "transparent" || space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
1706
1724
  else luminanceAt = (v) => {
1707
- return metricLuminance(metric, okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1725
+ return metricLuminance(metric, okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v, ctx.config.pastel));
1708
1726
  };
1709
1727
  t = findValueForMixContrast({
1710
1728
  preferredValue: t,
@@ -1721,7 +1739,7 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1721
1739
  l: targetVariant.l,
1722
1740
  alpha: clamp(t, 0, 1)
1723
1741
  });
1724
- if (space === "srgb") return linearRgbToToneVariant(linearSrgbLerp(baseLinear, targetLinear, t));
1742
+ if (space === "srgb") return linearRgbToToneVariant(linearSrgbLerp(baseLinear, targetLinear, t), ctx.config.pastel);
1725
1743
  return toToneVariant({
1726
1744
  h: mixHue(baseVariant, targetVariant, t),
1727
1745
  s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
@@ -1873,9 +1891,9 @@ const formatters = {
1873
1891
  function fmt(value, decimals) {
1874
1892
  return parseFloat(value.toFixed(decimals)).toString();
1875
1893
  }
1876
- function formatVariant(v, format = "okhsl") {
1894
+ function formatVariant(v, format = "okhsl", pastel = false) {
1877
1895
  const { l } = variantToOkhsl(v);
1878
- const base = formatters[format](v.h, v.s * 100, l * 100);
1896
+ const base = formatters[format](v.h, v.s * 100, l * 100, pastel);
1879
1897
  if (v.alpha >= 1) return base;
1880
1898
  const closing = base.lastIndexOf(")");
1881
1899
  return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
@@ -1887,44 +1905,44 @@ function resolveModes(override) {
1887
1905
  highContrast: override?.highContrast ?? cfg.modes.highContrast
1888
1906
  };
1889
1907
  }
1890
- function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
1908
+ function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false) {
1891
1909
  const tokens = {};
1892
1910
  for (const [name, color] of resolved) {
1893
1911
  const key = `#${prefix}${name}`;
1894
- const entry = { "": formatVariant(color.light, format) };
1895
- if (modes.dark) entry[states.dark] = formatVariant(color.dark, format);
1896
- if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format);
1897
- if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format);
1912
+ const entry = { "": formatVariant(color.light, format, pastel) };
1913
+ if (modes.dark) entry[states.dark] = formatVariant(color.dark, format, pastel);
1914
+ if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format, pastel);
1915
+ if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format, pastel);
1898
1916
  tokens[key] = entry;
1899
1917
  }
1900
1918
  return tokens;
1901
1919
  }
1902
- function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl") {
1920
+ function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl", pastel = false) {
1903
1921
  const result = { light: {} };
1904
1922
  if (modes.dark) result.dark = {};
1905
1923
  if (modes.highContrast) result.lightContrast = {};
1906
1924
  if (modes.dark && modes.highContrast) result.darkContrast = {};
1907
1925
  for (const [name, color] of resolved) {
1908
1926
  const key = `${prefix}${name}`;
1909
- result.light[key] = formatVariant(color.light, format);
1910
- if (modes.dark) result.dark[key] = formatVariant(color.dark, format);
1911
- if (modes.highContrast) result.lightContrast[key] = formatVariant(color.lightContrast, format);
1912
- if (modes.dark && modes.highContrast) result.darkContrast[key] = formatVariant(color.darkContrast, format);
1927
+ result.light[key] = formatVariant(color.light, format, pastel);
1928
+ if (modes.dark) result.dark[key] = formatVariant(color.dark, format, pastel);
1929
+ if (modes.highContrast) result.lightContrast[key] = formatVariant(color.lightContrast, format, pastel);
1930
+ if (modes.dark && modes.highContrast) result.darkContrast[key] = formatVariant(color.darkContrast, format, pastel);
1913
1931
  }
1914
1932
  return result;
1915
1933
  }
1916
- function buildJsonMap(resolved, modes, format = "okhsl") {
1934
+ function buildJsonMap(resolved, modes, format = "okhsl", pastel = false) {
1917
1935
  const result = {};
1918
1936
  for (const [name, color] of resolved) {
1919
- const entry = { light: formatVariant(color.light, format) };
1920
- if (modes.dark) entry.dark = formatVariant(color.dark, format);
1921
- if (modes.highContrast) entry.lightContrast = formatVariant(color.lightContrast, format);
1922
- if (modes.dark && modes.highContrast) entry.darkContrast = formatVariant(color.darkContrast, format);
1937
+ const entry = { light: formatVariant(color.light, format, pastel) };
1938
+ if (modes.dark) entry.dark = formatVariant(color.dark, format, pastel);
1939
+ if (modes.highContrast) entry.lightContrast = formatVariant(color.lightContrast, format, pastel);
1940
+ if (modes.dark && modes.highContrast) entry.darkContrast = formatVariant(color.darkContrast, format, pastel);
1923
1941
  result[name] = entry;
1924
1942
  }
1925
1943
  return result;
1926
1944
  }
1927
- function buildCssMap(resolved, prefix, suffix, format) {
1945
+ function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
1928
1946
  const lines = {
1929
1947
  light: [],
1930
1948
  dark: [],
@@ -1933,10 +1951,10 @@ function buildCssMap(resolved, prefix, suffix, format) {
1933
1951
  };
1934
1952
  for (const [name, color] of resolved) {
1935
1953
  const prop = `--${prefix}${name}${suffix}`;
1936
- lines.light.push(`${prop}: ${formatVariant(color.light, format)};`);
1937
- lines.dark.push(`${prop}: ${formatVariant(color.dark, format)};`);
1938
- lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format)};`);
1939
- lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format)};`);
1954
+ lines.light.push(`${prop}: ${formatVariant(color.light, format, pastel)};`);
1955
+ lines.dark.push(`${prop}: ${formatVariant(color.dark, format, pastel)};`);
1956
+ lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format, pastel)};`);
1957
+ lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format, pastel)};`);
1940
1958
  }
1941
1959
  return {
1942
1960
  light: lines.light.join("\n"),
@@ -1988,7 +2006,6 @@ function buildValueFormConfigOverride(userOverride) {
1988
2006
  lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : false,
1989
2007
  darkTone: userOverride?.darkTone !== void 0 ? userOverride.darkTone : cfg.darkTone,
1990
2008
  darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
1991
- saturationTaper: userOverride?.saturationTaper ?? cfg.saturationTaper,
1992
2009
  autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
1993
2010
  shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
1994
2011
  };
@@ -2005,7 +2022,6 @@ function buildStructuredConfigOverride(userOverride) {
2005
2022
  lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : cfg.lightTone,
2006
2023
  darkTone: userOverride?.darkTone !== void 0 ? userOverride.darkTone : cfg.darkTone,
2007
2024
  darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
2008
- saturationTaper: userOverride?.saturationTaper ?? cfg.saturationTaper,
2009
2025
  autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
2010
2026
  shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
2011
2027
  };
@@ -2321,7 +2337,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
2321
2337
  };
2322
2338
  };
2323
2339
  const tokenLike = (options) => {
2324
- return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
2340
+ return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format, effectiveConfig.pastel)[`#${primary}`];
2325
2341
  };
2326
2342
  return {
2327
2343
  resolve() {
@@ -2330,10 +2346,10 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
2330
2346
  token: tokenLike,
2331
2347
  tasty: tokenLike,
2332
2348
  json(options) {
2333
- return buildJsonMap(resolveOnce(), resolveModes(options?.modes), options?.format)[primary];
2349
+ return buildJsonMap(resolveOnce(), resolveModes(options?.modes), options?.format, effectiveConfig.pastel)[primary];
2334
2350
  },
2335
2351
  css(options) {
2336
- return buildCssMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb");
2352
+ return buildCssMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb", effectiveConfig.pastel);
2337
2353
  },
2338
2354
  export: exportData
2339
2355
  };
@@ -2573,9 +2589,10 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
2573
2589
  const seen = /* @__PURE__ */ new Map();
2574
2590
  for (const [themeName, theme] of Object.entries(themes)) {
2575
2591
  const resolved = theme.resolve();
2592
+ const pastel = theme.getConfig().pastel;
2576
2593
  const prefix = resolvePrefix(options, themeName, true);
2577
- merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix));
2578
- if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), ""));
2594
+ merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix, pastel));
2595
+ if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), "", pastel));
2579
2596
  }
2580
2597
  return acc;
2581
2598
  }
@@ -2584,7 +2601,7 @@ function createPalette(themes, paletteOptions) {
2584
2601
  return {
2585
2602
  tokens(options) {
2586
2603
  const modes = resolveModes(options?.modes);
2587
- return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildFlatTokenMap(filtered, prefix, modes, options?.format), (acc, part) => {
2604
+ return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildFlatTokenMap(filtered, prefix, modes, options?.format, pastel), (acc, part) => {
2588
2605
  for (const variant of Object.keys(part)) {
2589
2606
  if (!acc[variant]) acc[variant] = {};
2590
2607
  Object.assign(acc[variant], part[variant]);
@@ -2598,18 +2615,18 @@ function createPalette(themes, paletteOptions) {
2598
2615
  highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2599
2616
  };
2600
2617
  const modes = resolveModes(options?.modes);
2601
- return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildTokenMap(filtered, prefix, states, modes, options?.format), (acc, part) => Object.assign(acc, part), () => ({}));
2618
+ return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildTokenMap(filtered, prefix, states, modes, options?.format, pastel), (acc, part) => Object.assign(acc, part), () => ({}));
2602
2619
  },
2603
2620
  json(options) {
2604
2621
  const modes = resolveModes(options?.modes);
2605
2622
  const result = {};
2606
- for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
2623
+ for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format, theme.getConfig().pastel);
2607
2624
  return result;
2608
2625
  },
2609
2626
  css(options) {
2610
2627
  const suffix = options?.suffix ?? "-color";
2611
2628
  const format = options?.format ?? "rgb";
2612
- const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildCssMap(filtered, prefix, suffix, format), (acc, part) => {
2629
+ const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildCssMap(filtered, prefix, suffix, format, pastel), (acc, part) => {
2613
2630
  for (const key of [
2614
2631
  "light",
2615
2632
  "dark",
@@ -2676,6 +2693,9 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2676
2693
  get saturation() {
2677
2694
  return saturation;
2678
2695
  },
2696
+ getConfig() {
2697
+ return getEffectiveConfig();
2698
+ },
2679
2699
  colors(defs) {
2680
2700
  colorDefs = {
2681
2701
  ...colorDefs,
@@ -2730,7 +2750,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2730
2750
  },
2731
2751
  tokens(options) {
2732
2752
  const modes = resolveModes(options?.modes);
2733
- return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
2753
+ return buildFlatTokenMap(resolveCached(), "", modes, options?.format, getEffectiveConfig().pastel);
2734
2754
  },
2735
2755
  tasty(options) {
2736
2756
  const cfg = getEffectiveConfig();
@@ -2739,14 +2759,14 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2739
2759
  highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2740
2760
  };
2741
2761
  const modes = resolveModes(options?.modes);
2742
- return buildTokenMap(resolveCached(), "", states, modes, options?.format);
2762
+ return buildTokenMap(resolveCached(), "", states, modes, options?.format, cfg.pastel);
2743
2763
  },
2744
2764
  json(options) {
2745
2765
  const modes = resolveModes(options?.modes);
2746
- return buildJsonMap(resolveCached(), modes, options?.format);
2766
+ return buildJsonMap(resolveCached(), modes, options?.format, getEffectiveConfig().pastel);
2747
2767
  },
2748
2768
  css(options) {
2749
- return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb");
2769
+ return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb", getEffectiveConfig().pastel);
2750
2770
  }
2751
2771
  };
2752
2772
  }
@@ -2769,7 +2789,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2769
2789
  * Create a single-hue glaze theme.
2770
2790
  *
2771
2791
  * An optional `config` override can be supplied to customize the resolve
2772
- * behavior for this theme (tone windows, saturation taper, etc.). The
2792
+ * behavior for this theme (tone windows, etc.). The
2773
2793
  * override is **merged over the live global config at resolve time** —
2774
2794
  * the theme still reacts to later `configure()` calls for fields it
2775
2795
  * didn't override.
@@ -2829,7 +2849,7 @@ glaze.from = function from(data) {
2829
2849
  *
2830
2850
  * // Config override on any form
2831
2851
  * glaze.color('#26fcb2', { darkTone: false, autoFlip: false })
2832
- * glaze.color({ from: '#fff', base: bg }, { saturationTaper: 0 })
2852
+ * glaze.color({ from: '#fff', base: bg })
2833
2853
  * ```
2834
2854
  *
2835
2855
  * Defaults: every form defaults to `mode: 'auto'`. Value-shorthand forms
@@ -2883,8 +2903,8 @@ glaze.shadow = function shadow(input) {
2883
2903
  };
2884
2904
  };
2885
2905
  /** Format a resolved color variant as a CSS string. */
2886
- glaze.format = function format(variant, colorFormat) {
2887
- return formatVariant(variant, colorFormat);
2906
+ glaze.format = function format(variant, colorFormat, pastel) {
2907
+ return formatVariant(variant, colorFormat, pastel);
2888
2908
  };
2889
2909
  /**
2890
2910
  * Create a theme from a hex color string.
@@ -2941,6 +2961,7 @@ glaze.resetConfig = function resetConfig$1() {
2941
2961
  exports.REF_EPS = REF_EPS;
2942
2962
  exports.apcaContrast = apcaContrast;
2943
2963
  exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
2964
+ exports.cuspLightness = cuspLightness;
2944
2965
  exports.findToneForContrast = findToneForContrast;
2945
2966
  exports.findValueForMixContrast = findValueForMixContrast;
2946
2967
  exports.formatHsl = formatHsl;