@tenphi/glaze 0.13.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 +13 -11
- package/dist/index.cjs +913 -567
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +296 -172
- package/dist/index.d.mts +296 -172
- package/dist/index.mjs +902 -567
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +197 -136
- package/docs/methodology.md +64 -54
- package/docs/migration.md +81 -10
- package/docs/okhst.md +216 -0
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -96,7 +96,12 @@ const K2 = .03;
|
|
|
96
96
|
const K3 = (1 + K1) / (1 + K2);
|
|
97
97
|
const EPSILON = 1e-10;
|
|
98
98
|
const constrainAngle = (angle) => (angle % 360 + 360) % 360;
|
|
99
|
+
/**
|
|
100
|
+
* OKHSL toe function: maps OKLab lightness L to perceptual lightness l.
|
|
101
|
+
* Exported for the OKHST tone transfers in `okhst.ts`.
|
|
102
|
+
*/
|
|
99
103
|
const toe = (x) => .5 * (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x));
|
|
104
|
+
/** Inverse OKHSL toe: maps perceptual lightness l back to OKLab lightness L. */
|
|
100
105
|
const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2));
|
|
101
106
|
const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
102
107
|
const dotXY = (a, b) => a[0] * b[0] + a[1] * b[1];
|
|
@@ -251,10 +256,48 @@ const getCs = (L, a, b, cusp) => {
|
|
|
251
256
|
cMax
|
|
252
257
|
];
|
|
253
258
|
};
|
|
259
|
+
const CYAN_A = Math.cos(199.8 * Math.PI / 180);
|
|
260
|
+
const CYAN_B = Math.sin(199.8 * Math.PI / 180);
|
|
261
|
+
const BLUE_A = Math.cos(267.4 * Math.PI / 180);
|
|
262
|
+
const BLUE_B = Math.sin(267.4 * Math.PI / 180);
|
|
263
|
+
let cyanCusp;
|
|
264
|
+
let blueCusp;
|
|
265
|
+
/**
|
|
266
|
+
* Computes the maximum safe OKLCH chroma that fits inside the sRGB gamut
|
|
267
|
+
* for all possible hues at a given OKLab lightness `L`.
|
|
268
|
+
*/
|
|
269
|
+
function computeSafeChromaOKLCH(L) {
|
|
270
|
+
if (!cyanCusp) cyanCusp = findCuspOKLCH(CYAN_A, CYAN_B);
|
|
271
|
+
if (!blueCusp) blueCusp = findCuspOKLCH(BLUE_A, BLUE_B);
|
|
272
|
+
const c1 = findGamutIntersectionOKLCH(CYAN_A, CYAN_B, L, 1, L, cyanCusp);
|
|
273
|
+
const c2 = findGamutIntersectionOKLCH(BLUE_A, BLUE_B, L, 1, L, blueCusp);
|
|
274
|
+
return Math.min(c1, c2);
|
|
275
|
+
}
|
|
276
|
+
/** Per-hue cusp-lightness cache. The cusp is mode-independent, so keying on
|
|
277
|
+
* a rounded hue is safe and keeps the cache small. */
|
|
278
|
+
const cuspLightnessCache = /* @__PURE__ */ new Map();
|
|
279
|
+
/**
|
|
280
|
+
* OKHSL lightness of the gamut cusp for a hue — the lightness where the
|
|
281
|
+
* realizable chroma peaks. Reuses the same `find_cusp` OKHSL already runs for
|
|
282
|
+
* its `s` normalization (no new color math); the OKLab cusp lightness is run
|
|
283
|
+
* through the OKHSL `toe` and clamped to `[0.001, 0.999]` so divisions that
|
|
284
|
+
* key off it stay safe. Cached per (rounded) hue.
|
|
285
|
+
*
|
|
286
|
+
* @param h Hue, 0–360.
|
|
287
|
+
*/
|
|
288
|
+
function cuspLightness(h) {
|
|
289
|
+
const key = Math.round(constrainAngle(h) * 100) / 100;
|
|
290
|
+
const cached = cuspLightnessCache.get(key);
|
|
291
|
+
if (cached !== void 0) return cached;
|
|
292
|
+
const hNorm = key / 360;
|
|
293
|
+
const lc = clampVal(toe(findCuspOKLCH(Math.cos(TAU * hNorm), Math.sin(TAU * hNorm))[0]), .001, .999);
|
|
294
|
+
cuspLightnessCache.set(key, lc);
|
|
295
|
+
return lc;
|
|
296
|
+
}
|
|
254
297
|
/**
|
|
255
298
|
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
|
|
256
299
|
*/
|
|
257
|
-
function okhslToOklab(h, s, l) {
|
|
300
|
+
function okhslToOklab(h, s, l, pastel = false) {
|
|
258
301
|
const L = toeInv(l);
|
|
259
302
|
let a = 0;
|
|
260
303
|
let b = 0;
|
|
@@ -262,24 +305,30 @@ function okhslToOklab(h, s, l) {
|
|
|
262
305
|
if (L !== 0 && L !== 1 && s !== 0) {
|
|
263
306
|
const a_ = Math.cos(TAU * hNorm);
|
|
264
307
|
const b_ = Math.sin(TAU * hNorm);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (s < mid) {
|
|
270
|
-
t = midInv * s;
|
|
271
|
-
k0 = 0;
|
|
272
|
-
k1 = mid * c0;
|
|
273
|
-
k2 = 1 - k1 / cMid;
|
|
308
|
+
if (pastel) {
|
|
309
|
+
const c = s * computeSafeChromaOKLCH(L);
|
|
310
|
+
a = c * a_;
|
|
311
|
+
b = c * b_;
|
|
274
312
|
} else {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
313
|
+
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
314
|
+
const mid = .8;
|
|
315
|
+
const midInv = 1.25;
|
|
316
|
+
let t, k0, k1, k2;
|
|
317
|
+
if (s < mid) {
|
|
318
|
+
t = midInv * s;
|
|
319
|
+
k0 = 0;
|
|
320
|
+
k1 = mid * c0;
|
|
321
|
+
k2 = 1 - k1 / cMid;
|
|
322
|
+
} else {
|
|
323
|
+
t = 5 * (s - .8);
|
|
324
|
+
k0 = cMid;
|
|
325
|
+
k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
|
|
326
|
+
k2 = 1 - k1 / (cMax - cMid);
|
|
327
|
+
}
|
|
328
|
+
const c = k0 + t * k1 / (1 - k2 * t);
|
|
329
|
+
a = c * a_;
|
|
330
|
+
b = c * b_;
|
|
279
331
|
}
|
|
280
|
-
const c = k0 + t * k1 / (1 - k2 * t);
|
|
281
|
-
a = c * a_;
|
|
282
|
-
b = c * b_;
|
|
283
332
|
}
|
|
284
333
|
return [
|
|
285
334
|
L,
|
|
@@ -291,8 +340,8 @@ function okhslToOklab(h, s, l) {
|
|
|
291
340
|
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
|
|
292
341
|
* Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
|
|
293
342
|
*/
|
|
294
|
-
function okhslToLinearSrgb(h, s, l) {
|
|
295
|
-
return OKLabToLinearSRGB(okhslToOklab(h, s, l));
|
|
343
|
+
function okhslToLinearSrgb(h, s, l, pastel = false) {
|
|
344
|
+
return OKLabToLinearSRGB(okhslToOklab(h, s, l, pastel));
|
|
296
345
|
}
|
|
297
346
|
/**
|
|
298
347
|
* Compute relative luminance Y from linear sRGB channels.
|
|
@@ -322,8 +371,8 @@ const sRGBGammaToLinear = (val) => {
|
|
|
322
371
|
/**
|
|
323
372
|
* Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
|
|
324
373
|
*/
|
|
325
|
-
function okhslToSrgb(h, s, l) {
|
|
326
|
-
const lin = okhslToLinearSrgb(h, s, l);
|
|
374
|
+
function okhslToSrgb(h, s, l, pastel = false) {
|
|
375
|
+
const lin = okhslToLinearSrgb(h, s, l, pastel);
|
|
327
376
|
return [
|
|
328
377
|
Math.max(0, Math.min(1, sRGBLinearToGamma(lin[0]))),
|
|
329
378
|
Math.max(0, Math.min(1, sRGBLinearToGamma(lin[1]))),
|
|
@@ -341,6 +390,22 @@ function gamutClampedLuminance(linearRgb) {
|
|
|
341
390
|
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
342
391
|
return .2126 * r + .7152 * g + .0722 * b;
|
|
343
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Compute APCA screen luminance (`Ys`) from linear sRGB.
|
|
395
|
+
*
|
|
396
|
+
* APCA does not use the WCAG piecewise sRGB EOTF; it defines its own
|
|
397
|
+
* luminance as `0.2126·R^2.4 + 0.7152·G^2.4 + 0.0722·B^2.4` over the
|
|
398
|
+
* gamma-encoded (display) channels with a simple 2.4 exponent. The APCA
|
|
399
|
+
* soft-clamp threshold in `apcaContrast` is calibrated against this basis,
|
|
400
|
+
* so the solver must feed it `Ys`, not WCAG relative luminance. Channels
|
|
401
|
+
* are gamut-clamped to [0, 1] first, matching `gamutClampedLuminance`.
|
|
402
|
+
*/
|
|
403
|
+
function apcaLuminanceFromLinearRgb(linearRgb) {
|
|
404
|
+
const r = Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0])));
|
|
405
|
+
const g = Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1])));
|
|
406
|
+
const b = Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2])));
|
|
407
|
+
return .2126 * Math.pow(r, 2.4) + .7152 * Math.pow(g, 2.4) + .0722 * Math.pow(b, 2.4);
|
|
408
|
+
}
|
|
344
409
|
const linearSrgbToOklab = (rgb) => {
|
|
345
410
|
return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
|
|
346
411
|
};
|
|
@@ -349,7 +414,7 @@ const linearSrgbToOklab = (rgb) => {
|
|
|
349
414
|
* Input: [L, a, b] where L: 0–1, a/b: roughly -0.5 to 0.5.
|
|
350
415
|
* Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
|
|
351
416
|
*/
|
|
352
|
-
const oklabToOkhsl = (lab) => {
|
|
417
|
+
const oklabToOkhsl = (lab, pastel = false) => {
|
|
353
418
|
const L = lab[0];
|
|
354
419
|
const a = lab[1];
|
|
355
420
|
const b = lab[2];
|
|
@@ -369,19 +434,22 @@ const oklabToOkhsl = (lab) => {
|
|
|
369
434
|
const b_ = b / C;
|
|
370
435
|
let h = Math.atan2(b, a) * (180 / Math.PI);
|
|
371
436
|
h = constrainAngle(h);
|
|
372
|
-
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
373
|
-
const mid = .8;
|
|
374
|
-
const midInv = 1.25;
|
|
375
437
|
let s;
|
|
376
|
-
if (C
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
438
|
+
if (pastel) s = C / computeSafeChromaOKLCH(L);
|
|
439
|
+
else {
|
|
440
|
+
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
441
|
+
const mid = .8;
|
|
442
|
+
const midInv = 1.25;
|
|
443
|
+
if (C < cMid) {
|
|
444
|
+
const k1 = mid * c0;
|
|
445
|
+
s = C / (k1 + C * (1 - k1 / cMid)) / midInv;
|
|
446
|
+
} else {
|
|
447
|
+
const k0 = cMid;
|
|
448
|
+
const k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
|
|
449
|
+
const k2 = 1 - k1 / (cMax - cMid);
|
|
450
|
+
const cDiff = C - k0;
|
|
451
|
+
s = mid + cDiff / (k1 + cDiff * k2) / 5;
|
|
452
|
+
}
|
|
385
453
|
}
|
|
386
454
|
const l = toe(L);
|
|
387
455
|
return [
|
|
@@ -394,12 +462,12 @@ const oklabToOkhsl = (lab) => {
|
|
|
394
462
|
* Convert gamma-encoded sRGB (0–1 per channel) to OKHSL.
|
|
395
463
|
* Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
|
|
396
464
|
*/
|
|
397
|
-
function srgbToOkhsl(rgb) {
|
|
465
|
+
function srgbToOkhsl(rgb, pastel = false) {
|
|
398
466
|
return oklabToOkhsl(linearSrgbToOklab([
|
|
399
467
|
sRGBGammaToLinear(rgb[0]),
|
|
400
468
|
sRGBGammaToLinear(rgb[1]),
|
|
401
469
|
sRGBGammaToLinear(rgb[2])
|
|
402
|
-
]));
|
|
470
|
+
]), pastel);
|
|
403
471
|
}
|
|
404
472
|
/**
|
|
405
473
|
* Convert CSS HSL (sRGB-based) to gamma-encoded sRGB [r, g, b] in 0–1 range.
|
|
@@ -514,24 +582,26 @@ function fmt$1(value, decimals) {
|
|
|
514
582
|
* Format OKHSL values as a CSS `okhsl(H S% L%)` string.
|
|
515
583
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
516
584
|
*/
|
|
517
|
-
function formatOkhsl(h, s, l) {
|
|
518
|
-
|
|
585
|
+
function formatOkhsl(h, s, l, pastel = false) {
|
|
586
|
+
let outS = s;
|
|
587
|
+
if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s / 100, l / 100, true), false)[1] * 100;
|
|
588
|
+
return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(l, 2)}%)`;
|
|
519
589
|
}
|
|
520
590
|
/**
|
|
521
591
|
* Format OKHSL values as a CSS `rgb(R G B)` string.
|
|
522
592
|
* Uses 2 decimal places to avoid 8-bit quantization contrast loss.
|
|
523
593
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
524
594
|
*/
|
|
525
|
-
function formatRgb(h, s, l) {
|
|
526
|
-
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
|
|
595
|
+
function formatRgb(h, s, l, pastel = false) {
|
|
596
|
+
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100, pastel);
|
|
527
597
|
return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
|
|
528
598
|
}
|
|
529
599
|
/**
|
|
530
600
|
* Format OKHSL values as a CSS `hsl(H S% L%)` string.
|
|
531
601
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
532
602
|
*/
|
|
533
|
-
function formatHsl(h, s, l) {
|
|
534
|
-
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
|
|
603
|
+
function formatHsl(h, s, l, pastel = false) {
|
|
604
|
+
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100, pastel);
|
|
535
605
|
const max = Math.max(r, g, b);
|
|
536
606
|
const min = Math.min(r, g, b);
|
|
537
607
|
const delta = max - min;
|
|
@@ -550,8 +620,8 @@ function formatHsl(h, s, l) {
|
|
|
550
620
|
* Format OKHSL values as a CSS `oklch(L C H)` string.
|
|
551
621
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
552
622
|
*/
|
|
553
|
-
function formatOklch(h, s, l) {
|
|
554
|
-
const [L, a, b] = okhslToOklab(h, s / 100, l / 100);
|
|
623
|
+
function formatOklch(h, s, l, pastel = false) {
|
|
624
|
+
const [L, a, b] = okhslToOklab(h, s / 100, l / 100, pastel);
|
|
555
625
|
const C = Math.sqrt(a * a + b * b);
|
|
556
626
|
let hh = Math.atan2(b, a) * (180 / Math.PI);
|
|
557
627
|
hh = constrainAngle(hh);
|
|
@@ -566,10 +636,17 @@ function formatOklch(h, s, l) {
|
|
|
566
636
|
*/
|
|
567
637
|
function defaultConfig() {
|
|
568
638
|
return {
|
|
569
|
-
|
|
570
|
-
|
|
639
|
+
lightTone: {
|
|
640
|
+
lo: 10,
|
|
641
|
+
hi: 100,
|
|
642
|
+
eps: .05
|
|
643
|
+
},
|
|
644
|
+
darkTone: {
|
|
645
|
+
lo: 15,
|
|
646
|
+
hi: 95,
|
|
647
|
+
eps: .05
|
|
648
|
+
},
|
|
571
649
|
darkDesaturation: .1,
|
|
572
|
-
darkCurve: .5,
|
|
573
650
|
states: {
|
|
574
651
|
dark: "@dark",
|
|
575
652
|
highContrast: "@high-contrast"
|
|
@@ -578,7 +655,8 @@ function defaultConfig() {
|
|
|
578
655
|
dark: true,
|
|
579
656
|
highContrast: false
|
|
580
657
|
},
|
|
581
|
-
autoFlip: true
|
|
658
|
+
autoFlip: true,
|
|
659
|
+
pastel: false
|
|
582
660
|
};
|
|
583
661
|
}
|
|
584
662
|
let globalConfig = defaultConfig();
|
|
@@ -605,10 +683,9 @@ function snapshotConfig() {
|
|
|
605
683
|
function configure(config) {
|
|
606
684
|
configVersion++;
|
|
607
685
|
globalConfig = {
|
|
608
|
-
|
|
609
|
-
|
|
686
|
+
lightTone: config.lightTone ?? globalConfig.lightTone,
|
|
687
|
+
darkTone: config.darkTone ?? globalConfig.darkTone,
|
|
610
688
|
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
611
|
-
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
612
689
|
states: {
|
|
613
690
|
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
614
691
|
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -618,7 +695,8 @@ function configure(config) {
|
|
|
618
695
|
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
619
696
|
},
|
|
620
697
|
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
|
|
621
|
-
autoFlip: config.autoFlip ?? globalConfig.autoFlip
|
|
698
|
+
autoFlip: config.autoFlip ?? globalConfig.autoFlip,
|
|
699
|
+
pastel: config.pastel ?? globalConfig.pastel
|
|
622
700
|
};
|
|
623
701
|
}
|
|
624
702
|
function resetConfig() {
|
|
@@ -628,20 +706,20 @@ function resetConfig() {
|
|
|
628
706
|
/**
|
|
629
707
|
* Merge a per-instance config override over a base resolved config.
|
|
630
708
|
* Only fields present in `override` are replaced; others fall through
|
|
631
|
-
* from `base`. `false` for
|
|
632
|
-
* (treated as
|
|
709
|
+
* from `base`. `false` for tone windows passes through as-is
|
|
710
|
+
* (treated as the full range by `activeWindow()` in okhst.ts).
|
|
633
711
|
*/
|
|
634
712
|
function mergeConfig(base, override) {
|
|
635
713
|
if (!override) return base;
|
|
636
714
|
return {
|
|
637
|
-
|
|
638
|
-
|
|
715
|
+
lightTone: override.lightTone !== void 0 ? override.lightTone : base.lightTone,
|
|
716
|
+
darkTone: override.darkTone !== void 0 ? override.darkTone : base.darkTone,
|
|
639
717
|
darkDesaturation: override.darkDesaturation ?? base.darkDesaturation,
|
|
640
|
-
darkCurve: override.darkCurve ?? base.darkCurve,
|
|
641
718
|
states: base.states,
|
|
642
719
|
modes: base.modes,
|
|
643
720
|
shadowTuning: override.shadowTuning ?? base.shadowTuning,
|
|
644
|
-
autoFlip: override.autoFlip ?? base.autoFlip
|
|
721
|
+
autoFlip: override.autoFlip ?? base.autoFlip,
|
|
722
|
+
pastel: override.pastel ?? base.pastel
|
|
645
723
|
};
|
|
646
724
|
}
|
|
647
725
|
|
|
@@ -656,6 +734,10 @@ function pairHC(p) {
|
|
|
656
734
|
function clamp(v, min, max) {
|
|
657
735
|
return Math.max(min, Math.min(max, v));
|
|
658
736
|
}
|
|
737
|
+
/** Whether a tone value is an extreme keyword (`'max'` / `'min'`). */
|
|
738
|
+
function isExtremeTone(value) {
|
|
739
|
+
return value === "max" || value === "min";
|
|
740
|
+
}
|
|
659
741
|
/**
|
|
660
742
|
* Parse a value that can be absolute (number) or relative (signed string).
|
|
661
743
|
* Returns the numeric value and whether it's relative.
|
|
@@ -671,6 +753,31 @@ function parseRelativeOrAbsolute(value) {
|
|
|
671
753
|
};
|
|
672
754
|
}
|
|
673
755
|
/**
|
|
756
|
+
* Parse a tone value into a normalized shape.
|
|
757
|
+
* - `'max'` / `'min'` → `{ kind: 'extreme', value: 100 | 0 }` (an absolute
|
|
758
|
+
* author tone before scheme mapping — `'max'` is 100, `'min'` is 0).
|
|
759
|
+
* - `'+N'` / `'-N'` → `{ kind: 'relative', value: ±N }`.
|
|
760
|
+
* - number → `{ kind: 'absolute', value }`.
|
|
761
|
+
*/
|
|
762
|
+
function parseToneValue(value) {
|
|
763
|
+
if (value === "max") return {
|
|
764
|
+
kind: "extreme",
|
|
765
|
+
value: 100
|
|
766
|
+
};
|
|
767
|
+
if (value === "min") return {
|
|
768
|
+
kind: "extreme",
|
|
769
|
+
value: 0
|
|
770
|
+
};
|
|
771
|
+
if (typeof value === "number") return {
|
|
772
|
+
kind: "absolute",
|
|
773
|
+
value
|
|
774
|
+
};
|
|
775
|
+
return {
|
|
776
|
+
kind: "relative",
|
|
777
|
+
value: parseFloat(value)
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
674
781
|
* Compute the effective hue for a color, given the theme seed hue
|
|
675
782
|
* and an optional per-color hue override.
|
|
676
783
|
*/
|
|
@@ -681,23 +788,205 @@ function resolveEffectiveHue(seedHue, defHue) {
|
|
|
681
788
|
return (parsed.value % 360 + 360) % 360;
|
|
682
789
|
}
|
|
683
790
|
/**
|
|
684
|
-
* Check whether a
|
|
685
|
-
* (i.e. a number, not a relative string).
|
|
791
|
+
* Check whether a tone value represents an absolute root definition
|
|
792
|
+
* (i.e. a number, not a relative string). Extreme keywords (`'max'` /
|
|
793
|
+
* `'min'`) also count — they need no base.
|
|
686
794
|
*/
|
|
687
|
-
function
|
|
688
|
-
if (
|
|
689
|
-
|
|
795
|
+
function isAbsoluteTone(tone) {
|
|
796
|
+
if (tone === void 0) return false;
|
|
797
|
+
const normal = Array.isArray(tone) ? tone[0] : tone;
|
|
798
|
+
return typeof normal === "number" || isExtremeTone(normal);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/okhst.ts
|
|
803
|
+
/**
|
|
804
|
+
* OKHST — the contrast-uniform tone space.
|
|
805
|
+
*
|
|
806
|
+
* OKHST is OKHSL with its lightness axis replaced by a contrast-uniform
|
|
807
|
+
* "tone" axis. It shares `h` / `s` with OKHSL verbatim and swaps `l` for
|
|
808
|
+
* `t`. This module owns:
|
|
809
|
+
*
|
|
810
|
+
* - the closed-form tone transfers (`toTone` / `fromTone`) at a fixed
|
|
811
|
+
* reference eps, plus the gray luminance helpers (`lToY` / `yToL`),
|
|
812
|
+
* - the `{ h, s, t }` <-> `{ h, s, l }` color-space converters,
|
|
813
|
+
* - the resolved-variant edge adapter (`variantToOkhsl`),
|
|
814
|
+
* - the per-scheme tone mapping that replaced the Möbius dark curve
|
|
815
|
+
* (`mapToneForScheme`), the dark desaturation reducer, and the solver's scheme
|
|
816
|
+
* tone range.
|
|
817
|
+
*
|
|
818
|
+
* See `docs/okhst.md` for the full specification and the calibrated
|
|
819
|
+
* default constants.
|
|
820
|
+
*/
|
|
821
|
+
/**
|
|
822
|
+
* Reference eps for the OKHST color space. WCAG 2 contrast is
|
|
823
|
+
* `(Y_hi + 0.05) / (Y_lo + 0.05)`, so an eps of `0.05` makes equal tone
|
|
824
|
+
* steps yield equal WCAG contrast. This is the canonical eps used by
|
|
825
|
+
* `okhst()` input, `{ h, s, t }` input, stored `ResolvedColorVariant.t`,
|
|
826
|
+
* relative `tone` offsets, and the contrast solver.
|
|
827
|
+
*/
|
|
828
|
+
const REF_EPS = .05;
|
|
829
|
+
/**
|
|
830
|
+
* Gray luminance from OKHSL lightness. For an achromatic color the OKLab
|
|
831
|
+
* lightness is `toeInv(l)` and luminance is its cube.
|
|
832
|
+
*/
|
|
833
|
+
function lToY(l) {
|
|
834
|
+
const L = toeInv(l);
|
|
835
|
+
return L * L * L;
|
|
836
|
+
}
|
|
837
|
+
/** OKHSL lightness from gray luminance — exact inverse of {@link lToY}. */
|
|
838
|
+
function yToL(y) {
|
|
839
|
+
return toe(Math.cbrt(Math.max(0, y)));
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Map a luminance `Y` (0–1) to tone (0–100) at the given eps.
|
|
843
|
+
* `toneFromY(0) === 0` and `toneFromY(1) === 100` for any eps.
|
|
844
|
+
*/
|
|
845
|
+
function toneFromY(y, eps = REF_EPS) {
|
|
846
|
+
return (Math.log(y + eps) - Math.log(eps)) / (Math.log(1 + eps) - Math.log(eps)) * 100;
|
|
847
|
+
}
|
|
848
|
+
/** Map a tone (0–100) back to luminance (0–1). Inverse of {@link toneFromY}. */
|
|
849
|
+
function yFromTone(t, eps = REF_EPS) {
|
|
850
|
+
const den = Math.log(1 + eps) - Math.log(eps);
|
|
851
|
+
return Math.exp(t / 100 * den + Math.log(eps)) - eps;
|
|
852
|
+
}
|
|
853
|
+
/** OKHSL lightness (0–1) -> tone (0–100). */
|
|
854
|
+
function toTone(l, eps = REF_EPS) {
|
|
855
|
+
return toneFromY(lToY(l), eps);
|
|
856
|
+
}
|
|
857
|
+
/** Tone (0–100) -> OKHSL lightness (0–1). Inverse of {@link toTone}. */
|
|
858
|
+
function fromTone(t, eps = REF_EPS) {
|
|
859
|
+
return yToL(yFromTone(t, eps));
|
|
860
|
+
}
|
|
861
|
+
/** Convert OKHST `{ h, s, t }` (t in 0–1) to OKHSL `{ h, s, l }`. */
|
|
862
|
+
function okhstToOkhsl(c) {
|
|
863
|
+
return {
|
|
864
|
+
h: c.h,
|
|
865
|
+
s: c.s,
|
|
866
|
+
l: clamp(fromTone(c.t * 100), 0, 1)
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/** Convert OKHSL `{ h, s, l }` to OKHST `{ h, s, t }` (t in 0–1). */
|
|
870
|
+
function okhslToOkhst(c) {
|
|
871
|
+
return {
|
|
872
|
+
h: c.h,
|
|
873
|
+
s: c.s,
|
|
874
|
+
t: clamp(toTone(c.l) / 100, 0, 1)
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Edge adapter: a resolved variant stores canonical tone `t` (0–1). Convert
|
|
879
|
+
* it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect.
|
|
880
|
+
*/
|
|
881
|
+
function variantToOkhsl(v) {
|
|
882
|
+
return {
|
|
883
|
+
h: v.h,
|
|
884
|
+
s: v.s,
|
|
885
|
+
l: clamp(fromTone(v.t * 100), 0, 1)
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Normalize any {@link ToneWindow} form to `{ lo, hi, eps }`.
|
|
890
|
+
* - `false`: full range `[0, 100]` at the reference eps (boundaries removed,
|
|
891
|
+
* curve preserved).
|
|
892
|
+
* - `[lo, hi]`: endpoints at the reference eps (the common form).
|
|
893
|
+
* - `{ lo, hi, eps }`: passed through (advanced eps tuning).
|
|
894
|
+
*/
|
|
895
|
+
function normalizeToneWindow(win) {
|
|
896
|
+
if (win === false) return {
|
|
897
|
+
lo: 0,
|
|
898
|
+
hi: 100,
|
|
899
|
+
eps: REF_EPS
|
|
900
|
+
};
|
|
901
|
+
if (Array.isArray(win)) return {
|
|
902
|
+
lo: win[0],
|
|
903
|
+
hi: win[1],
|
|
904
|
+
eps: REF_EPS
|
|
905
|
+
};
|
|
906
|
+
return {
|
|
907
|
+
lo: win.lo,
|
|
908
|
+
hi: win.hi,
|
|
909
|
+
eps: win.eps
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Resolve the active tone window for a scheme as OKHSL-lightness endpoints.
|
|
914
|
+
* - HC variants always return the full range `[0, 100]` with the mode eps.
|
|
915
|
+
* - `false` (= "no clamping") is treated as `[0, 100]` with the reference eps.
|
|
916
|
+
*/
|
|
917
|
+
function activeWindow(isHighContrast, kind, config) {
|
|
918
|
+
const win = normalizeToneWindow(kind === "dark" ? config.darkTone : config.lightTone);
|
|
919
|
+
if (isHighContrast) return {
|
|
920
|
+
lo: 0,
|
|
921
|
+
hi: 100,
|
|
922
|
+
eps: win.eps
|
|
923
|
+
};
|
|
924
|
+
return win;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Remap an authored tone (0–100) into a scheme window and return the final
|
|
928
|
+
* OKHSL lightness (0–100). The window endpoints are OKHSL lightnesses; the
|
|
929
|
+
* author tone is positioned within the window's tone interval (using the
|
|
930
|
+
* window's render eps), then converted back to lightness.
|
|
931
|
+
*/
|
|
932
|
+
function remapToneToLightness(authorTone, win) {
|
|
933
|
+
const loT = toTone(win.lo / 100, win.eps);
|
|
934
|
+
const hiT = toTone(win.hi / 100, win.eps);
|
|
935
|
+
return clamp(fromTone(loT + authorTone / 100 * (hiT - loT), win.eps) * 100, 0, 100);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Map an authored tone for a scheme and return the canonical stored tone
|
|
939
|
+
* (0–100, reference eps).
|
|
940
|
+
*
|
|
941
|
+
* - `static`: identity — the same tone renders in every scheme.
|
|
942
|
+
* - `auto` + dark: invert (`100 - tone`) then remap into the dark window.
|
|
943
|
+
* - `auto`/`fixed` + light, or `fixed` + dark: remap, no inversion.
|
|
944
|
+
*
|
|
945
|
+
* The window remap uses the mode's render eps to land a final OKHSL
|
|
946
|
+
* lightness; that lightness is then re-expressed as canonical tone so
|
|
947
|
+
* relative offsets and contrast stay comparable across schemes.
|
|
948
|
+
*/
|
|
949
|
+
function mapToneForScheme(authorTone, mode, isDark, isHighContrast, config) {
|
|
950
|
+
if (mode === "static") return clamp(authorTone, 0, 100);
|
|
951
|
+
const win = activeWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
952
|
+
return clamp(toTone(remapToneToLightness(clamp(isDark && mode === "auto" ? 100 - authorTone : authorTone, 0, 100), win) / 100), 0, 100);
|
|
953
|
+
}
|
|
954
|
+
/** Dark-scheme desaturation reducer (unchanged from the legacy pipeline). */
|
|
955
|
+
function mapSaturationDark(s, mode, config) {
|
|
956
|
+
if (mode === "static") return s;
|
|
957
|
+
return s * (1 - config.darkDesaturation);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Tone search range (0–1) for the contrast solver in a given scheme.
|
|
961
|
+
* `static` searches the full range; otherwise the scheme window's tone
|
|
962
|
+
* endpoints (HC bypasses to full range).
|
|
963
|
+
*/
|
|
964
|
+
function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
965
|
+
if (mode === "static") return [0, 1];
|
|
966
|
+
const win = activeWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
967
|
+
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
690
968
|
}
|
|
691
969
|
|
|
692
970
|
//#endregion
|
|
693
971
|
//#region src/contrast-solver.ts
|
|
694
972
|
/**
|
|
695
|
-
*
|
|
973
|
+
* Contrast solver — operates in OKHST tone.
|
|
974
|
+
*
|
|
975
|
+
* Finds the tone closest to a preferred tone that satisfies a contrast
|
|
976
|
+
* floor (WCAG 2 ratio or APCA Lc) against a base color. Because tone is
|
|
977
|
+
* contrast-uniform, the WCAG branch gets a closed-form seed and the search
|
|
978
|
+
* converges quickly.
|
|
696
979
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
|
|
980
|
+
* Public API: `findToneForContrast`, `findValueForMixContrast`,
|
|
981
|
+
* `resolveMinContrast`, `resolveContrastForMode`, `apcaContrast`.
|
|
982
|
+
*/
|
|
983
|
+
/**
|
|
984
|
+
* Luminance of a linear-sRGB color in the basis the metric expects: WCAG
|
|
985
|
+
* relative luminance for `wcag`, APCA screen luminance (`Ys`) for `apca`.
|
|
700
986
|
*/
|
|
987
|
+
function metricLuminance(metric, linearRgb) {
|
|
988
|
+
return metric === "apca" ? apcaLuminanceFromLinearRgb(linearRgb) : gamutClampedLuminance(linearRgb);
|
|
989
|
+
}
|
|
701
990
|
const CONTRAST_PRESETS = {
|
|
702
991
|
AA: 4.5,
|
|
703
992
|
AAA: 7,
|
|
@@ -708,15 +997,75 @@ function resolveMinContrast(value) {
|
|
|
708
997
|
if (typeof value === "number") return Math.max(1, value);
|
|
709
998
|
return CONTRAST_PRESETS[value];
|
|
710
999
|
}
|
|
1000
|
+
function pickPair(p, isHighContrast) {
|
|
1001
|
+
return Array.isArray(p) ? isHighContrast ? p[1] : p[0] : p;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Resolve a `ContrastSpec` (already selected from any outer HC pair) for a
|
|
1005
|
+
* given mode into `{ metric, target }`. Handles the inner metric HC pair and
|
|
1006
|
+
* preset resolution.
|
|
1007
|
+
*/
|
|
1008
|
+
function resolveContrastForMode(spec, isHighContrast) {
|
|
1009
|
+
if (typeof spec === "number" || typeof spec === "string") return {
|
|
1010
|
+
metric: "wcag",
|
|
1011
|
+
target: resolveMinContrast(spec)
|
|
1012
|
+
};
|
|
1013
|
+
if ("apca" in spec) return {
|
|
1014
|
+
metric: "apca",
|
|
1015
|
+
target: Math.abs(pickPair(spec.apca, isHighContrast))
|
|
1016
|
+
};
|
|
1017
|
+
return {
|
|
1018
|
+
metric: "wcag",
|
|
1019
|
+
target: resolveMinContrast(pickPair(spec.wcag, isHighContrast))
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const APCA_EXPONENTS = {
|
|
1023
|
+
mainTRC: 2.4,
|
|
1024
|
+
normBG: .56,
|
|
1025
|
+
normTXT: .57,
|
|
1026
|
+
revTXT: .62,
|
|
1027
|
+
revBG: .65
|
|
1028
|
+
};
|
|
1029
|
+
const APCA_BLACK_THRESH = .022;
|
|
1030
|
+
const APCA_BLACK_CLIP = 1.414;
|
|
1031
|
+
const APCA_DELTA_Y_MIN = 5e-4;
|
|
1032
|
+
const APCA_SCALE = 1.14;
|
|
1033
|
+
const APCA_LO_OFFSET = .027;
|
|
1034
|
+
function apcaSoftClamp(y) {
|
|
1035
|
+
const yc = Math.max(0, y);
|
|
1036
|
+
if (yc >= APCA_BLACK_THRESH) return yc;
|
|
1037
|
+
return yc + Math.pow(APCA_BLACK_THRESH - yc, APCA_BLACK_CLIP);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* APCA lightness contrast (Lc), signed: positive for dark text on light bg,
|
|
1041
|
+
* negative for light text on dark bg. Inputs are screen luminances (0–1).
|
|
1042
|
+
*/
|
|
1043
|
+
function apcaContrast(yText, yBg) {
|
|
1044
|
+
const txt = apcaSoftClamp(yText);
|
|
1045
|
+
const bg = apcaSoftClamp(yBg);
|
|
1046
|
+
if (Math.abs(bg - txt) < APCA_DELTA_Y_MIN) return 0;
|
|
1047
|
+
let sapc;
|
|
1048
|
+
if (bg > txt) {
|
|
1049
|
+
sapc = (Math.pow(bg, APCA_EXPONENTS.normBG) - Math.pow(txt, APCA_EXPONENTS.normTXT)) * APCA_SCALE;
|
|
1050
|
+
return sapc < .1 ? 0 : (sapc - APCA_LO_OFFSET) * 100;
|
|
1051
|
+
}
|
|
1052
|
+
sapc = (Math.pow(bg, APCA_EXPONENTS.revBG) - Math.pow(txt, APCA_EXPONENTS.revTXT)) * APCA_SCALE;
|
|
1053
|
+
return sapc > -.1 ? 0 : (sapc + APCA_LO_OFFSET) * 100;
|
|
1054
|
+
}
|
|
711
1055
|
const CACHE_SIZE = 512;
|
|
712
1056
|
const luminanceCache = /* @__PURE__ */ new Map();
|
|
713
1057
|
const cacheOrder = [];
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1058
|
+
/**
|
|
1059
|
+
* Luminance of an OKHST color `(h, s, t)` with t in 0–1 (reference eps), in
|
|
1060
|
+
* the metric's luminance basis. The metric is part of the cache key because
|
|
1061
|
+
* WCAG and APCA derive different luminances from the same color.
|
|
1062
|
+
*/
|
|
1063
|
+
function cachedLuminance(metric, h, s, t, pastel) {
|
|
1064
|
+
const tRounded = Math.round(t * 1e4) / 1e4;
|
|
1065
|
+
const key = `${metric}|${h}|${s}|${tRounded}|${pastel}`;
|
|
717
1066
|
const cached = luminanceCache.get(key);
|
|
718
1067
|
if (cached !== void 0) return cached;
|
|
719
|
-
const y =
|
|
1068
|
+
const y = metricLuminance(metric, okhslToLinearSrgb(h, s, fromTone(tRounded * 100, REF_EPS), pastel));
|
|
720
1069
|
if (luminanceCache.size >= CACHE_SIZE) {
|
|
721
1070
|
const evict = cacheOrder.shift();
|
|
722
1071
|
luminanceCache.delete(evict);
|
|
@@ -726,263 +1075,189 @@ function cachedLuminance(h, s, l) {
|
|
|
726
1075
|
return y;
|
|
727
1076
|
}
|
|
728
1077
|
/**
|
|
729
|
-
*
|
|
1078
|
+
* Score a candidate luminance against the base for a metric. Returns a value
|
|
1079
|
+
* that is `>= target` exactly when the floor is met (WCAG ratio, or APCA Lc
|
|
1080
|
+
* magnitude).
|
|
730
1081
|
*/
|
|
731
|
-
function
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
1082
|
+
function metricScore(metric, yCandidate, yBase) {
|
|
1083
|
+
if (metric === "wcag") return contrastRatioFromLuminance(yCandidate, yBase);
|
|
1084
|
+
return Math.abs(apcaContrast(yCandidate, yBase));
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Binary search one branch `[lo, hi]` for the position nearest to `anchor`
|
|
1088
|
+
* that meets `target`. The domain is whatever `lum` interprets (tone 0–1 or
|
|
1089
|
+
* mix parameter 0–1); the search is identical in both cases.
|
|
1090
|
+
*/
|
|
1091
|
+
function searchBranch(lum, lo, hi, yBase, metric, target, epsilon, maxIter, anchor) {
|
|
1092
|
+
const scoreLo = metricScore(metric, lum(lo), yBase);
|
|
1093
|
+
const scoreHi = metricScore(metric, lum(hi), yBase);
|
|
1094
|
+
if (scoreLo < target && scoreHi < target) return scoreLo >= scoreHi ? {
|
|
1095
|
+
pos: lo,
|
|
1096
|
+
contrast: scoreLo,
|
|
1097
|
+
met: false
|
|
1098
|
+
} : {
|
|
1099
|
+
pos: hi,
|
|
1100
|
+
contrast: scoreHi,
|
|
1101
|
+
met: false
|
|
1102
|
+
};
|
|
748
1103
|
let low = lo;
|
|
749
1104
|
let high = hi;
|
|
750
1105
|
for (let i = 0; i < maxIter; i++) {
|
|
751
1106
|
if (high - low < epsilon) break;
|
|
752
1107
|
const mid = (low + high) / 2;
|
|
753
|
-
if (
|
|
1108
|
+
if (metricScore(metric, lum(mid), yBase) >= target) if (mid < anchor) low = mid;
|
|
754
1109
|
else high = mid;
|
|
755
|
-
else if (mid <
|
|
1110
|
+
else if (mid < anchor) high = mid;
|
|
756
1111
|
else low = mid;
|
|
757
1112
|
}
|
|
758
|
-
const
|
|
759
|
-
const
|
|
760
|
-
const
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
lightness: high,
|
|
772
|
-
contrast: crHigh,
|
|
773
|
-
met: true
|
|
774
|
-
};
|
|
775
|
-
}
|
|
1113
|
+
const scoreLow = metricScore(metric, lum(low), yBase);
|
|
1114
|
+
const scoreHigh = metricScore(metric, lum(high), yBase);
|
|
1115
|
+
const lowPasses = scoreLow >= target;
|
|
1116
|
+
const highPasses = scoreHigh >= target;
|
|
1117
|
+
if (lowPasses && highPasses) return Math.abs(low - anchor) <= Math.abs(high - anchor) ? {
|
|
1118
|
+
pos: low,
|
|
1119
|
+
contrast: scoreLow,
|
|
1120
|
+
met: true
|
|
1121
|
+
} : {
|
|
1122
|
+
pos: high,
|
|
1123
|
+
contrast: scoreHigh,
|
|
1124
|
+
met: true
|
|
1125
|
+
};
|
|
776
1126
|
if (lowPasses) return {
|
|
777
|
-
|
|
778
|
-
contrast:
|
|
1127
|
+
pos: low,
|
|
1128
|
+
contrast: scoreLow,
|
|
779
1129
|
met: true
|
|
780
1130
|
};
|
|
781
1131
|
if (highPasses) return {
|
|
782
|
-
|
|
783
|
-
contrast:
|
|
1132
|
+
pos: high,
|
|
1133
|
+
contrast: scoreHigh,
|
|
784
1134
|
met: true
|
|
785
1135
|
};
|
|
786
|
-
return
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
let bestL = lo;
|
|
795
|
-
let bestCr = 0;
|
|
796
|
-
let bestMet = false;
|
|
797
|
-
for (let i = 0; i <= STEPS; i++) {
|
|
798
|
-
const l = lo + step * i;
|
|
799
|
-
const cr = contrastRatioFromLuminance(cachedLuminance(h, s, l), yBase);
|
|
800
|
-
if (cr >= target && !bestMet) {
|
|
801
|
-
bestL = l;
|
|
802
|
-
bestCr = cr;
|
|
803
|
-
bestMet = true;
|
|
804
|
-
} else if (cr >= target && bestMet) {
|
|
805
|
-
bestL = l;
|
|
806
|
-
bestCr = cr;
|
|
807
|
-
} else if (!bestMet && cr > bestCr) {
|
|
808
|
-
bestL = l;
|
|
809
|
-
bestCr = cr;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
if (bestMet && bestL > lo + step) {
|
|
813
|
-
let rLo = bestL - step;
|
|
814
|
-
let rHi = bestL;
|
|
815
|
-
for (let i = 0; i < maxIter; i++) {
|
|
816
|
-
if (rHi - rLo < epsilon) break;
|
|
817
|
-
const mid = (rLo + rHi) / 2;
|
|
818
|
-
const cr = contrastRatioFromLuminance(cachedLuminance(h, s, mid), yBase);
|
|
819
|
-
if (cr >= target) {
|
|
820
|
-
rHi = mid;
|
|
821
|
-
bestL = mid;
|
|
822
|
-
bestCr = cr;
|
|
823
|
-
} else rLo = mid;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return {
|
|
827
|
-
lightness: bestL,
|
|
828
|
-
contrast: bestCr,
|
|
829
|
-
met: bestMet
|
|
1136
|
+
return scoreLow >= scoreHigh ? {
|
|
1137
|
+
pos: low,
|
|
1138
|
+
contrast: scoreLow,
|
|
1139
|
+
met: false
|
|
1140
|
+
} : {
|
|
1141
|
+
pos: high,
|
|
1142
|
+
contrast: scoreHigh,
|
|
1143
|
+
met: false
|
|
830
1144
|
};
|
|
831
1145
|
}
|
|
832
1146
|
/**
|
|
833
|
-
*
|
|
834
|
-
* against
|
|
1147
|
+
* Closed-form WCAG tone seed: the gray tone whose luminance produces exactly
|
|
1148
|
+
* the target ratio against the base, on the requested side. Used to bias the
|
|
1149
|
+
* preferred tone before the search so chromatic refinement starts close.
|
|
835
1150
|
*/
|
|
836
|
-
function
|
|
837
|
-
const
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
met: true,
|
|
846
|
-
branch: "preferred"
|
|
847
|
-
};
|
|
848
|
-
const [minL, maxL] = lightnessRange;
|
|
849
|
-
const canDarker = preferredLightness > minL;
|
|
850
|
-
const canLighter = preferredLightness < maxL;
|
|
851
|
-
let initialIsDarker;
|
|
852
|
-
if (options.initialDirection !== void 0) initialIsDarker = options.initialDirection === "darker";
|
|
853
|
-
else if (canDarker && !canLighter) initialIsDarker = true;
|
|
854
|
-
else if (!canDarker && canLighter) initialIsDarker = false;
|
|
855
|
-
else if (!canDarker && !canLighter) return {
|
|
856
|
-
lightness: preferredLightness,
|
|
857
|
-
contrast: crPref,
|
|
858
|
-
met: false,
|
|
859
|
-
branch: "preferred"
|
|
860
|
-
};
|
|
861
|
-
else {
|
|
862
|
-
const yMinExt = cachedLuminance(hue, saturation, minL);
|
|
863
|
-
const yMaxExt = cachedLuminance(hue, saturation, maxL);
|
|
864
|
-
initialIsDarker = contrastRatioFromLuminance(yMinExt, yBase) >= contrastRatioFromLuminance(yMaxExt, yBase);
|
|
865
|
-
}
|
|
866
|
-
const searchInitial = () => initialIsDarker ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
|
|
867
|
-
const searchOpposite = () => initialIsDarker ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
|
|
868
|
-
const initialBranchName = initialIsDarker ? "darker" : "lighter";
|
|
869
|
-
const oppositeBranchName = initialIsDarker ? "lighter" : "darker";
|
|
870
|
-
const initialResult = searchInitial();
|
|
1151
|
+
function wcagToneSeed(yBase, target, darker) {
|
|
1152
|
+
const yTarget = darker ? (yBase + .05) / target - .05 : target * (yBase + .05) - .05;
|
|
1153
|
+
const yClamped = Math.max(0, Math.min(1, yTarget));
|
|
1154
|
+
return Math.max(0, Math.min(1, toneFromY(yClamped, REF_EPS) / 100));
|
|
1155
|
+
}
|
|
1156
|
+
function solveNearestContrast(opts) {
|
|
1157
|
+
const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, initialIsLower } = opts;
|
|
1158
|
+
const runBranch = (lower) => lower ? searchBranch(lum, lo, searchAnchor, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor) : searchBranch(lum, searchAnchor, hi, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor);
|
|
1159
|
+
const initialResult = runBranch(initialIsLower);
|
|
871
1160
|
initialResult.met = initialResult.contrast >= target;
|
|
872
|
-
if (initialResult.met && !
|
|
1161
|
+
if (initialResult.met && !flip) return {
|
|
873
1162
|
...initialResult,
|
|
874
|
-
|
|
1163
|
+
lower: initialIsLower
|
|
875
1164
|
};
|
|
876
|
-
if (
|
|
877
|
-
const oppositeResult = (
|
|
1165
|
+
if (flip) {
|
|
1166
|
+
const oppositeResult = (initialIsLower ? distanceAnchor < hi : distanceAnchor > lo) ? runBranch(!initialIsLower) : null;
|
|
878
1167
|
if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
|
|
879
|
-
if (initialResult.met && oppositeResult?.met) {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
flipped: true
|
|
888
|
-
};
|
|
889
|
-
}
|
|
1168
|
+
if (initialResult.met && oppositeResult?.met) return Math.abs(initialResult.pos - distanceAnchor) <= Math.abs(oppositeResult.pos - distanceAnchor) ? {
|
|
1169
|
+
...initialResult,
|
|
1170
|
+
lower: initialIsLower
|
|
1171
|
+
} : {
|
|
1172
|
+
...oppositeResult,
|
|
1173
|
+
lower: !initialIsLower,
|
|
1174
|
+
flipped: true
|
|
1175
|
+
};
|
|
890
1176
|
if (initialResult.met) return {
|
|
891
1177
|
...initialResult,
|
|
892
|
-
|
|
1178
|
+
lower: initialIsLower
|
|
893
1179
|
};
|
|
894
1180
|
if (oppositeResult?.met) return {
|
|
895
1181
|
...oppositeResult,
|
|
896
|
-
|
|
1182
|
+
lower: !initialIsLower,
|
|
897
1183
|
flipped: true
|
|
898
1184
|
};
|
|
899
1185
|
}
|
|
900
|
-
const extreme =
|
|
1186
|
+
const extreme = initialIsLower ? lo : hi;
|
|
901
1187
|
return {
|
|
902
|
-
|
|
903
|
-
contrast:
|
|
1188
|
+
pos: extreme,
|
|
1189
|
+
contrast: metricScore(metric, lum(extreme), yBase),
|
|
904
1190
|
met: false,
|
|
905
|
-
|
|
1191
|
+
lower: initialIsLower
|
|
906
1192
|
};
|
|
907
1193
|
}
|
|
908
1194
|
/**
|
|
909
|
-
*
|
|
910
|
-
* to `
|
|
1195
|
+
* Find the tone that satisfies a contrast floor against a base color,
|
|
1196
|
+
* staying as close to `preferredTone` as possible.
|
|
911
1197
|
*/
|
|
912
|
-
function
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
met: false
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
let low = lo;
|
|
928
|
-
let high = hi;
|
|
929
|
-
for (let i = 0; i < maxIter; i++) {
|
|
930
|
-
if (high - low < epsilon) break;
|
|
931
|
-
const mid = (low + high) / 2;
|
|
932
|
-
if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
|
|
933
|
-
else high = mid;
|
|
934
|
-
else if (mid < preferred) high = mid;
|
|
935
|
-
else low = mid;
|
|
936
|
-
}
|
|
937
|
-
const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
|
|
938
|
-
const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
|
|
939
|
-
const lowPasses = crLow >= target;
|
|
940
|
-
const highPasses = crHigh >= target;
|
|
941
|
-
if (lowPasses && highPasses) {
|
|
942
|
-
if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
|
|
943
|
-
lightness: low,
|
|
944
|
-
contrast: crLow,
|
|
945
|
-
met: true
|
|
946
|
-
};
|
|
947
|
-
return {
|
|
948
|
-
lightness: high,
|
|
949
|
-
contrast: crHigh,
|
|
950
|
-
met: true
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
if (lowPasses) return {
|
|
954
|
-
lightness: low,
|
|
955
|
-
contrast: crLow,
|
|
956
|
-
met: true
|
|
1198
|
+
function findToneForContrast(options) {
|
|
1199
|
+
const { hue, saturation, preferredTone, baseLinearRgb, contrast, toneRange = [0, 1], epsilon = 1e-4, maxIterations = 18, pastel = false } = options;
|
|
1200
|
+
const { metric, target } = contrast;
|
|
1201
|
+
const searchTarget = metric === "wcag" ? target * 1.01 : target + .5;
|
|
1202
|
+
const yBase = metricLuminance(metric, baseLinearRgb);
|
|
1203
|
+
const lum = (t) => cachedLuminance(metric, hue, saturation, t, pastel);
|
|
1204
|
+
const scorePref = metricScore(metric, lum(preferredTone), yBase);
|
|
1205
|
+
if (scorePref >= searchTarget) return {
|
|
1206
|
+
tone: preferredTone,
|
|
1207
|
+
contrast: scorePref,
|
|
1208
|
+
met: true,
|
|
1209
|
+
branch: "preferred"
|
|
957
1210
|
};
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1211
|
+
const [minT, maxT] = toneRange;
|
|
1212
|
+
const canDarker = preferredTone > minT;
|
|
1213
|
+
const canLighter = preferredTone < maxT;
|
|
1214
|
+
let initialIsDarker;
|
|
1215
|
+
if (options.initialDirection !== void 0) initialIsDarker = options.initialDirection === "darker";
|
|
1216
|
+
else if (canDarker && !canLighter) initialIsDarker = true;
|
|
1217
|
+
else if (!canDarker && canLighter) initialIsDarker = false;
|
|
1218
|
+
else if (!canDarker && !canLighter) return {
|
|
1219
|
+
tone: preferredTone,
|
|
1220
|
+
contrast: scorePref,
|
|
1221
|
+
met: false,
|
|
1222
|
+
branch: "preferred"
|
|
962
1223
|
};
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1224
|
+
else initialIsDarker = metricScore(metric, lum(minT), yBase) >= metricScore(metric, lum(maxT), yBase);
|
|
1225
|
+
const solved = solveNearestContrast({
|
|
1226
|
+
lum,
|
|
1227
|
+
yBase,
|
|
1228
|
+
metric,
|
|
1229
|
+
target,
|
|
1230
|
+
searchTarget,
|
|
1231
|
+
lo: minT,
|
|
1232
|
+
hi: maxT,
|
|
1233
|
+
searchAnchor: metric === "wcag" ? clamp(initialIsDarker ? Math.min(preferredTone, wcagToneSeed(yBase, target, true)) : Math.max(preferredTone, wcagToneSeed(yBase, target, false)), minT, maxT) : preferredTone,
|
|
1234
|
+
distanceAnchor: preferredTone,
|
|
1235
|
+
epsilon,
|
|
1236
|
+
maxIterations,
|
|
1237
|
+
flip: options.flip ?? false,
|
|
1238
|
+
initialIsLower: initialIsDarker
|
|
1239
|
+
});
|
|
1240
|
+
return {
|
|
1241
|
+
tone: solved.pos,
|
|
1242
|
+
contrast: solved.contrast,
|
|
1243
|
+
met: solved.met,
|
|
1244
|
+
branch: solved.lower ? "darker" : "lighter",
|
|
1245
|
+
...solved.flipped ? { flipped: true } : {}
|
|
971
1246
|
};
|
|
972
1247
|
}
|
|
973
1248
|
/**
|
|
974
|
-
* Find the mix parameter (ratio or opacity) that satisfies a
|
|
975
|
-
*
|
|
1249
|
+
* Find the mix parameter (ratio or opacity) that satisfies a contrast floor
|
|
1250
|
+
* against a base color, staying as close to `preferredValue` as possible.
|
|
976
1251
|
*/
|
|
977
1252
|
function findValueForMixContrast(options) {
|
|
978
|
-
const { preferredValue, baseLinearRgb, contrast
|
|
979
|
-
const target =
|
|
980
|
-
const searchTarget = target * 1.01;
|
|
981
|
-
const yBase =
|
|
982
|
-
const
|
|
983
|
-
if (
|
|
1253
|
+
const { preferredValue, baseLinearRgb, contrast, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
1254
|
+
const { metric, target } = contrast;
|
|
1255
|
+
const searchTarget = metric === "wcag" ? target * 1.01 : target + .5;
|
|
1256
|
+
const yBase = metricLuminance(metric, baseLinearRgb);
|
|
1257
|
+
const scorePref = metricScore(metric, luminanceAtValue(preferredValue), yBase);
|
|
1258
|
+
if (scorePref >= searchTarget) return {
|
|
984
1259
|
value: preferredValue,
|
|
985
|
-
contrast:
|
|
1260
|
+
contrast: scorePref,
|
|
986
1261
|
met: true
|
|
987
1262
|
};
|
|
988
1263
|
const canLower = preferredValue > 0;
|
|
@@ -992,52 +1267,30 @@ function findValueForMixContrast(options) {
|
|
|
992
1267
|
else if (!canLower && canUpper) initialIsLower = false;
|
|
993
1268
|
else if (!canLower && !canUpper) return {
|
|
994
1269
|
value: preferredValue,
|
|
995
|
-
contrast:
|
|
1270
|
+
contrast: scorePref,
|
|
996
1271
|
met: false
|
|
997
1272
|
};
|
|
998
|
-
else initialIsLower =
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
contrast: initialResult.contrast,
|
|
1015
|
-
met: true
|
|
1016
|
-
};
|
|
1017
|
-
return {
|
|
1018
|
-
value: oppositeResult.lightness,
|
|
1019
|
-
contrast: oppositeResult.contrast,
|
|
1020
|
-
met: true,
|
|
1021
|
-
flipped: true
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
if (initialResult.met) return {
|
|
1025
|
-
value: initialResult.lightness,
|
|
1026
|
-
contrast: initialResult.contrast,
|
|
1027
|
-
met: true
|
|
1028
|
-
};
|
|
1029
|
-
if (oppositeResult?.met) return {
|
|
1030
|
-
value: oppositeResult.lightness,
|
|
1031
|
-
contrast: oppositeResult.contrast,
|
|
1032
|
-
met: true,
|
|
1033
|
-
flipped: true
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
const extreme = initialIsLower ? 0 : 1;
|
|
1273
|
+
else initialIsLower = metricScore(metric, luminanceAtValue(0), yBase) >= metricScore(metric, luminanceAtValue(1), yBase);
|
|
1274
|
+
const solved = solveNearestContrast({
|
|
1275
|
+
lum: luminanceAtValue,
|
|
1276
|
+
yBase,
|
|
1277
|
+
metric,
|
|
1278
|
+
target,
|
|
1279
|
+
searchTarget,
|
|
1280
|
+
lo: 0,
|
|
1281
|
+
hi: 1,
|
|
1282
|
+
searchAnchor: preferredValue,
|
|
1283
|
+
distanceAnchor: preferredValue,
|
|
1284
|
+
epsilon,
|
|
1285
|
+
maxIterations,
|
|
1286
|
+
flip: options.flip ?? false,
|
|
1287
|
+
initialIsLower
|
|
1288
|
+
});
|
|
1037
1289
|
return {
|
|
1038
|
-
value:
|
|
1039
|
-
contrast:
|
|
1040
|
-
met:
|
|
1290
|
+
value: solved.pos,
|
|
1291
|
+
contrast: solved.contrast,
|
|
1292
|
+
met: solved.met,
|
|
1293
|
+
...solved.flipped ? { flipped: true } : {}
|
|
1041
1294
|
};
|
|
1042
1295
|
}
|
|
1043
1296
|
|
|
@@ -1113,73 +1366,13 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1113
1366
|
};
|
|
1114
1367
|
}
|
|
1115
1368
|
|
|
1116
|
-
//#endregion
|
|
1117
|
-
//#region src/scheme-mapping.ts
|
|
1118
|
-
/**
|
|
1119
|
-
* Light / dark scheme lightness mappings.
|
|
1120
|
-
*
|
|
1121
|
-
* Owns the active lightness window selection (from a resolved effective
|
|
1122
|
-
* config passed in), the Möbius curve used by the `'auto'` dark
|
|
1123
|
-
* adaptation, and the saturation-desaturation reducer for dark mode.
|
|
1124
|
-
*
|
|
1125
|
-
* All functions take a `GlazeConfigResolved` so the full config
|
|
1126
|
-
* (including per-instance overrides) is available without re-reading
|
|
1127
|
-
* the global singleton inside the resolver.
|
|
1128
|
-
*/
|
|
1129
|
-
/**
|
|
1130
|
-
* Resolve the active lightness window for a scheme.
|
|
1131
|
-
* - HC variants always return `[0, 100]` (no clamping in high-contrast).
|
|
1132
|
-
* - `false` (= "no clamping") is treated as `[0, 100]`.
|
|
1133
|
-
* - Otherwise uses the window from the resolved effective config.
|
|
1134
|
-
*/
|
|
1135
|
-
function lightnessWindow(isHighContrast, kind, config) {
|
|
1136
|
-
if (isHighContrast) return [0, 100];
|
|
1137
|
-
const win = kind === "dark" ? config.darkLightness : config.lightLightness;
|
|
1138
|
-
if (win === false) return [0, 100];
|
|
1139
|
-
return win;
|
|
1140
|
-
}
|
|
1141
|
-
function mapLightnessLight(l, mode, isHighContrast, config) {
|
|
1142
|
-
if (mode === "static") return l;
|
|
1143
|
-
const [lo, hi] = lightnessWindow(isHighContrast, "light", config);
|
|
1144
|
-
return l * (hi - lo) / 100 + lo;
|
|
1145
|
-
}
|
|
1146
|
-
function mobiusCurve(t, beta) {
|
|
1147
|
-
if (beta >= 1) return t;
|
|
1148
|
-
return t / (t + beta * (1 - t));
|
|
1149
|
-
}
|
|
1150
|
-
function mapLightnessDark(l, mode, isHighContrast, config) {
|
|
1151
|
-
if (mode === "static") return l;
|
|
1152
|
-
const beta = isHighContrast ? pairHC(config.darkCurve) : pairNormal(config.darkCurve);
|
|
1153
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", config);
|
|
1154
|
-
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1155
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", config);
|
|
1156
|
-
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1157
|
-
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1158
|
-
}
|
|
1159
|
-
function lightMappedToDark(lightL, isHighContrast, config) {
|
|
1160
|
-
const beta = isHighContrast ? pairHC(config.darkCurve) : pairNormal(config.darkCurve);
|
|
1161
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", config);
|
|
1162
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", config);
|
|
1163
|
-
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1164
|
-
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1165
|
-
}
|
|
1166
|
-
function mapSaturationDark(s, mode, config) {
|
|
1167
|
-
if (mode === "static") return s;
|
|
1168
|
-
return s * (1 - config.darkDesaturation);
|
|
1169
|
-
}
|
|
1170
|
-
function schemeLightnessRange(isDark, mode, isHighContrast, config) {
|
|
1171
|
-
if (mode === "static") return [0, 1];
|
|
1172
|
-
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
1173
|
-
return [lo / 100, hi / 100];
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
1369
|
//#endregion
|
|
1177
1370
|
//#region src/validation.ts
|
|
1178
1371
|
/**
|
|
1179
1372
|
* Color graph validation and topological sort.
|
|
1180
1373
|
*
|
|
1181
1374
|
* `validateColorDefs` rejects bad references (missing / shadow-referencing /
|
|
1182
|
-
* base/contrast/
|
|
1375
|
+
* base/contrast/tone mismatches) and detects cycles before the
|
|
1183
1376
|
* resolver runs. `topoSort` orders defs so each color is processed after
|
|
1184
1377
|
* its base / bg / fg / target dependencies.
|
|
1185
1378
|
*/
|
|
@@ -1205,11 +1398,11 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1205
1398
|
}
|
|
1206
1399
|
const regDef = def;
|
|
1207
1400
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
1208
|
-
if (regDef.
|
|
1401
|
+
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1209
1402
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1210
1403
|
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1211
|
-
if (!
|
|
1212
|
-
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived
|
|
1404
|
+
if (!isAbsoluteTone(regDef.tone) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "tone" (root) or "base" (dependent).`);
|
|
1405
|
+
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived tone unpredictable.`);
|
|
1213
1406
|
}
|
|
1214
1407
|
const visited = /* @__PURE__ */ new Set();
|
|
1215
1408
|
const inStack = /* @__PURE__ */ new Set();
|
|
@@ -1272,30 +1465,46 @@ const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
|
1272
1465
|
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
1273
1466
|
/**
|
|
1274
1467
|
* Slack factor below the requested target before we emit a warning.
|
|
1275
|
-
* The contrast solver
|
|
1276
|
-
*
|
|
1277
|
-
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
1278
|
-
* is effectively a pass and not worth nagging the user about.
|
|
1468
|
+
* The contrast solver overshoots to absorb rounding noise, so an actual
|
|
1469
|
+
* value within ~2x that overshoot is effectively a pass.
|
|
1279
1470
|
*/
|
|
1280
|
-
const
|
|
1471
|
+
const CONTRAST_WARN_SLACK_WCAG = .98;
|
|
1472
|
+
/** APCA Lc is on a 0–106 scale; allow a small absolute slack. */
|
|
1473
|
+
const CONTRAST_WARN_SLACK_APCA = 1.5;
|
|
1281
1474
|
function schemeLabel(isDark, isHighContrast) {
|
|
1282
1475
|
if (isDark && isHighContrast) return "darkContrast";
|
|
1283
1476
|
if (isDark) return "dark";
|
|
1284
1477
|
if (isHighContrast) return "lightContrast";
|
|
1285
1478
|
return "light";
|
|
1286
1479
|
}
|
|
1287
|
-
function
|
|
1288
|
-
return
|
|
1480
|
+
function metricLabel(c) {
|
|
1481
|
+
return c.metric === "apca" ? `APCA Lc ${c.target.toFixed(1)}` : `WCAG ${c.target.toFixed(2)}`;
|
|
1289
1482
|
}
|
|
1290
|
-
function
|
|
1291
|
-
|
|
1292
|
-
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
1293
|
-
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1294
|
-
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
1295
|
-
if (contrastWarnCache.has(key)) return;
|
|
1483
|
+
function dedupe(key) {
|
|
1484
|
+
if (contrastWarnCache.has(key)) return true;
|
|
1296
1485
|
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
1297
1486
|
contrastWarnCache.add(key);
|
|
1298
|
-
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
/** Warn when the solver could not reach the requested contrast floor. */
|
|
1490
|
+
function warnContrastUnmet(name, isDark, isHighContrast, contrast, actual) {
|
|
1491
|
+
if (actual >= (contrast.metric === "apca" ? contrast.target - CONTRAST_WARN_SLACK_APCA : contrast.target * CONTRAST_WARN_SLACK_WCAG)) return;
|
|
1492
|
+
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1493
|
+
if (dedupe(`unmet|${name}|${scheme}|${contrast.metric}|${contrast.target.toFixed(2)}|${actual.toFixed(2)}`)) return;
|
|
1494
|
+
console.warn(`glaze: color "${name}" cannot meet ${metricLabel(contrast)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the tone window, lowering the contrast target, or picking a base color further from this color's tone.`);
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Verification (§10): a chromatic swatch inherits the gray tone's
|
|
1498
|
+
* lightness but drifts in real luminance, so a contrast-floored color may
|
|
1499
|
+
* land slightly under the contrast its tone implies. Emit an advisory
|
|
1500
|
+
* warning when the actual measured contrast drifts below the target.
|
|
1501
|
+
*/
|
|
1502
|
+
function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase) {
|
|
1503
|
+
const actual = contrast.metric === "apca" ? Math.abs(apcaContrast(yColor, yBase)) : contrastRatioFromLuminance(yColor, yBase);
|
|
1504
|
+
if (actual >= (contrast.metric === "apca" ? contrast.target - CONTRAST_WARN_SLACK_APCA : contrast.target * CONTRAST_WARN_SLACK_WCAG)) return;
|
|
1505
|
+
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1506
|
+
if (dedupe(`drift|${name}|${scheme}|${contrast.metric}|${contrast.target.toFixed(2)}|${actual.toFixed(2)}`)) return;
|
|
1507
|
+
console.warn(`glaze: color "${name}" drifts below ${metricLabel(contrast)} in ${scheme} scheme (measured ${actual.toFixed(2)}). Chromatic luminance differs from the gray tone; nudge the tone or saturation if the floor matters.`);
|
|
1299
1508
|
}
|
|
1300
1509
|
|
|
1301
1510
|
//#endregion
|
|
@@ -1308,6 +1517,11 @@ function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
|
1308
1517
|
* Owns the per-scheme resolve helpers for regular, shadow, and mix
|
|
1309
1518
|
* color defs.
|
|
1310
1519
|
*
|
|
1520
|
+
* Variants are stored in OKHST: `h` / `s` are OKHSL hue/saturation and
|
|
1521
|
+
* `t` is the canonical contrast-uniform tone (0–1, reference eps). The
|
|
1522
|
+
* resolver works in tone for regular colors and converts to/from OKHSL
|
|
1523
|
+
* lightness only at the mix/shadow and luminance edges.
|
|
1524
|
+
*
|
|
1311
1525
|
* Every function receives a single `GlazeConfigResolved` so the full
|
|
1312
1526
|
* per-instance config (including overrides) is available without
|
|
1313
1527
|
* re-reading the global singleton mid-resolve.
|
|
@@ -1318,10 +1532,50 @@ function getSchemeVariant(color, isDark, isHighContrast) {
|
|
|
1318
1532
|
if (isHighContrast) return color.lightContrast;
|
|
1319
1533
|
return color.light;
|
|
1320
1534
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1535
|
+
/** Edge adapter: resolved variant (`t`) → OKHSL-lightness variant. */
|
|
1536
|
+
function toOkhslVariant(v) {
|
|
1537
|
+
const c = variantToOkhsl(v);
|
|
1323
1538
|
return {
|
|
1324
|
-
|
|
1539
|
+
h: c.h,
|
|
1540
|
+
s: c.s,
|
|
1541
|
+
l: c.l,
|
|
1542
|
+
alpha: v.alpha
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
/** Edge adapter: OKHSL-lightness variant → resolved variant (`t`). */
|
|
1546
|
+
function toToneVariant(v) {
|
|
1547
|
+
const c = okhslToOkhst({
|
|
1548
|
+
h: v.h,
|
|
1549
|
+
s: v.s,
|
|
1550
|
+
l: v.l
|
|
1551
|
+
});
|
|
1552
|
+
return {
|
|
1553
|
+
h: c.h,
|
|
1554
|
+
s: c.s,
|
|
1555
|
+
t: c.t,
|
|
1556
|
+
alpha: v.alpha
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function resolveContrastSpec(spec, isHighContrast) {
|
|
1560
|
+
return resolveContrastForMode(isHighContrast ? pairHC(spec) : pairNormal(spec), isHighContrast);
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Apply the relative-tone delta against a base, honoring `flip`.
|
|
1564
|
+
*
|
|
1565
|
+
* When `flip` is on and `base + delta` falls outside `[0, 100]`, mirror the
|
|
1566
|
+
* delta to the other side of the base (so an offset that would clamp instead
|
|
1567
|
+
* reflects back into range). When off, the caller clamps as usual.
|
|
1568
|
+
*/
|
|
1569
|
+
function applyToneFlip(delta, baseTone, flip) {
|
|
1570
|
+
if (!flip) return delta;
|
|
1571
|
+
const target = baseTone + delta;
|
|
1572
|
+
if (target >= 0 && target <= 100) return delta;
|
|
1573
|
+
return -delta;
|
|
1574
|
+
}
|
|
1575
|
+
function resolveRootColor(def, isHighContrast) {
|
|
1576
|
+
const rawT = def.tone;
|
|
1577
|
+
return {
|
|
1578
|
+
authorTone: clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100),
|
|
1325
1579
|
satFactor: clamp(def.saturation ?? 1, 0, 1)
|
|
1326
1580
|
};
|
|
1327
1581
|
}
|
|
@@ -1331,47 +1585,49 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1331
1585
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
1332
1586
|
const mode = def.mode ?? "auto";
|
|
1333
1587
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
1588
|
+
const flip = def.flip ?? ctx.config.autoFlip;
|
|
1334
1589
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1335
|
-
const
|
|
1336
|
-
let
|
|
1337
|
-
const
|
|
1338
|
-
if (
|
|
1590
|
+
const baseTone = baseVariant.t * 100;
|
|
1591
|
+
let preferredTone;
|
|
1592
|
+
const rawTone = def.tone;
|
|
1593
|
+
if (rawTone === void 0) preferredTone = baseTone;
|
|
1339
1594
|
else {
|
|
1340
|
-
const parsed =
|
|
1341
|
-
if (parsed.relative) {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.config);
|
|
1595
|
+
const parsed = parseToneValue(isHighContrast ? pairHC(rawTone) : pairNormal(rawTone));
|
|
1596
|
+
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
1597
|
+
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
1598
|
+
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
|
|
1599
|
+
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
1600
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
|
|
1347
1601
|
}
|
|
1348
1602
|
const rawContrast = def.contrast;
|
|
1349
1603
|
if (rawContrast !== void 0) {
|
|
1350
|
-
const
|
|
1604
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast);
|
|
1351
1605
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
1352
|
-
const
|
|
1353
|
-
const
|
|
1606
|
+
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
1607
|
+
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, ctx.config.pastel);
|
|
1608
|
+
const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
1354
1609
|
let initialDirection;
|
|
1355
|
-
if (
|
|
1356
|
-
else if (
|
|
1357
|
-
const result =
|
|
1610
|
+
if (preferredTone < baseTone) initialDirection = "darker";
|
|
1611
|
+
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
1612
|
+
const result = findToneForContrast({
|
|
1358
1613
|
hue: effectiveHue,
|
|
1359
1614
|
saturation: effectiveSat,
|
|
1360
|
-
|
|
1615
|
+
preferredTone: clamp(preferredTone / 100, toneRange[0], toneRange[1]),
|
|
1361
1616
|
baseLinearRgb,
|
|
1362
|
-
contrast:
|
|
1363
|
-
|
|
1617
|
+
contrast: resolvedContrast,
|
|
1618
|
+
toneRange: [0, 1],
|
|
1364
1619
|
initialDirection,
|
|
1365
|
-
flip
|
|
1620
|
+
flip,
|
|
1621
|
+
pastel: ctx.config.pastel
|
|
1366
1622
|
});
|
|
1367
|
-
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast,
|
|
1623
|
+
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
1368
1624
|
return {
|
|
1369
|
-
|
|
1625
|
+
tone: result.tone * 100,
|
|
1370
1626
|
satFactor
|
|
1371
1627
|
};
|
|
1372
1628
|
}
|
|
1373
1629
|
return {
|
|
1374
|
-
|
|
1630
|
+
tone: clamp(preferredTone, 0, 100),
|
|
1375
1631
|
satFactor
|
|
1376
1632
|
};
|
|
1377
1633
|
}
|
|
@@ -1380,51 +1636,39 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1380
1636
|
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
1381
1637
|
const regDef = def;
|
|
1382
1638
|
const mode = regDef.mode ?? "auto";
|
|
1383
|
-
const isRoot =
|
|
1639
|
+
const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
|
|
1384
1640
|
const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
|
|
1385
|
-
let
|
|
1641
|
+
let finalTone;
|
|
1386
1642
|
let satFactor;
|
|
1387
1643
|
if (isRoot) {
|
|
1388
|
-
const root = resolveRootColor(
|
|
1389
|
-
|
|
1644
|
+
const root = resolveRootColor(regDef, isHighContrast);
|
|
1645
|
+
finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
|
|
1390
1646
|
satFactor = root.satFactor;
|
|
1391
1647
|
} else {
|
|
1392
1648
|
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue);
|
|
1393
|
-
|
|
1649
|
+
finalTone = dep.tone;
|
|
1394
1650
|
satFactor = dep.satFactor;
|
|
1395
1651
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
finalL = mapLightnessDark(lightL, mode, isHighContrast, ctx.config);
|
|
1400
|
-
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config);
|
|
1401
|
-
} else if (isDark && !isRoot) {
|
|
1402
|
-
finalL = lightL;
|
|
1403
|
-
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config);
|
|
1404
|
-
} else if (isRoot) {
|
|
1405
|
-
finalL = mapLightnessLight(lightL, mode, isHighContrast, ctx.config);
|
|
1406
|
-
finalSat = satFactor * ctx.saturation / 100;
|
|
1407
|
-
} else {
|
|
1408
|
-
finalL = lightL;
|
|
1409
|
-
finalSat = satFactor * ctx.saturation / 100;
|
|
1410
|
-
}
|
|
1652
|
+
const baseSat = satFactor * ctx.saturation / 100;
|
|
1653
|
+
const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
|
|
1654
|
+
const toneFraction = clamp(finalTone / 100, 0, 1);
|
|
1411
1655
|
return {
|
|
1412
1656
|
h: effectiveHue,
|
|
1413
1657
|
s: clamp(finalSat, 0, 1),
|
|
1414
|
-
|
|
1658
|
+
t: toneFraction,
|
|
1415
1659
|
alpha: regDef.opacity ?? 1
|
|
1416
1660
|
};
|
|
1417
1661
|
}
|
|
1418
1662
|
function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
1419
|
-
const bgVariant = getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast);
|
|
1663
|
+
const bgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast));
|
|
1420
1664
|
let fgVariant;
|
|
1421
|
-
if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
|
|
1665
|
+
if (def.fg) fgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast));
|
|
1422
1666
|
const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
|
|
1423
1667
|
const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
|
|
1424
|
-
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
1668
|
+
return toToneVariant(computeShadow(bgVariant, fgVariant, intensity, tuning));
|
|
1425
1669
|
}
|
|
1426
|
-
function
|
|
1427
|
-
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
1670
|
+
function okhslVariantToLinearRgb(v, pastel) {
|
|
1671
|
+
return okhslToLinearSrgb(v.h, v.s, v.l, pastel);
|
|
1428
1672
|
}
|
|
1429
1673
|
/**
|
|
1430
1674
|
* Resolve hue for OKHSL mixing, handling achromatic colors.
|
|
@@ -1447,59 +1691,59 @@ function linearSrgbLerp(base, target, t) {
|
|
|
1447
1691
|
base[2] + (target[2] - base[2]) * t
|
|
1448
1692
|
];
|
|
1449
1693
|
}
|
|
1450
|
-
function
|
|
1694
|
+
function linearRgbToToneVariant(rgb, pastel) {
|
|
1451
1695
|
const [h, s, l] = srgbToOkhsl([
|
|
1452
1696
|
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
|
|
1453
1697
|
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
|
|
1454
1698
|
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
|
|
1455
|
-
]);
|
|
1456
|
-
return {
|
|
1699
|
+
], pastel);
|
|
1700
|
+
return toToneVariant({
|
|
1457
1701
|
h,
|
|
1458
1702
|
s,
|
|
1459
1703
|
l,
|
|
1460
1704
|
alpha: 1
|
|
1461
|
-
};
|
|
1705
|
+
});
|
|
1462
1706
|
}
|
|
1463
1707
|
function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
1464
1708
|
const baseResolved = ctx.resolved.get(def.base);
|
|
1465
1709
|
const targetResolved = ctx.resolved.get(def.target);
|
|
1466
|
-
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1467
|
-
const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
|
|
1710
|
+
const baseVariant = toOkhslVariant(getSchemeVariant(baseResolved, isDark, isHighContrast));
|
|
1711
|
+
const targetVariant = toOkhslVariant(getSchemeVariant(targetResolved, isDark, isHighContrast));
|
|
1468
1712
|
let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
|
|
1469
1713
|
const blend = def.blend ?? "opaque";
|
|
1470
1714
|
const space = def.space ?? "okhsl";
|
|
1471
|
-
const baseLinear =
|
|
1472
|
-
const targetLinear =
|
|
1715
|
+
const baseLinear = okhslVariantToLinearRgb(baseVariant, ctx.config.pastel);
|
|
1716
|
+
const targetLinear = okhslVariantToLinearRgb(targetVariant, ctx.config.pastel);
|
|
1473
1717
|
if (def.contrast !== void 0) {
|
|
1474
|
-
const
|
|
1718
|
+
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast);
|
|
1719
|
+
const metric = resolvedContrast.metric;
|
|
1475
1720
|
let luminanceAt;
|
|
1476
|
-
if (blend === "transparent") luminanceAt = (v) =>
|
|
1477
|
-
else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1721
|
+
if (blend === "transparent" || space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1478
1722
|
else luminanceAt = (v) => {
|
|
1479
|
-
return
|
|
1723
|
+
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));
|
|
1480
1724
|
};
|
|
1481
1725
|
t = findValueForMixContrast({
|
|
1482
1726
|
preferredValue: t,
|
|
1483
1727
|
baseLinearRgb: baseLinear,
|
|
1484
1728
|
targetLinearRgb: targetLinear,
|
|
1485
|
-
contrast:
|
|
1729
|
+
contrast: resolvedContrast,
|
|
1486
1730
|
luminanceAtValue: luminanceAt,
|
|
1487
1731
|
flip: ctx.config.autoFlip
|
|
1488
1732
|
}).value;
|
|
1489
1733
|
}
|
|
1490
|
-
if (blend === "transparent") return {
|
|
1734
|
+
if (blend === "transparent") return toToneVariant({
|
|
1491
1735
|
h: targetVariant.h,
|
|
1492
1736
|
s: targetVariant.s,
|
|
1493
1737
|
l: targetVariant.l,
|
|
1494
1738
|
alpha: clamp(t, 0, 1)
|
|
1495
|
-
};
|
|
1496
|
-
if (space === "srgb") return
|
|
1497
|
-
return {
|
|
1739
|
+
});
|
|
1740
|
+
if (space === "srgb") return linearRgbToToneVariant(linearSrgbLerp(baseLinear, targetLinear, t), ctx.config.pastel);
|
|
1741
|
+
return toToneVariant({
|
|
1498
1742
|
h: mixHue(baseVariant, targetVariant, t),
|
|
1499
1743
|
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
1500
1744
|
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
1501
1745
|
alpha: 1
|
|
1502
|
-
};
|
|
1746
|
+
});
|
|
1503
1747
|
}
|
|
1504
1748
|
function defMode(def) {
|
|
1505
1749
|
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
@@ -1545,6 +1789,53 @@ function seedField(order, ctx, field, source) {
|
|
|
1545
1789
|
});
|
|
1546
1790
|
}
|
|
1547
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* After the four passes, surface chromatic contrast drift (§10): a color
|
|
1794
|
+
* resolved with a `base` + `contrast` may land slightly under the contrast
|
|
1795
|
+
* its tone implies because chromatic luminance drifts from the gray tone.
|
|
1796
|
+
*/
|
|
1797
|
+
function verifyContrastDrift(order, defs, result) {
|
|
1798
|
+
for (const name of order) {
|
|
1799
|
+
const def = defs[name];
|
|
1800
|
+
if (isShadowDef(def) || isMixDef(def)) continue;
|
|
1801
|
+
const regDef = def;
|
|
1802
|
+
if (regDef.contrast === void 0 || !regDef.base) continue;
|
|
1803
|
+
const color = result.get(name);
|
|
1804
|
+
const base = result.get(regDef.base);
|
|
1805
|
+
if (!color || !base) continue;
|
|
1806
|
+
for (const s of [
|
|
1807
|
+
{
|
|
1808
|
+
isDark: false,
|
|
1809
|
+
isHighContrast: false,
|
|
1810
|
+
field: "light"
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
isDark: false,
|
|
1814
|
+
isHighContrast: true,
|
|
1815
|
+
field: "lightContrast"
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
isDark: true,
|
|
1819
|
+
isHighContrast: false,
|
|
1820
|
+
field: "dark"
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
isDark: true,
|
|
1824
|
+
isHighContrast: true,
|
|
1825
|
+
field: "darkContrast"
|
|
1826
|
+
}
|
|
1827
|
+
]) {
|
|
1828
|
+
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast);
|
|
1829
|
+
const cVariant = color[s.field];
|
|
1830
|
+
const bVariant = base[s.field];
|
|
1831
|
+
const cOkhsl = toOkhslVariant(cVariant);
|
|
1832
|
+
const bOkhsl = toOkhslVariant(bVariant);
|
|
1833
|
+
const yC = metricLuminance(spec.metric, okhslToLinearSrgb(cOkhsl.h, cOkhsl.s, cOkhsl.l));
|
|
1834
|
+
const yB = metricLuminance(spec.metric, okhslToLinearSrgb(bOkhsl.h, bOkhsl.s, bOkhsl.l));
|
|
1835
|
+
warnContrastDrift(name, s.isDark, s.isHighContrast, spec, yC, yB);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1548
1839
|
function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
1549
1840
|
validateColorDefs(defs, externalBases);
|
|
1550
1841
|
const order = topoSort(defs);
|
|
@@ -1573,6 +1864,7 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
1573
1864
|
darkContrast: darkHCMap.get(name),
|
|
1574
1865
|
mode: defMode(defs[name])
|
|
1575
1866
|
});
|
|
1867
|
+
verifyContrastDrift(order, defs, result);
|
|
1576
1868
|
return result;
|
|
1577
1869
|
}
|
|
1578
1870
|
|
|
@@ -1597,8 +1889,9 @@ const formatters = {
|
|
|
1597
1889
|
function fmt(value, decimals) {
|
|
1598
1890
|
return parseFloat(value.toFixed(decimals)).toString();
|
|
1599
1891
|
}
|
|
1600
|
-
function formatVariant(v, format = "okhsl") {
|
|
1601
|
-
const
|
|
1892
|
+
function formatVariant(v, format = "okhsl", pastel = false) {
|
|
1893
|
+
const { l } = variantToOkhsl(v);
|
|
1894
|
+
const base = formatters[format](v.h, v.s * 100, l * 100, pastel);
|
|
1602
1895
|
if (v.alpha >= 1) return base;
|
|
1603
1896
|
const closing = base.lastIndexOf(")");
|
|
1604
1897
|
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
@@ -1610,44 +1903,44 @@ function resolveModes(override) {
|
|
|
1610
1903
|
highContrast: override?.highContrast ?? cfg.modes.highContrast
|
|
1611
1904
|
};
|
|
1612
1905
|
}
|
|
1613
|
-
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
|
|
1906
|
+
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false) {
|
|
1614
1907
|
const tokens = {};
|
|
1615
1908
|
for (const [name, color] of resolved) {
|
|
1616
1909
|
const key = `#${prefix}${name}`;
|
|
1617
|
-
const entry = { "": formatVariant(color.light, format) };
|
|
1618
|
-
if (modes.dark) entry[states.dark] = formatVariant(color.dark, format);
|
|
1619
|
-
if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format);
|
|
1620
|
-
if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format);
|
|
1910
|
+
const entry = { "": formatVariant(color.light, format, pastel) };
|
|
1911
|
+
if (modes.dark) entry[states.dark] = formatVariant(color.dark, format, pastel);
|
|
1912
|
+
if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format, pastel);
|
|
1913
|
+
if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format, pastel);
|
|
1621
1914
|
tokens[key] = entry;
|
|
1622
1915
|
}
|
|
1623
1916
|
return tokens;
|
|
1624
1917
|
}
|
|
1625
|
-
function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl") {
|
|
1918
|
+
function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl", pastel = false) {
|
|
1626
1919
|
const result = { light: {} };
|
|
1627
1920
|
if (modes.dark) result.dark = {};
|
|
1628
1921
|
if (modes.highContrast) result.lightContrast = {};
|
|
1629
1922
|
if (modes.dark && modes.highContrast) result.darkContrast = {};
|
|
1630
1923
|
for (const [name, color] of resolved) {
|
|
1631
1924
|
const key = `${prefix}${name}`;
|
|
1632
|
-
result.light[key] = formatVariant(color.light, format);
|
|
1633
|
-
if (modes.dark) result.dark[key] = formatVariant(color.dark, format);
|
|
1634
|
-
if (modes.highContrast) result.lightContrast[key] = formatVariant(color.lightContrast, format);
|
|
1635
|
-
if (modes.dark && modes.highContrast) result.darkContrast[key] = formatVariant(color.darkContrast, format);
|
|
1925
|
+
result.light[key] = formatVariant(color.light, format, pastel);
|
|
1926
|
+
if (modes.dark) result.dark[key] = formatVariant(color.dark, format, pastel);
|
|
1927
|
+
if (modes.highContrast) result.lightContrast[key] = formatVariant(color.lightContrast, format, pastel);
|
|
1928
|
+
if (modes.dark && modes.highContrast) result.darkContrast[key] = formatVariant(color.darkContrast, format, pastel);
|
|
1636
1929
|
}
|
|
1637
1930
|
return result;
|
|
1638
1931
|
}
|
|
1639
|
-
function buildJsonMap(resolved, modes, format = "okhsl") {
|
|
1932
|
+
function buildJsonMap(resolved, modes, format = "okhsl", pastel = false) {
|
|
1640
1933
|
const result = {};
|
|
1641
1934
|
for (const [name, color] of resolved) {
|
|
1642
|
-
const entry = { light: formatVariant(color.light, format) };
|
|
1643
|
-
if (modes.dark) entry.dark = formatVariant(color.dark, format);
|
|
1644
|
-
if (modes.highContrast) entry.lightContrast = formatVariant(color.lightContrast, format);
|
|
1645
|
-
if (modes.dark && modes.highContrast) entry.darkContrast = formatVariant(color.darkContrast, format);
|
|
1935
|
+
const entry = { light: formatVariant(color.light, format, pastel) };
|
|
1936
|
+
if (modes.dark) entry.dark = formatVariant(color.dark, format, pastel);
|
|
1937
|
+
if (modes.highContrast) entry.lightContrast = formatVariant(color.lightContrast, format, pastel);
|
|
1938
|
+
if (modes.dark && modes.highContrast) entry.darkContrast = formatVariant(color.darkContrast, format, pastel);
|
|
1646
1939
|
result[name] = entry;
|
|
1647
1940
|
}
|
|
1648
1941
|
return result;
|
|
1649
1942
|
}
|
|
1650
|
-
function buildCssMap(resolved, prefix, suffix, format) {
|
|
1943
|
+
function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
|
|
1651
1944
|
const lines = {
|
|
1652
1945
|
light: [],
|
|
1653
1946
|
dark: [],
|
|
@@ -1656,10 +1949,10 @@ function buildCssMap(resolved, prefix, suffix, format) {
|
|
|
1656
1949
|
};
|
|
1657
1950
|
for (const [name, color] of resolved) {
|
|
1658
1951
|
const prop = `--${prefix}${name}${suffix}`;
|
|
1659
|
-
lines.light.push(`${prop}: ${formatVariant(color.light, format)};`);
|
|
1660
|
-
lines.dark.push(`${prop}: ${formatVariant(color.dark, format)};`);
|
|
1661
|
-
lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format)};`);
|
|
1662
|
-
lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format)};`);
|
|
1952
|
+
lines.light.push(`${prop}: ${formatVariant(color.light, format, pastel)};`);
|
|
1953
|
+
lines.dark.push(`${prop}: ${formatVariant(color.dark, format, pastel)};`);
|
|
1954
|
+
lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format, pastel)};`);
|
|
1955
|
+
lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format, pastel)};`);
|
|
1663
1956
|
}
|
|
1664
1957
|
return {
|
|
1665
1958
|
light: lines.light.join("\n"),
|
|
@@ -1675,9 +1968,9 @@ function buildCssMap(resolved, prefix, suffix, format) {
|
|
|
1675
1968
|
* Standalone single-color tokens (`glaze.color()` / `glaze.colorFrom()`).
|
|
1676
1969
|
*
|
|
1677
1970
|
* Owns the value-shorthand parser (hex, `rgb()` / `hsl()` / `okhsl()` /
|
|
1678
|
-
* `oklch()`, `{ r, g, b }`, `{ h, s, l }`, `{
|
|
1679
|
-
* validator, the two factory paths
|
|
1680
|
-
* JSON-safe export / rehydration round-trip.
|
|
1971
|
+
* `okhst()` / `oklch()`, `{ r, g, b }`, `{ h, s, l }`, `{ h, s, t }`,
|
|
1972
|
+
* `{ l, c, h }`), the structured-input validator, the two factory paths
|
|
1973
|
+
* (value vs structured), and the JSON-safe export / rehydration round-trip.
|
|
1681
1974
|
*
|
|
1682
1975
|
* Standalone tokens snapshot the full effective config at create time
|
|
1683
1976
|
* so later `configure()` calls do not retroactively change exported
|
|
@@ -1688,7 +1981,7 @@ function buildCssMap(resolved, prefix, suffix, format) {
|
|
|
1688
1981
|
*/
|
|
1689
1982
|
/** Internal name of the user-facing standalone color in the synthesized def map. */
|
|
1690
1983
|
const STANDALONE_VALUE = "value";
|
|
1691
|
-
/** Internal name of the hidden static-anchor seed used for relative
|
|
1984
|
+
/** Internal name of the hidden static-anchor seed used for relative tone / contrast. */
|
|
1692
1985
|
const STANDALONE_SEED = "seed";
|
|
1693
1986
|
/** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
|
|
1694
1987
|
const STANDALONE_BASE = "externalBase";
|
|
@@ -1701,17 +1994,16 @@ const RESERVED_STANDALONE_NAMES = new Set([
|
|
|
1701
1994
|
/**
|
|
1702
1995
|
* Build the per-token effective config override for a value-form color.
|
|
1703
1996
|
*
|
|
1704
|
-
* Light window defaults to `false` (preserve input
|
|
1997
|
+
* Light window defaults to `false` (preserve input tone exactly).
|
|
1705
1998
|
* All other fields snapshot from global at create time. User override
|
|
1706
1999
|
* fields win over all defaults.
|
|
1707
2000
|
*/
|
|
1708
2001
|
function buildValueFormConfigOverride(userOverride) {
|
|
1709
2002
|
const cfg = getConfig();
|
|
1710
2003
|
return {
|
|
1711
|
-
|
|
1712
|
-
|
|
2004
|
+
lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : false,
|
|
2005
|
+
darkTone: userOverride?.darkTone !== void 0 ? userOverride.darkTone : cfg.darkTone,
|
|
1713
2006
|
darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
|
|
1714
|
-
darkCurve: userOverride?.darkCurve ?? cfg.darkCurve,
|
|
1715
2007
|
autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
|
|
1716
2008
|
shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
|
|
1717
2009
|
};
|
|
@@ -1725,10 +2017,9 @@ function buildValueFormConfigOverride(userOverride) {
|
|
|
1725
2017
|
function buildStructuredConfigOverride(userOverride) {
|
|
1726
2018
|
const cfg = getConfig();
|
|
1727
2019
|
return {
|
|
1728
|
-
|
|
1729
|
-
|
|
2020
|
+
lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : cfg.lightTone,
|
|
2021
|
+
darkTone: userOverride?.darkTone !== void 0 ? userOverride.darkTone : cfg.darkTone,
|
|
1730
2022
|
darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
|
|
1731
|
-
darkCurve: userOverride?.darkCurve ?? cfg.darkCurve,
|
|
1732
2023
|
autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
|
|
1733
2024
|
shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
|
|
1734
2025
|
};
|
|
@@ -1750,7 +2041,7 @@ function resolvedConfigFromOverride(override) {
|
|
|
1750
2041
|
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1751
2042
|
* are out of scope.
|
|
1752
2043
|
*/
|
|
1753
|
-
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
2044
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1754
2045
|
function parseNumberOrPercent(raw, percentScale) {
|
|
1755
2046
|
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1756
2047
|
return parseFloat(raw);
|
|
@@ -1831,6 +2122,11 @@ function parseColorString(input) {
|
|
|
1831
2122
|
s: parseNumberOrPercent(components[1], 1),
|
|
1832
2123
|
l: parseNumberOrPercent(components[2], 1)
|
|
1833
2124
|
};
|
|
2125
|
+
case "okhst": return okhstToOkhsl({
|
|
2126
|
+
h: parseFloat(components[0]),
|
|
2127
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
2128
|
+
t: parseNumberOrPercent(components[2], 1)
|
|
2129
|
+
});
|
|
1834
2130
|
case "oklch": {
|
|
1835
2131
|
const L = parseNumberOrPercent(components[0], 1);
|
|
1836
2132
|
const C = parseNumberOrPercent(components[1], .4);
|
|
@@ -1856,7 +2152,7 @@ function parseColorString(input) {
|
|
|
1856
2152
|
function validateOkhslColor(value) {
|
|
1857
2153
|
const { h, s, l } = value;
|
|
1858
2154
|
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1859
|
-
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation,
|
|
2155
|
+
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
1860
2156
|
}
|
|
1861
2157
|
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
1862
2158
|
function validateRgbColor(value) {
|
|
@@ -1894,6 +2190,15 @@ function isRgbColorObject(value) {
|
|
|
1894
2190
|
function isOklchColorObject(value) {
|
|
1895
2191
|
return "c" in value && "l" in value && "h" in value;
|
|
1896
2192
|
}
|
|
2193
|
+
function isOkhstColorObject(value) {
|
|
2194
|
+
return "t" in value && "h" in value && "s" in value;
|
|
2195
|
+
}
|
|
2196
|
+
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
2197
|
+
function validateOkhstColor(value) {
|
|
2198
|
+
const { h, s, t } = value;
|
|
2199
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
2200
|
+
if (s > 1.5 || t > 1.5) throw new Error("glaze.color: OkhstColor s/t must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
2201
|
+
}
|
|
1897
2202
|
/**
|
|
1898
2203
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
1899
2204
|
* Must be a finite number in `0..=1`.
|
|
@@ -1903,7 +2208,7 @@ function validateStandaloneOpacity(value) {
|
|
|
1903
2208
|
}
|
|
1904
2209
|
/**
|
|
1905
2210
|
* Validate a structured `GlazeColorInput`. Range-checks the `hue` /
|
|
1906
|
-
* `saturation` / `
|
|
2211
|
+
* `saturation` / `tone` numerics (and any HC-pair second value)
|
|
1907
2212
|
* before the resolver sees them so out-of-range or non-finite inputs
|
|
1908
2213
|
* fail with a helpful, top-level error rather than producing a
|
|
1909
2214
|
* NaN-laden token. `opacity` is checked here too so all input
|
|
@@ -1912,13 +2217,14 @@ function validateStandaloneOpacity(value) {
|
|
|
1912
2217
|
function validateStructuredInput(input) {
|
|
1913
2218
|
if (!Number.isFinite(input.hue)) throw new Error(`glaze.color: structured hue must be a finite number (got ${input.hue}).`);
|
|
1914
2219
|
if (!Number.isFinite(input.saturation) || input.saturation < 0 || input.saturation > 100) throw new Error(`glaze.color: structured saturation must be a finite number in 0–100 (got ${input.saturation}).`);
|
|
1915
|
-
const
|
|
1916
|
-
if (
|
|
2220
|
+
const checkTone = (value, label) => {
|
|
2221
|
+
if (value === "max" || value === "min") return;
|
|
2222
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) throw new Error(`glaze.color: structured ${label} must be a finite number in 0–100 or 'max'/'min' (got ${String(value)}).`);
|
|
1917
2223
|
};
|
|
1918
|
-
if (Array.isArray(input.
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
} else
|
|
2224
|
+
if (Array.isArray(input.tone)) {
|
|
2225
|
+
checkTone(input.tone[0], "tone[normal]");
|
|
2226
|
+
checkTone(input.tone[1], "tone[hc]");
|
|
2227
|
+
} else checkTone(input.tone, "tone");
|
|
1922
2228
|
if (input.saturationFactor !== void 0) {
|
|
1923
2229
|
if (!Number.isFinite(input.saturationFactor) || input.saturationFactor < 0 || input.saturationFactor > 1) throw new Error(`glaze.color: structured saturationFactor must be a finite number in 0–1 (got ${input.saturationFactor}).`);
|
|
1924
2230
|
}
|
|
@@ -1960,6 +2266,10 @@ function extractOkhslFromValue(value) {
|
|
|
1960
2266
|
validateOklchColor(value);
|
|
1961
2267
|
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
1962
2268
|
}
|
|
2269
|
+
if (isOkhstColorObject(value)) {
|
|
2270
|
+
validateOkhstColor(value);
|
|
2271
|
+
return okhstToOkhsl(value);
|
|
2272
|
+
}
|
|
1963
2273
|
validateOkhslColor(value);
|
|
1964
2274
|
return value;
|
|
1965
2275
|
}
|
|
@@ -1969,7 +2279,7 @@ function extractOkhslFromValue(value) {
|
|
|
1969
2279
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
1970
2280
|
* across every value-shorthand form.
|
|
1971
2281
|
*
|
|
1972
|
-
* When the user requests `contrast` or relative `
|
|
2282
|
+
* When the user requests `contrast` or relative `tone`, a hidden
|
|
1973
2283
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
1974
2284
|
* the seed pinned to the literal user-provided color across all four
|
|
1975
2285
|
* variants, so the contrast solver always anchors against it.
|
|
@@ -1978,19 +2288,21 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
1978
2288
|
const seedHue = typeof options?.hue === "number" ? options.hue : main.h;
|
|
1979
2289
|
const seedSaturation = options?.saturation ?? main.s * 100;
|
|
1980
2290
|
const relativeHue = typeof options?.hue === "string" ? options.hue : void 0;
|
|
1981
|
-
const
|
|
2291
|
+
const toneOption = options?.tone;
|
|
1982
2292
|
const hasExternalBase = options?.base !== void 0;
|
|
1983
|
-
const needsSeedAnchor = !hasExternalBase && (options?.contrast !== void 0 ||
|
|
2293
|
+
const needsSeedAnchor = !hasExternalBase && (options?.contrast !== void 0 || toneOption !== void 0 && !isAbsoluteTone(toneOption));
|
|
1984
2294
|
if (options?.opacity !== void 0) validateStandaloneOpacity(options.opacity);
|
|
1985
2295
|
const userName = options?.name;
|
|
1986
2296
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
1987
2297
|
const primary = userName ?? STANDALONE_VALUE;
|
|
2298
|
+
const seedTone = toTone(main.l);
|
|
1988
2299
|
const valueDef = {
|
|
1989
2300
|
hue: relativeHue,
|
|
1990
2301
|
saturation: options?.saturationFactor,
|
|
1991
|
-
|
|
2302
|
+
tone: toneOption ?? seedTone,
|
|
1992
2303
|
contrast: options?.contrast,
|
|
1993
2304
|
mode: options?.mode ?? "auto",
|
|
2305
|
+
flip: options?.flip,
|
|
1994
2306
|
opacity: options?.opacity,
|
|
1995
2307
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
1996
2308
|
};
|
|
@@ -1998,7 +2310,7 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
1998
2310
|
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
1999
2311
|
hue: main.h,
|
|
2000
2312
|
saturation: 1,
|
|
2001
|
-
|
|
2313
|
+
tone: seedTone,
|
|
2002
2314
|
mode: "static"
|
|
2003
2315
|
};
|
|
2004
2316
|
return {
|
|
@@ -2023,7 +2335,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
|
|
|
2023
2335
|
};
|
|
2024
2336
|
};
|
|
2025
2337
|
const tokenLike = (options) => {
|
|
2026
|
-
return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
|
|
2338
|
+
return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format, effectiveConfig.pastel)[`#${primary}`];
|
|
2027
2339
|
};
|
|
2028
2340
|
return {
|
|
2029
2341
|
resolve() {
|
|
@@ -2032,19 +2344,19 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
|
|
|
2032
2344
|
token: tokenLike,
|
|
2033
2345
|
tasty: tokenLike,
|
|
2034
2346
|
json(options) {
|
|
2035
|
-
return buildJsonMap(resolveOnce(), resolveModes(options?.modes), options?.format)[primary];
|
|
2347
|
+
return buildJsonMap(resolveOnce(), resolveModes(options?.modes), options?.format, effectiveConfig.pastel)[primary];
|
|
2036
2348
|
},
|
|
2037
2349
|
css(options) {
|
|
2038
|
-
return buildCssMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb");
|
|
2350
|
+
return buildCssMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb", effectiveConfig.pastel);
|
|
2039
2351
|
},
|
|
2040
2352
|
export: exportData
|
|
2041
2353
|
};
|
|
2042
2354
|
}
|
|
2043
2355
|
/**
|
|
2044
2356
|
* When a value/`from` color links to a base that was created via the
|
|
2045
|
-
* structured form (with explicit `hue`/`saturation`/`
|
|
2046
|
-
* that base with `
|
|
2047
|
-
* contrast/
|
|
2357
|
+
* structured form (with explicit `hue`/`saturation`/`tone`), resolve
|
|
2358
|
+
* that base with `lightTone: false` for the linking math so the
|
|
2359
|
+
* contrast/tone anchor matches the input tone — not the
|
|
2048
2360
|
* windowed output. The original base token's `.resolve()` is unaffected.
|
|
2049
2361
|
*/
|
|
2050
2362
|
function toLinkingBase(base) {
|
|
@@ -2053,7 +2365,7 @@ function toLinkingBase(base) {
|
|
|
2053
2365
|
if (exp.form !== "structured") return base;
|
|
2054
2366
|
const linkingConfig = {
|
|
2055
2367
|
...exp.config ?? {},
|
|
2056
|
-
|
|
2368
|
+
lightTone: false
|
|
2057
2369
|
};
|
|
2058
2370
|
return colorFromExport({
|
|
2059
2371
|
...exp,
|
|
@@ -2086,18 +2398,22 @@ function createColorToken(input, configOverride) {
|
|
|
2086
2398
|
const hasExternalBase = baseToken !== void 0;
|
|
2087
2399
|
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
2088
2400
|
const defs = { [primary]: {
|
|
2089
|
-
|
|
2401
|
+
tone: input.tone,
|
|
2090
2402
|
saturation: input.saturationFactor,
|
|
2091
2403
|
mode: input.mode ?? "auto",
|
|
2404
|
+
flip: input.flip,
|
|
2092
2405
|
contrast: input.contrast,
|
|
2093
2406
|
opacity: input.opacity,
|
|
2094
2407
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
2095
2408
|
} };
|
|
2096
|
-
if (needsSeedAnchor)
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2409
|
+
if (needsSeedAnchor) {
|
|
2410
|
+
const seedTone = pairNormal(input.tone);
|
|
2411
|
+
defs[STANDALONE_SEED] = {
|
|
2412
|
+
tone: seedTone === "max" ? 100 : seedTone === "min" ? 0 : seedTone,
|
|
2413
|
+
saturation: 1,
|
|
2414
|
+
mode: "static"
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2101
2417
|
const effectiveConfigOverride = buildStructuredConfigOverride(configOverride);
|
|
2102
2418
|
const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
|
|
2103
2419
|
const exportData = () => ({
|
|
@@ -2130,9 +2446,10 @@ function buildOverridesExport(options) {
|
|
|
2130
2446
|
const out = {};
|
|
2131
2447
|
if (options.hue !== void 0) out.hue = options.hue;
|
|
2132
2448
|
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2133
|
-
if (options.
|
|
2449
|
+
if (options.tone !== void 0) out.tone = options.tone;
|
|
2134
2450
|
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2135
2451
|
if (options.mode !== void 0) out.mode = options.mode;
|
|
2452
|
+
if (options.flip !== void 0) out.flip = options.flip;
|
|
2136
2453
|
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2137
2454
|
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2138
2455
|
if (options.name !== void 0) out.name = options.name;
|
|
@@ -2143,10 +2460,11 @@ function buildStructuredInputExport(input) {
|
|
|
2143
2460
|
const out = {
|
|
2144
2461
|
hue: input.hue,
|
|
2145
2462
|
saturation: input.saturation,
|
|
2146
|
-
|
|
2463
|
+
tone: input.tone
|
|
2147
2464
|
};
|
|
2148
2465
|
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2149
2466
|
if (input.mode !== void 0) out.mode = input.mode;
|
|
2467
|
+
if (input.flip !== void 0) out.flip = input.flip;
|
|
2150
2468
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2151
2469
|
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2152
2470
|
if (input.name !== void 0) out.name = input.name;
|
|
@@ -2163,9 +2481,10 @@ function rehydrateOverrides(data) {
|
|
|
2163
2481
|
const out = {};
|
|
2164
2482
|
if (data.hue !== void 0) out.hue = data.hue;
|
|
2165
2483
|
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
2166
|
-
if (data.
|
|
2484
|
+
if (data.tone !== void 0) out.tone = data.tone;
|
|
2167
2485
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
2168
2486
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
2487
|
+
if (data.flip !== void 0) out.flip = data.flip;
|
|
2169
2488
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2170
2489
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2171
2490
|
if (data.name !== void 0) out.name = data.name;
|
|
@@ -2176,10 +2495,11 @@ function rehydrateStructuredInput(data) {
|
|
|
2176
2495
|
const out = {
|
|
2177
2496
|
hue: data.hue,
|
|
2178
2497
|
saturation: data.saturation,
|
|
2179
|
-
|
|
2498
|
+
tone: data.tone
|
|
2180
2499
|
};
|
|
2181
2500
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
2182
2501
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
2502
|
+
if (data.flip !== void 0) out.flip = data.flip;
|
|
2183
2503
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2184
2504
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2185
2505
|
if (data.name !== void 0) out.name = data.name;
|
|
@@ -2267,9 +2587,10 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
|
|
|
2267
2587
|
const seen = /* @__PURE__ */ new Map();
|
|
2268
2588
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
2269
2589
|
const resolved = theme.resolve();
|
|
2590
|
+
const pastel = theme.getConfig().pastel;
|
|
2270
2591
|
const prefix = resolvePrefix(options, themeName, true);
|
|
2271
|
-
merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix));
|
|
2272
|
-
if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), ""));
|
|
2592
|
+
merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix, pastel));
|
|
2593
|
+
if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), "", pastel));
|
|
2273
2594
|
}
|
|
2274
2595
|
return acc;
|
|
2275
2596
|
}
|
|
@@ -2278,7 +2599,7 @@ function createPalette(themes, paletteOptions) {
|
|
|
2278
2599
|
return {
|
|
2279
2600
|
tokens(options) {
|
|
2280
2601
|
const modes = resolveModes(options?.modes);
|
|
2281
|
-
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildFlatTokenMap(filtered, prefix, modes, options?.format), (acc, part) => {
|
|
2602
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildFlatTokenMap(filtered, prefix, modes, options?.format, pastel), (acc, part) => {
|
|
2282
2603
|
for (const variant of Object.keys(part)) {
|
|
2283
2604
|
if (!acc[variant]) acc[variant] = {};
|
|
2284
2605
|
Object.assign(acc[variant], part[variant]);
|
|
@@ -2292,18 +2613,18 @@ function createPalette(themes, paletteOptions) {
|
|
|
2292
2613
|
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2293
2614
|
};
|
|
2294
2615
|
const modes = resolveModes(options?.modes);
|
|
2295
|
-
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildTokenMap(filtered, prefix, states, modes, options?.format), (acc, part) => Object.assign(acc, part), () => ({}));
|
|
2616
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildTokenMap(filtered, prefix, states, modes, options?.format, pastel), (acc, part) => Object.assign(acc, part), () => ({}));
|
|
2296
2617
|
},
|
|
2297
2618
|
json(options) {
|
|
2298
2619
|
const modes = resolveModes(options?.modes);
|
|
2299
2620
|
const result = {};
|
|
2300
|
-
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
2621
|
+
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format, theme.getConfig().pastel);
|
|
2301
2622
|
return result;
|
|
2302
2623
|
},
|
|
2303
2624
|
css(options) {
|
|
2304
2625
|
const suffix = options?.suffix ?? "-color";
|
|
2305
2626
|
const format = options?.format ?? "rgb";
|
|
2306
|
-
const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildCssMap(filtered, prefix, suffix, format), (acc, part) => {
|
|
2627
|
+
const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildCssMap(filtered, prefix, suffix, format, pastel), (acc, part) => {
|
|
2307
2628
|
for (const key of [
|
|
2308
2629
|
"light",
|
|
2309
2630
|
"dark",
|
|
@@ -2370,6 +2691,9 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2370
2691
|
get saturation() {
|
|
2371
2692
|
return saturation;
|
|
2372
2693
|
},
|
|
2694
|
+
getConfig() {
|
|
2695
|
+
return getEffectiveConfig();
|
|
2696
|
+
},
|
|
2373
2697
|
colors(defs) {
|
|
2374
2698
|
colorDefs = {
|
|
2375
2699
|
...colorDefs,
|
|
@@ -2424,7 +2748,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2424
2748
|
},
|
|
2425
2749
|
tokens(options) {
|
|
2426
2750
|
const modes = resolveModes(options?.modes);
|
|
2427
|
-
return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
|
|
2751
|
+
return buildFlatTokenMap(resolveCached(), "", modes, options?.format, getEffectiveConfig().pastel);
|
|
2428
2752
|
},
|
|
2429
2753
|
tasty(options) {
|
|
2430
2754
|
const cfg = getEffectiveConfig();
|
|
@@ -2433,14 +2757,14 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2433
2757
|
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2434
2758
|
};
|
|
2435
2759
|
const modes = resolveModes(options?.modes);
|
|
2436
|
-
return buildTokenMap(resolveCached(), "", states, modes, options?.format);
|
|
2760
|
+
return buildTokenMap(resolveCached(), "", states, modes, options?.format, cfg.pastel);
|
|
2437
2761
|
},
|
|
2438
2762
|
json(options) {
|
|
2439
2763
|
const modes = resolveModes(options?.modes);
|
|
2440
|
-
return buildJsonMap(resolveCached(), modes, options?.format);
|
|
2764
|
+
return buildJsonMap(resolveCached(), modes, options?.format, getEffectiveConfig().pastel);
|
|
2441
2765
|
},
|
|
2442
2766
|
css(options) {
|
|
2443
|
-
return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
2767
|
+
return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb", getEffectiveConfig().pastel);
|
|
2444
2768
|
}
|
|
2445
2769
|
};
|
|
2446
2770
|
}
|
|
@@ -2448,7 +2772,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2448
2772
|
//#endregion
|
|
2449
2773
|
//#region src/glaze.ts
|
|
2450
2774
|
/**
|
|
2451
|
-
* Glaze —
|
|
2775
|
+
* Glaze — OKHST color theme generator.
|
|
2452
2776
|
*
|
|
2453
2777
|
* Public API entry. Wires `glaze()` and its attached static methods to
|
|
2454
2778
|
* the focused modules in this folder:
|
|
@@ -2463,7 +2787,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2463
2787
|
* Create a single-hue glaze theme.
|
|
2464
2788
|
*
|
|
2465
2789
|
* An optional `config` override can be supplied to customize the resolve
|
|
2466
|
-
* behavior for this theme (
|
|
2790
|
+
* behavior for this theme (tone windows, etc.). The
|
|
2467
2791
|
* override is **merged over the live global config at resolve time** —
|
|
2468
2792
|
* the theme still reacts to later `configure()` calls for fields it
|
|
2469
2793
|
* didn't override.
|
|
@@ -2474,7 +2798,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2474
2798
|
* // or shorthand:
|
|
2475
2799
|
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
2476
2800
|
* // with config override:
|
|
2477
|
-
* const raw = glaze(280, 80, {
|
|
2801
|
+
* const raw = glaze(280, 80, { lightTone: false });
|
|
2478
2802
|
* ```
|
|
2479
2803
|
*/
|
|
2480
2804
|
function glaze(hueOrOptions, saturation, config) {
|
|
@@ -2500,15 +2824,15 @@ glaze.from = function from(data) {
|
|
|
2500
2824
|
*
|
|
2501
2825
|
* | Shape | Example | Notes |
|
|
2502
2826
|
* |---|---|---|
|
|
2503
|
-
* | Bare string | `'#26fcb2'`, `'rgb(38 252 178)'` | Hex or CSS color function |
|
|
2504
|
-
* | Value object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, `{r,g,b}`, `{l,c,h}` |
|
|
2827
|
+
* | Bare string | `'#26fcb2'`, `'rgb(38 252 178)'` | Hex or CSS color function (incl. `okhst()`) |
|
|
2828
|
+
* | Value object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, OKHST (`{h,s,t}`), `{r,g,b}`, `{l,c,h}` |
|
|
2505
2829
|
* | `{ from, ...overrides }` | `{ from: '#fff', base: bg, contrast: 'AA' }` | Value + color overrides |
|
|
2506
|
-
* | Structured | `{ hue: 152, saturation: 95,
|
|
2830
|
+
* | Structured | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token |
|
|
2507
2831
|
*
|
|
2508
2832
|
* **arg2 — config override** (optional, all shapes):
|
|
2509
2833
|
* Overrides the resolve-relevant global config fields for this token.
|
|
2510
2834
|
* Fields that are omitted fall through to the live global config at
|
|
2511
|
-
* create time (and are snapshotted). Pass `false` for a
|
|
2835
|
+
* create time (and are snapshotted). Pass `false` for a tone window
|
|
2512
2836
|
* to disable clamping entirely.
|
|
2513
2837
|
*
|
|
2514
2838
|
* ```ts
|
|
@@ -2519,19 +2843,19 @@ glaze.from = function from(data) {
|
|
|
2519
2843
|
* glaze.color({ from: '#fff', base: bg, contrast: 'AA' })
|
|
2520
2844
|
*
|
|
2521
2845
|
* // Structured form — full theme-style token
|
|
2522
|
-
* glaze.color({ hue: 152, saturation: 95,
|
|
2846
|
+
* glaze.color({ hue: 152, saturation: 95, tone: 74 })
|
|
2523
2847
|
*
|
|
2524
2848
|
* // Config override on any form
|
|
2525
|
-
* glaze.color('#26fcb2', {
|
|
2526
|
-
* glaze.color({ from: '#fff', base: bg }
|
|
2849
|
+
* glaze.color('#26fcb2', { darkTone: false, autoFlip: false })
|
|
2850
|
+
* glaze.color({ from: '#fff', base: bg })
|
|
2527
2851
|
* ```
|
|
2528
2852
|
*
|
|
2529
2853
|
* Defaults: every form defaults to `mode: 'auto'`. Value-shorthand forms
|
|
2530
|
-
* (bare strings and value objects) preserve light
|
|
2531
|
-
* (`
|
|
2532
|
-
*
|
|
2854
|
+
* (bare strings and value objects) preserve light tone exactly
|
|
2855
|
+
* (`lightTone: false` internally). Structured form snapshots both
|
|
2856
|
+
* tone windows from `globalConfig` at create time.
|
|
2533
2857
|
*
|
|
2534
|
-
* Relative `
|
|
2858
|
+
* Relative `tone: '+N'` and `contrast` anchor to the literal seed by
|
|
2535
2859
|
* default; when `base` is set they anchor to the base's resolved variant
|
|
2536
2860
|
* per scheme. Relative `hue: '+N'` always anchors to the seed, not the base.
|
|
2537
2861
|
*/
|
|
@@ -2557,17 +2881,28 @@ glaze.shadow = function shadow(input) {
|
|
|
2557
2881
|
const fg = input.fg ? extractOkhslFromValue(input.fg) : void 0;
|
|
2558
2882
|
const cfg = getConfig();
|
|
2559
2883
|
const tuning = resolveShadowTuning(input.tuning, cfg.shadowTuning);
|
|
2560
|
-
|
|
2884
|
+
const result = computeShadow({
|
|
2561
2885
|
...bg,
|
|
2562
2886
|
alpha: 1
|
|
2563
2887
|
}, fg ? {
|
|
2564
2888
|
...fg,
|
|
2565
2889
|
alpha: 1
|
|
2566
2890
|
} : void 0, input.intensity, tuning);
|
|
2891
|
+
const { h, s, t } = okhslToOkhst({
|
|
2892
|
+
h: result.h,
|
|
2893
|
+
s: result.s,
|
|
2894
|
+
l: result.l
|
|
2895
|
+
});
|
|
2896
|
+
return {
|
|
2897
|
+
h,
|
|
2898
|
+
s,
|
|
2899
|
+
t,
|
|
2900
|
+
alpha: result.alpha
|
|
2901
|
+
};
|
|
2567
2902
|
};
|
|
2568
2903
|
/** Format a resolved color variant as a CSS string. */
|
|
2569
|
-
glaze.format = function format(variant, colorFormat) {
|
|
2570
|
-
return formatVariant(variant, colorFormat);
|
|
2904
|
+
glaze.format = function format(variant, colorFormat, pastel) {
|
|
2905
|
+
return formatVariant(variant, colorFormat, pastel);
|
|
2571
2906
|
};
|
|
2572
2907
|
/**
|
|
2573
2908
|
* Create a theme from a hex color string.
|
|
@@ -2621,5 +2956,5 @@ glaze.resetConfig = function resetConfig$1() {
|
|
|
2621
2956
|
};
|
|
2622
2957
|
|
|
2623
2958
|
//#endregion
|
|
2624
|
-
export { contrastRatioFromLuminance,
|
|
2959
|
+
export { REF_EPS, apcaContrast, contrastRatioFromLuminance, cuspLightness, findToneForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, fromTone, gamutClampedLuminance, glaze, hslToSrgb, okhslToLinearSrgb, okhslToOkhst, okhslToOklab, okhslToSrgb, okhstToOkhsl, oklabToOkhsl, parseHex, parseHexAlpha, relativeLuminanceFromLinearRgb, resolveContrastForMode, resolveMinContrast, srgbToOkhsl, toTone, toneFromY, variantToOkhsl, yFromTone };
|
|
2625
2960
|
//# sourceMappingURL=index.mjs.map
|