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