@tenphi/glaze 0.0.0-snapshot.4c063ef → 0.0.0-snapshot.709df23
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 +208 -23
- package/dist/index.cjs +308 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -22
- package/dist/index.d.mts +116 -22
- package/dist/index.mjs +307 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -81,8 +81,8 @@ const OKLab_to_linear_sRGB_coefficients = [
|
|
|
81
81
|
.73956515,
|
|
82
82
|
-.45954404,
|
|
83
83
|
.08285427,
|
|
84
|
-
.
|
|
85
|
-
|
|
84
|
+
.1254107,
|
|
85
|
+
.14503204
|
|
86
86
|
]],
|
|
87
87
|
[[.13110757611180954, 1.813339709266608], [
|
|
88
88
|
1.35733652,
|
|
@@ -254,10 +254,9 @@ const getCs = (L, a, b, cusp) => {
|
|
|
254
254
|
];
|
|
255
255
|
};
|
|
256
256
|
/**
|
|
257
|
-
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to
|
|
258
|
-
* Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
|
|
257
|
+
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
|
|
259
258
|
*/
|
|
260
|
-
function
|
|
259
|
+
function okhslToOklab(h, s, l) {
|
|
261
260
|
const L = toeInv(l);
|
|
262
261
|
let a = 0;
|
|
263
262
|
let b = 0;
|
|
@@ -284,11 +283,18 @@ function okhslToLinearSrgb(h, s, l) {
|
|
|
284
283
|
a = c * a_;
|
|
285
284
|
b = c * b_;
|
|
286
285
|
}
|
|
287
|
-
return
|
|
286
|
+
return [
|
|
288
287
|
L,
|
|
289
288
|
a,
|
|
290
289
|
b
|
|
291
|
-
]
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
|
|
294
|
+
* Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
|
|
295
|
+
*/
|
|
296
|
+
function okhslToLinearSrgb(h, s, l) {
|
|
297
|
+
return OKLabToLinearSRGB(okhslToOklab(h, s, l));
|
|
292
298
|
}
|
|
293
299
|
/**
|
|
294
300
|
* Compute relative luminance Y from linear sRGB channels.
|
|
@@ -327,40 +333,15 @@ function okhslToSrgb(h, s, l) {
|
|
|
327
333
|
];
|
|
328
334
|
}
|
|
329
335
|
/**
|
|
330
|
-
*
|
|
336
|
+
* Compute WCAG 2 relative luminance from linear sRGB, matching the browser
|
|
337
|
+
* rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
|
|
338
|
+
* This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
|
|
331
339
|
*/
|
|
332
|
-
function
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (L !== 0 && L !== 1 && s !== 0) {
|
|
338
|
-
const a_ = Math.cos(TAU * hNorm);
|
|
339
|
-
const b_ = Math.sin(TAU * hNorm);
|
|
340
|
-
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
341
|
-
const mid = .8;
|
|
342
|
-
const midInv = 1.25;
|
|
343
|
-
let t, k0, k1, k2;
|
|
344
|
-
if (s < mid) {
|
|
345
|
-
t = midInv * s;
|
|
346
|
-
k0 = 0;
|
|
347
|
-
k1 = mid * c0;
|
|
348
|
-
k2 = 1 - k1 / cMid;
|
|
349
|
-
} else {
|
|
350
|
-
t = 5 * (s - .8);
|
|
351
|
-
k0 = cMid;
|
|
352
|
-
k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
|
|
353
|
-
k2 = 1 - k1 / (cMax - cMid);
|
|
354
|
-
}
|
|
355
|
-
const c = k0 + t * k1 / (1 - k2 * t);
|
|
356
|
-
a = c * a_;
|
|
357
|
-
b = c * b_;
|
|
358
|
-
}
|
|
359
|
-
return [
|
|
360
|
-
L,
|
|
361
|
-
a,
|
|
362
|
-
b
|
|
363
|
-
];
|
|
340
|
+
function gamutClampedLuminance(linearRgb) {
|
|
341
|
+
const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
|
|
342
|
+
const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
|
|
343
|
+
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
344
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
364
345
|
}
|
|
365
346
|
const linearSrgbToOklab = (rgb) => {
|
|
366
347
|
return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
|
|
@@ -449,15 +430,16 @@ function fmt$1(value, decimals) {
|
|
|
449
430
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
450
431
|
*/
|
|
451
432
|
function formatOkhsl(h, s, l) {
|
|
452
|
-
return `okhsl(${fmt$1(h,
|
|
433
|
+
return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
|
|
453
434
|
}
|
|
454
435
|
/**
|
|
455
|
-
* Format OKHSL values as a CSS `rgb(R G B)` string
|
|
436
|
+
* Format OKHSL values as a CSS `rgb(R G B)` string.
|
|
437
|
+
* Uses 2 decimal places to avoid 8-bit quantization contrast loss.
|
|
456
438
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
457
439
|
*/
|
|
458
440
|
function formatRgb(h, s, l) {
|
|
459
441
|
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
|
|
460
|
-
return `rgb(${
|
|
442
|
+
return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
|
|
461
443
|
}
|
|
462
444
|
/**
|
|
463
445
|
* Format OKHSL values as a CSS `hsl(H S% L%)` string.
|
|
@@ -477,7 +459,7 @@ function formatHsl(h, s, l) {
|
|
|
477
459
|
else if (max === g) hh = ((b - r) / delta + 2) * 60;
|
|
478
460
|
else hh = ((r - g) / delta + 4) * 60;
|
|
479
461
|
}
|
|
480
|
-
return `hsl(${fmt$1(hh,
|
|
462
|
+
return `hsl(${fmt$1(hh, 2)} ${fmt$1(ss * 100, 2)}% ${fmt$1(ll * 100, 2)}%)`;
|
|
481
463
|
}
|
|
482
464
|
/**
|
|
483
465
|
* Format OKHSL values as a CSS `oklch(L C H)` string.
|
|
@@ -488,7 +470,7 @@ function formatOklch(h, s, l) {
|
|
|
488
470
|
const C = Math.sqrt(a * a + b * b);
|
|
489
471
|
let hh = Math.atan2(b, a) * (180 / Math.PI);
|
|
490
472
|
hh = constrainAngle(hh);
|
|
491
|
-
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh,
|
|
473
|
+
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
492
474
|
}
|
|
493
475
|
|
|
494
476
|
//#endregion
|
|
@@ -518,7 +500,7 @@ function cachedLuminance(h, s, l) {
|
|
|
518
500
|
const key = `${h}|${s}|${lRounded}`;
|
|
519
501
|
const cached = luminanceCache.get(key);
|
|
520
502
|
if (cached !== void 0) return cached;
|
|
521
|
-
const y =
|
|
503
|
+
const y = gamutClampedLuminance(okhslToLinearSrgb(h, s, lRounded));
|
|
522
504
|
if (luminanceCache.size >= CACHE_SIZE) {
|
|
523
505
|
const evict = cacheOrder.shift();
|
|
524
506
|
luminanceCache.delete(evict);
|
|
@@ -638,17 +620,20 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
|
638
620
|
function findLightnessForContrast(options) {
|
|
639
621
|
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
640
622
|
const target = resolveMinContrast(contrastInput);
|
|
641
|
-
const
|
|
623
|
+
const searchTarget = target * 1.007;
|
|
624
|
+
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
642
625
|
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
643
|
-
if (crPref >=
|
|
626
|
+
if (crPref >= searchTarget) return {
|
|
644
627
|
lightness: preferredLightness,
|
|
645
628
|
contrast: crPref,
|
|
646
629
|
met: true,
|
|
647
630
|
branch: "preferred"
|
|
648
631
|
};
|
|
649
632
|
const [minL, maxL] = lightnessRange;
|
|
650
|
-
const darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase,
|
|
651
|
-
const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase,
|
|
633
|
+
const darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
|
|
634
|
+
const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
|
|
635
|
+
if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
|
|
636
|
+
if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
|
|
652
637
|
const darkerPasses = darkerResult?.met ?? false;
|
|
653
638
|
const lighterPasses = lighterResult?.met ?? false;
|
|
654
639
|
if (darkerPasses && lighterPasses) {
|
|
@@ -687,6 +672,135 @@ function findLightnessForContrast(options) {
|
|
|
687
672
|
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
688
673
|
return candidates[0];
|
|
689
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Binary-search one branch [lo, hi] for the nearest passing mix value
|
|
677
|
+
* to `preferred`.
|
|
678
|
+
*/
|
|
679
|
+
function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
|
|
680
|
+
const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
|
|
681
|
+
const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
|
|
682
|
+
if (crLo < target && crHi < target) {
|
|
683
|
+
if (crLo >= crHi) return {
|
|
684
|
+
lightness: lo,
|
|
685
|
+
contrast: crLo,
|
|
686
|
+
met: false
|
|
687
|
+
};
|
|
688
|
+
return {
|
|
689
|
+
lightness: hi,
|
|
690
|
+
contrast: crHi,
|
|
691
|
+
met: false
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
let low = lo;
|
|
695
|
+
let high = hi;
|
|
696
|
+
for (let i = 0; i < maxIter; i++) {
|
|
697
|
+
if (high - low < epsilon) break;
|
|
698
|
+
const mid = (low + high) / 2;
|
|
699
|
+
if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
|
|
700
|
+
else high = mid;
|
|
701
|
+
else if (mid < preferred) high = mid;
|
|
702
|
+
else low = mid;
|
|
703
|
+
}
|
|
704
|
+
const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
|
|
705
|
+
const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
|
|
706
|
+
const lowPasses = crLow >= target;
|
|
707
|
+
const highPasses = crHigh >= target;
|
|
708
|
+
if (lowPasses && highPasses) {
|
|
709
|
+
if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
|
|
710
|
+
lightness: low,
|
|
711
|
+
contrast: crLow,
|
|
712
|
+
met: true
|
|
713
|
+
};
|
|
714
|
+
return {
|
|
715
|
+
lightness: high,
|
|
716
|
+
contrast: crHigh,
|
|
717
|
+
met: true
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
if (lowPasses) return {
|
|
721
|
+
lightness: low,
|
|
722
|
+
contrast: crLow,
|
|
723
|
+
met: true
|
|
724
|
+
};
|
|
725
|
+
if (highPasses) return {
|
|
726
|
+
lightness: high,
|
|
727
|
+
contrast: crHigh,
|
|
728
|
+
met: true
|
|
729
|
+
};
|
|
730
|
+
return crLow >= crHigh ? {
|
|
731
|
+
lightness: low,
|
|
732
|
+
contrast: crLow,
|
|
733
|
+
met: false
|
|
734
|
+
} : {
|
|
735
|
+
lightness: high,
|
|
736
|
+
contrast: crHigh,
|
|
737
|
+
met: false
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
|
|
742
|
+
* target against a base color, staying as close to `preferredValue` as possible.
|
|
743
|
+
*/
|
|
744
|
+
function findValueForMixContrast(options) {
|
|
745
|
+
const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
746
|
+
const target = resolveMinContrast(contrastInput);
|
|
747
|
+
const searchTarget = target * 1.01;
|
|
748
|
+
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
749
|
+
const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
|
|
750
|
+
if (crPref >= searchTarget) return {
|
|
751
|
+
value: preferredValue,
|
|
752
|
+
contrast: crPref,
|
|
753
|
+
met: true
|
|
754
|
+
};
|
|
755
|
+
const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
756
|
+
const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
757
|
+
if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
|
|
758
|
+
if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
|
|
759
|
+
const darkerPasses = darkerResult?.met ?? false;
|
|
760
|
+
const lighterPasses = lighterResult?.met ?? false;
|
|
761
|
+
if (darkerPasses && lighterPasses) {
|
|
762
|
+
if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
|
|
763
|
+
value: darkerResult.lightness,
|
|
764
|
+
contrast: darkerResult.contrast,
|
|
765
|
+
met: true
|
|
766
|
+
};
|
|
767
|
+
return {
|
|
768
|
+
value: lighterResult.lightness,
|
|
769
|
+
contrast: lighterResult.contrast,
|
|
770
|
+
met: true
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (darkerPasses) return {
|
|
774
|
+
value: darkerResult.lightness,
|
|
775
|
+
contrast: darkerResult.contrast,
|
|
776
|
+
met: true
|
|
777
|
+
};
|
|
778
|
+
if (lighterPasses) return {
|
|
779
|
+
value: lighterResult.lightness,
|
|
780
|
+
contrast: lighterResult.contrast,
|
|
781
|
+
met: true
|
|
782
|
+
};
|
|
783
|
+
const candidates = [];
|
|
784
|
+
if (darkerResult) candidates.push({
|
|
785
|
+
...darkerResult,
|
|
786
|
+
branch: "lower"
|
|
787
|
+
});
|
|
788
|
+
if (lighterResult) candidates.push({
|
|
789
|
+
...lighterResult,
|
|
790
|
+
branch: "upper"
|
|
791
|
+
});
|
|
792
|
+
if (candidates.length === 0) return {
|
|
793
|
+
value: preferredValue,
|
|
794
|
+
contrast: crPref,
|
|
795
|
+
met: false
|
|
796
|
+
};
|
|
797
|
+
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
798
|
+
return {
|
|
799
|
+
value: candidates[0].lightness,
|
|
800
|
+
contrast: candidates[0].contrast,
|
|
801
|
+
met: candidates[0].met
|
|
802
|
+
};
|
|
803
|
+
}
|
|
690
804
|
|
|
691
805
|
//#endregion
|
|
692
806
|
//#region src/glaze.ts
|
|
@@ -718,6 +832,9 @@ function pairHC(p) {
|
|
|
718
832
|
function isShadowDef(def) {
|
|
719
833
|
return def.type === "shadow";
|
|
720
834
|
}
|
|
835
|
+
function isMixDef(def) {
|
|
836
|
+
return def.type === "mix";
|
|
837
|
+
}
|
|
721
838
|
const DEFAULT_SHADOW_TUNING = {
|
|
722
839
|
saturationFactor: .18,
|
|
723
840
|
maxSaturation: .25,
|
|
@@ -785,10 +902,16 @@ function validateColorDefs(defs) {
|
|
|
785
902
|
}
|
|
786
903
|
continue;
|
|
787
904
|
}
|
|
905
|
+
if (isMixDef(def)) {
|
|
906
|
+
if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
907
|
+
if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
908
|
+
if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
909
|
+
if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
788
912
|
const regDef = def;
|
|
789
913
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
790
914
|
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
791
|
-
if (isAbsoluteLightness(regDef.lightness) && regDef.base !== void 0) console.warn(`glaze: color "${name}" has absolute "lightness" and "base". Absolute lightness takes precedence.`);
|
|
792
915
|
if (regDef.base && !names.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
793
916
|
if (regDef.base && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
794
917
|
if (!isAbsoluteLightness(regDef.lightness) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
|
|
@@ -804,6 +927,9 @@ function validateColorDefs(defs) {
|
|
|
804
927
|
if (isShadowDef(def)) {
|
|
805
928
|
dfs(def.bg);
|
|
806
929
|
if (def.fg) dfs(def.fg);
|
|
930
|
+
} else if (isMixDef(def)) {
|
|
931
|
+
dfs(def.base);
|
|
932
|
+
dfs(def.target);
|
|
807
933
|
} else {
|
|
808
934
|
const regDef = def;
|
|
809
935
|
if (regDef.base) dfs(regDef.base);
|
|
@@ -823,6 +949,9 @@ function topoSort(defs) {
|
|
|
823
949
|
if (isShadowDef(def)) {
|
|
824
950
|
visit(def.bg);
|
|
825
951
|
if (def.fg) visit(def.fg);
|
|
952
|
+
} else if (isMixDef(def)) {
|
|
953
|
+
visit(def.base);
|
|
954
|
+
visit(def.target);
|
|
826
955
|
} else {
|
|
827
956
|
const regDef = def;
|
|
828
957
|
if (regDef.base) visit(regDef.base);
|
|
@@ -847,6 +976,11 @@ function mapSaturationDark(s, mode) {
|
|
|
847
976
|
if (mode === "static") return s;
|
|
848
977
|
return s * (1 - globalConfig.darkDesaturation);
|
|
849
978
|
}
|
|
979
|
+
function schemeLightnessRange(isDark, mode) {
|
|
980
|
+
if (mode === "static") return [0, 1];
|
|
981
|
+
const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
982
|
+
return [lo / 100, hi / 100];
|
|
983
|
+
}
|
|
850
984
|
function clamp(v, min, max) {
|
|
851
985
|
return Math.max(min, Math.min(max, v));
|
|
852
986
|
}
|
|
@@ -907,20 +1041,22 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
907
1041
|
if (isDark && mode === "auto") delta = -delta;
|
|
908
1042
|
preferredL = clamp(baseL + delta, 0, 100);
|
|
909
1043
|
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
|
|
910
|
-
else preferredL =
|
|
1044
|
+
else preferredL = mapLightnessLight(parsed.value, mode);
|
|
911
1045
|
}
|
|
912
1046
|
const rawContrast = def.contrast;
|
|
913
1047
|
if (rawContrast !== void 0) {
|
|
914
1048
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
915
1049
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
916
1050
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1051
|
+
const lightnessRange = schemeLightnessRange(isDark, mode);
|
|
917
1052
|
return {
|
|
918
1053
|
l: findLightnessForContrast({
|
|
919
1054
|
hue: effectiveHue,
|
|
920
1055
|
saturation: effectiveSat,
|
|
921
|
-
preferredLightness: preferredL / 100,
|
|
1056
|
+
preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
|
|
922
1057
|
baseLinearRgb,
|
|
923
|
-
contrast: minCr
|
|
1058
|
+
contrast: minCr,
|
|
1059
|
+
lightnessRange
|
|
924
1060
|
}).lightness * 100,
|
|
925
1061
|
satFactor
|
|
926
1062
|
};
|
|
@@ -938,6 +1074,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
|
|
|
938
1074
|
}
|
|
939
1075
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
940
1076
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1077
|
+
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
941
1078
|
const regDef = def;
|
|
942
1079
|
const mode = regDef.mode ?? "auto";
|
|
943
1080
|
const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
|
|
@@ -983,6 +1120,83 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
983
1120
|
const tuning = resolveShadowTuning(def.tuning);
|
|
984
1121
|
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
985
1122
|
}
|
|
1123
|
+
function variantToLinearRgb(v) {
|
|
1124
|
+
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Resolve hue for OKHSL mixing, handling achromatic colors.
|
|
1128
|
+
* When one color has no saturation, its hue is meaningless —
|
|
1129
|
+
* use the hue from the color that has saturation (matches CSS
|
|
1130
|
+
* color-mix "missing component" behavior).
|
|
1131
|
+
*/
|
|
1132
|
+
function mixHue(base, target, t) {
|
|
1133
|
+
const SAT_EPSILON = 1e-6;
|
|
1134
|
+
const baseHasSat = base.s > SAT_EPSILON;
|
|
1135
|
+
const targetHasSat = target.s > SAT_EPSILON;
|
|
1136
|
+
if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
|
|
1137
|
+
if (targetHasSat) return target.h;
|
|
1138
|
+
return base.h;
|
|
1139
|
+
}
|
|
1140
|
+
function linearSrgbLerp(base, target, t) {
|
|
1141
|
+
return [
|
|
1142
|
+
base[0] + (target[0] - base[0]) * t,
|
|
1143
|
+
base[1] + (target[1] - base[1]) * t,
|
|
1144
|
+
base[2] + (target[2] - base[2]) * t
|
|
1145
|
+
];
|
|
1146
|
+
}
|
|
1147
|
+
function linearRgbToVariant(rgb) {
|
|
1148
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1149
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
|
|
1150
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
|
|
1151
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
|
|
1152
|
+
]);
|
|
1153
|
+
return {
|
|
1154
|
+
h,
|
|
1155
|
+
s,
|
|
1156
|
+
l,
|
|
1157
|
+
alpha: 1
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
1161
|
+
const baseResolved = ctx.resolved.get(def.base);
|
|
1162
|
+
const targetResolved = ctx.resolved.get(def.target);
|
|
1163
|
+
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1164
|
+
const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
|
|
1165
|
+
let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
|
|
1166
|
+
const blend = def.blend ?? "opaque";
|
|
1167
|
+
const space = def.space ?? "okhsl";
|
|
1168
|
+
const baseLinear = variantToLinearRgb(baseVariant);
|
|
1169
|
+
const targetLinear = variantToLinearRgb(targetVariant);
|
|
1170
|
+
if (def.contrast !== void 0) {
|
|
1171
|
+
const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
|
|
1172
|
+
let luminanceAt;
|
|
1173
|
+
if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1174
|
+
else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1175
|
+
else luminanceAt = (v) => {
|
|
1176
|
+
return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
|
|
1177
|
+
};
|
|
1178
|
+
t = findValueForMixContrast({
|
|
1179
|
+
preferredValue: t,
|
|
1180
|
+
baseLinearRgb: baseLinear,
|
|
1181
|
+
targetLinearRgb: targetLinear,
|
|
1182
|
+
contrast: minCr,
|
|
1183
|
+
luminanceAtValue: luminanceAt
|
|
1184
|
+
}).value;
|
|
1185
|
+
}
|
|
1186
|
+
if (blend === "transparent") return {
|
|
1187
|
+
h: targetVariant.h,
|
|
1188
|
+
s: targetVariant.s,
|
|
1189
|
+
l: targetVariant.l,
|
|
1190
|
+
alpha: clamp(t, 0, 1)
|
|
1191
|
+
};
|
|
1192
|
+
if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
|
|
1193
|
+
return {
|
|
1194
|
+
h: mixHue(baseVariant, targetVariant, t),
|
|
1195
|
+
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
1196
|
+
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
1197
|
+
alpha: 1
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
986
1200
|
function resolveAllColors(hue, saturation, defs) {
|
|
987
1201
|
validateColorDefs(defs);
|
|
988
1202
|
const order = topoSort(defs);
|
|
@@ -993,7 +1207,8 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
993
1207
|
resolved: /* @__PURE__ */ new Map()
|
|
994
1208
|
};
|
|
995
1209
|
function defMode(def) {
|
|
996
|
-
|
|
1210
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1211
|
+
return def.mode ?? "auto";
|
|
997
1212
|
}
|
|
998
1213
|
const lightMap = /* @__PURE__ */ new Map();
|
|
999
1214
|
for (const name of order) {
|
|
@@ -1206,26 +1421,40 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
1206
1421
|
}
|
|
1207
1422
|
};
|
|
1208
1423
|
}
|
|
1209
|
-
function resolvePrefix(options, themeName) {
|
|
1210
|
-
|
|
1211
|
-
if (
|
|
1424
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
1425
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
1426
|
+
if (prefix === true) return `${themeName}-`;
|
|
1427
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
1212
1428
|
return "";
|
|
1213
1429
|
}
|
|
1430
|
+
function validatePrimaryTheme(primary, themes) {
|
|
1431
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
1432
|
+
const available = Object.keys(themes).join(", ");
|
|
1433
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1214
1436
|
function createPalette(themes) {
|
|
1215
1437
|
return {
|
|
1216
1438
|
tokens(options) {
|
|
1439
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1217
1440
|
const modes = resolveModes(options?.modes);
|
|
1218
1441
|
const allTokens = {};
|
|
1219
1442
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1220
|
-
const
|
|
1443
|
+
const resolved = theme.resolve();
|
|
1444
|
+
const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
|
|
1221
1445
|
for (const variant of Object.keys(tokens)) {
|
|
1222
1446
|
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1223
1447
|
Object.assign(allTokens[variant], tokens[variant]);
|
|
1224
1448
|
}
|
|
1449
|
+
if (themeName === options?.primary) {
|
|
1450
|
+
const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
|
|
1451
|
+
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1452
|
+
}
|
|
1225
1453
|
}
|
|
1226
1454
|
return allTokens;
|
|
1227
1455
|
},
|
|
1228
1456
|
tasty(options) {
|
|
1457
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1229
1458
|
const states = {
|
|
1230
1459
|
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1231
1460
|
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1233,8 +1462,13 @@ function createPalette(themes) {
|
|
|
1233
1462
|
const modes = resolveModes(options?.modes);
|
|
1234
1463
|
const allTokens = {};
|
|
1235
1464
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1236
|
-
const
|
|
1465
|
+
const resolved = theme.resolve();
|
|
1466
|
+
const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
|
|
1237
1467
|
Object.assign(allTokens, tokens);
|
|
1468
|
+
if (themeName === options?.primary) {
|
|
1469
|
+
const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
|
|
1470
|
+
Object.assign(allTokens, unprefixed);
|
|
1471
|
+
}
|
|
1238
1472
|
}
|
|
1239
1473
|
return allTokens;
|
|
1240
1474
|
},
|
|
@@ -1245,6 +1479,7 @@ function createPalette(themes) {
|
|
|
1245
1479
|
return result;
|
|
1246
1480
|
},
|
|
1247
1481
|
css(options) {
|
|
1482
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1248
1483
|
const suffix = options?.suffix ?? "-color";
|
|
1249
1484
|
const format = options?.format ?? "rgb";
|
|
1250
1485
|
const allLines = {
|
|
@@ -1254,13 +1489,23 @@ function createPalette(themes) {
|
|
|
1254
1489
|
darkContrast: []
|
|
1255
1490
|
};
|
|
1256
1491
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1257
|
-
const
|
|
1492
|
+
const resolved = theme.resolve();
|
|
1493
|
+
const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
|
|
1258
1494
|
for (const key of [
|
|
1259
1495
|
"light",
|
|
1260
1496
|
"dark",
|
|
1261
1497
|
"lightContrast",
|
|
1262
1498
|
"darkContrast"
|
|
1263
1499
|
]) if (css[key]) allLines[key].push(css[key]);
|
|
1500
|
+
if (themeName === options?.primary) {
|
|
1501
|
+
const unprefixed = buildCssMap(resolved, "", suffix, format);
|
|
1502
|
+
for (const key of [
|
|
1503
|
+
"light",
|
|
1504
|
+
"dark",
|
|
1505
|
+
"lightContrast",
|
|
1506
|
+
"darkContrast"
|
|
1507
|
+
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1508
|
+
}
|
|
1264
1509
|
}
|
|
1265
1510
|
return {
|
|
1266
1511
|
light: allLines.light.join("\n"),
|
|
@@ -1433,10 +1678,12 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1433
1678
|
//#endregion
|
|
1434
1679
|
exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
|
|
1435
1680
|
exports.findLightnessForContrast = findLightnessForContrast;
|
|
1681
|
+
exports.findValueForMixContrast = findValueForMixContrast;
|
|
1436
1682
|
exports.formatHsl = formatHsl;
|
|
1437
1683
|
exports.formatOkhsl = formatOkhsl;
|
|
1438
1684
|
exports.formatOklch = formatOklch;
|
|
1439
1685
|
exports.formatRgb = formatRgb;
|
|
1686
|
+
exports.gamutClampedLuminance = gamutClampedLuminance;
|
|
1440
1687
|
exports.glaze = glaze;
|
|
1441
1688
|
exports.okhslToLinearSrgb = okhslToLinearSrgb;
|
|
1442
1689
|
exports.okhslToOklab = okhslToOklab;
|