@tenphi/glaze 0.0.0-snapshot.7c1fc7d → 0.0.0-snapshot.7e2a1da
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 +228 -34
- package/dist/index.cjs +318 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +123 -22
- package/dist/index.d.mts +123 -22
- package/dist/index.mjs +317 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -79,8 +79,8 @@ const OKLab_to_linear_sRGB_coefficients = [
|
|
|
79
79
|
.73956515,
|
|
80
80
|
-.45954404,
|
|
81
81
|
.08285427,
|
|
82
|
-
.
|
|
83
|
-
|
|
82
|
+
.1254107,
|
|
83
|
+
.14503204
|
|
84
84
|
]],
|
|
85
85
|
[[.13110757611180954, 1.813339709266608], [
|
|
86
86
|
1.35733652,
|
|
@@ -252,10 +252,9 @@ const getCs = (L, a, b, cusp) => {
|
|
|
252
252
|
];
|
|
253
253
|
};
|
|
254
254
|
/**
|
|
255
|
-
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to
|
|
256
|
-
* Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
|
|
255
|
+
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
|
|
257
256
|
*/
|
|
258
|
-
function
|
|
257
|
+
function okhslToOklab(h, s, l) {
|
|
259
258
|
const L = toeInv(l);
|
|
260
259
|
let a = 0;
|
|
261
260
|
let b = 0;
|
|
@@ -282,11 +281,18 @@ function okhslToLinearSrgb(h, s, l) {
|
|
|
282
281
|
a = c * a_;
|
|
283
282
|
b = c * b_;
|
|
284
283
|
}
|
|
285
|
-
return
|
|
284
|
+
return [
|
|
286
285
|
L,
|
|
287
286
|
a,
|
|
288
287
|
b
|
|
289
|
-
]
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
|
|
292
|
+
* Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
|
|
293
|
+
*/
|
|
294
|
+
function okhslToLinearSrgb(h, s, l) {
|
|
295
|
+
return OKLabToLinearSRGB(okhslToOklab(h, s, l));
|
|
290
296
|
}
|
|
291
297
|
/**
|
|
292
298
|
* Compute relative luminance Y from linear sRGB channels.
|
|
@@ -325,40 +331,15 @@ function okhslToSrgb(h, s, l) {
|
|
|
325
331
|
];
|
|
326
332
|
}
|
|
327
333
|
/**
|
|
328
|
-
*
|
|
334
|
+
* Compute WCAG 2 relative luminance from linear sRGB, matching the browser
|
|
335
|
+
* rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
|
|
336
|
+
* This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
|
|
329
337
|
*/
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (L !== 0 && L !== 1 && s !== 0) {
|
|
336
|
-
const a_ = Math.cos(TAU * hNorm);
|
|
337
|
-
const b_ = Math.sin(TAU * hNorm);
|
|
338
|
-
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
339
|
-
const mid = .8;
|
|
340
|
-
const midInv = 1.25;
|
|
341
|
-
let t, k0, k1, k2;
|
|
342
|
-
if (s < mid) {
|
|
343
|
-
t = midInv * s;
|
|
344
|
-
k0 = 0;
|
|
345
|
-
k1 = mid * c0;
|
|
346
|
-
k2 = 1 - k1 / cMid;
|
|
347
|
-
} else {
|
|
348
|
-
t = 5 * (s - .8);
|
|
349
|
-
k0 = cMid;
|
|
350
|
-
k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
|
|
351
|
-
k2 = 1 - k1 / (cMax - cMid);
|
|
352
|
-
}
|
|
353
|
-
const c = k0 + t * k1 / (1 - k2 * t);
|
|
354
|
-
a = c * a_;
|
|
355
|
-
b = c * b_;
|
|
356
|
-
}
|
|
357
|
-
return [
|
|
358
|
-
L,
|
|
359
|
-
a,
|
|
360
|
-
b
|
|
361
|
-
];
|
|
338
|
+
function gamutClampedLuminance(linearRgb) {
|
|
339
|
+
const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
|
|
340
|
+
const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
|
|
341
|
+
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
342
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
362
343
|
}
|
|
363
344
|
const linearSrgbToOklab = (rgb) => {
|
|
364
345
|
return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
|
|
@@ -450,12 +431,13 @@ function formatOkhsl(h, s, l) {
|
|
|
450
431
|
return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
|
|
451
432
|
}
|
|
452
433
|
/**
|
|
453
|
-
* Format OKHSL values as a CSS `rgb(R G B)` string
|
|
434
|
+
* Format OKHSL values as a CSS `rgb(R G B)` string.
|
|
435
|
+
* Uses 2 decimal places to avoid 8-bit quantization contrast loss.
|
|
454
436
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
455
437
|
*/
|
|
456
438
|
function formatRgb(h, s, l) {
|
|
457
439
|
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
|
|
458
|
-
return `rgb(${
|
|
440
|
+
return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
|
|
459
441
|
}
|
|
460
442
|
/**
|
|
461
443
|
* Format OKHSL values as a CSS `hsl(H S% L%)` string.
|
|
@@ -486,7 +468,7 @@ function formatOklch(h, s, l) {
|
|
|
486
468
|
const C = Math.sqrt(a * a + b * b);
|
|
487
469
|
let hh = Math.atan2(b, a) * (180 / Math.PI);
|
|
488
470
|
hh = constrainAngle(hh);
|
|
489
|
-
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh,
|
|
471
|
+
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
490
472
|
}
|
|
491
473
|
|
|
492
474
|
//#endregion
|
|
@@ -511,17 +493,6 @@ function resolveMinContrast(value) {
|
|
|
511
493
|
const CACHE_SIZE = 512;
|
|
512
494
|
const luminanceCache = /* @__PURE__ */ new Map();
|
|
513
495
|
const cacheOrder = [];
|
|
514
|
-
/**
|
|
515
|
-
* Compute WCAG 2 relative luminance from linear sRGB, matching the browser
|
|
516
|
-
* rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
|
|
517
|
-
* This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
|
|
518
|
-
*/
|
|
519
|
-
function gamutClampedLuminance(linearRgb) {
|
|
520
|
-
const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
|
|
521
|
-
const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
|
|
522
|
-
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
523
|
-
return .2126 * r + .7152 * g + .0722 * b;
|
|
524
|
-
}
|
|
525
496
|
function cachedLuminance(h, s, l) {
|
|
526
497
|
const lRounded = Math.round(l * 1e4) / 1e4;
|
|
527
498
|
const key = `${h}|${s}|${lRounded}`;
|
|
@@ -647,7 +618,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
|
647
618
|
function findLightnessForContrast(options) {
|
|
648
619
|
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
649
620
|
const target = resolveMinContrast(contrastInput);
|
|
650
|
-
const searchTarget = target
|
|
621
|
+
const searchTarget = target * 1.007;
|
|
651
622
|
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
652
623
|
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
653
624
|
if (crPref >= searchTarget) return {
|
|
@@ -699,6 +670,135 @@ function findLightnessForContrast(options) {
|
|
|
699
670
|
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
700
671
|
return candidates[0];
|
|
701
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* Binary-search one branch [lo, hi] for the nearest passing mix value
|
|
675
|
+
* to `preferred`.
|
|
676
|
+
*/
|
|
677
|
+
function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
|
|
678
|
+
const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
|
|
679
|
+
const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
|
|
680
|
+
if (crLo < target && crHi < target) {
|
|
681
|
+
if (crLo >= crHi) return {
|
|
682
|
+
lightness: lo,
|
|
683
|
+
contrast: crLo,
|
|
684
|
+
met: false
|
|
685
|
+
};
|
|
686
|
+
return {
|
|
687
|
+
lightness: hi,
|
|
688
|
+
contrast: crHi,
|
|
689
|
+
met: false
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
let low = lo;
|
|
693
|
+
let high = hi;
|
|
694
|
+
for (let i = 0; i < maxIter; i++) {
|
|
695
|
+
if (high - low < epsilon) break;
|
|
696
|
+
const mid = (low + high) / 2;
|
|
697
|
+
if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
|
|
698
|
+
else high = mid;
|
|
699
|
+
else if (mid < preferred) high = mid;
|
|
700
|
+
else low = mid;
|
|
701
|
+
}
|
|
702
|
+
const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
|
|
703
|
+
const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
|
|
704
|
+
const lowPasses = crLow >= target;
|
|
705
|
+
const highPasses = crHigh >= target;
|
|
706
|
+
if (lowPasses && highPasses) {
|
|
707
|
+
if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
|
|
708
|
+
lightness: low,
|
|
709
|
+
contrast: crLow,
|
|
710
|
+
met: true
|
|
711
|
+
};
|
|
712
|
+
return {
|
|
713
|
+
lightness: high,
|
|
714
|
+
contrast: crHigh,
|
|
715
|
+
met: true
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (lowPasses) return {
|
|
719
|
+
lightness: low,
|
|
720
|
+
contrast: crLow,
|
|
721
|
+
met: true
|
|
722
|
+
};
|
|
723
|
+
if (highPasses) return {
|
|
724
|
+
lightness: high,
|
|
725
|
+
contrast: crHigh,
|
|
726
|
+
met: true
|
|
727
|
+
};
|
|
728
|
+
return crLow >= crHigh ? {
|
|
729
|
+
lightness: low,
|
|
730
|
+
contrast: crLow,
|
|
731
|
+
met: false
|
|
732
|
+
} : {
|
|
733
|
+
lightness: high,
|
|
734
|
+
contrast: crHigh,
|
|
735
|
+
met: false
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
|
|
740
|
+
* target against a base color, staying as close to `preferredValue` as possible.
|
|
741
|
+
*/
|
|
742
|
+
function findValueForMixContrast(options) {
|
|
743
|
+
const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
744
|
+
const target = resolveMinContrast(contrastInput);
|
|
745
|
+
const searchTarget = target * 1.01;
|
|
746
|
+
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
747
|
+
const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
|
|
748
|
+
if (crPref >= searchTarget) return {
|
|
749
|
+
value: preferredValue,
|
|
750
|
+
contrast: crPref,
|
|
751
|
+
met: true
|
|
752
|
+
};
|
|
753
|
+
const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
754
|
+
const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
755
|
+
if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
|
|
756
|
+
if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
|
|
757
|
+
const darkerPasses = darkerResult?.met ?? false;
|
|
758
|
+
const lighterPasses = lighterResult?.met ?? false;
|
|
759
|
+
if (darkerPasses && lighterPasses) {
|
|
760
|
+
if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
|
|
761
|
+
value: darkerResult.lightness,
|
|
762
|
+
contrast: darkerResult.contrast,
|
|
763
|
+
met: true
|
|
764
|
+
};
|
|
765
|
+
return {
|
|
766
|
+
value: lighterResult.lightness,
|
|
767
|
+
contrast: lighterResult.contrast,
|
|
768
|
+
met: true
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
if (darkerPasses) return {
|
|
772
|
+
value: darkerResult.lightness,
|
|
773
|
+
contrast: darkerResult.contrast,
|
|
774
|
+
met: true
|
|
775
|
+
};
|
|
776
|
+
if (lighterPasses) return {
|
|
777
|
+
value: lighterResult.lightness,
|
|
778
|
+
contrast: lighterResult.contrast,
|
|
779
|
+
met: true
|
|
780
|
+
};
|
|
781
|
+
const candidates = [];
|
|
782
|
+
if (darkerResult) candidates.push({
|
|
783
|
+
...darkerResult,
|
|
784
|
+
branch: "lower"
|
|
785
|
+
});
|
|
786
|
+
if (lighterResult) candidates.push({
|
|
787
|
+
...lighterResult,
|
|
788
|
+
branch: "upper"
|
|
789
|
+
});
|
|
790
|
+
if (candidates.length === 0) return {
|
|
791
|
+
value: preferredValue,
|
|
792
|
+
contrast: crPref,
|
|
793
|
+
met: false
|
|
794
|
+
};
|
|
795
|
+
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
796
|
+
return {
|
|
797
|
+
value: candidates[0].lightness,
|
|
798
|
+
contrast: candidates[0].contrast,
|
|
799
|
+
met: candidates[0].met
|
|
800
|
+
};
|
|
801
|
+
}
|
|
702
802
|
|
|
703
803
|
//#endregion
|
|
704
804
|
//#region src/glaze.ts
|
|
@@ -712,6 +812,7 @@ let globalConfig = {
|
|
|
712
812
|
lightLightness: [10, 100],
|
|
713
813
|
darkLightness: [15, 95],
|
|
714
814
|
darkDesaturation: .1,
|
|
815
|
+
darkCurve: .5,
|
|
715
816
|
states: {
|
|
716
817
|
dark: "@dark",
|
|
717
818
|
highContrast: "@high-contrast"
|
|
@@ -730,6 +831,9 @@ function pairHC(p) {
|
|
|
730
831
|
function isShadowDef(def) {
|
|
731
832
|
return def.type === "shadow";
|
|
732
833
|
}
|
|
834
|
+
function isMixDef(def) {
|
|
835
|
+
return def.type === "mix";
|
|
836
|
+
}
|
|
733
837
|
const DEFAULT_SHADOW_TUNING = {
|
|
734
838
|
saturationFactor: .18,
|
|
735
839
|
maxSaturation: .25,
|
|
@@ -797,6 +901,13 @@ function validateColorDefs(defs) {
|
|
|
797
901
|
}
|
|
798
902
|
continue;
|
|
799
903
|
}
|
|
904
|
+
if (isMixDef(def)) {
|
|
905
|
+
if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
906
|
+
if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
907
|
+
if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
908
|
+
if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
800
911
|
const regDef = def;
|
|
801
912
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
802
913
|
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
@@ -815,6 +926,9 @@ function validateColorDefs(defs) {
|
|
|
815
926
|
if (isShadowDef(def)) {
|
|
816
927
|
dfs(def.bg);
|
|
817
928
|
if (def.fg) dfs(def.fg);
|
|
929
|
+
} else if (isMixDef(def)) {
|
|
930
|
+
dfs(def.base);
|
|
931
|
+
dfs(def.target);
|
|
818
932
|
} else {
|
|
819
933
|
const regDef = def;
|
|
820
934
|
if (regDef.base) dfs(regDef.base);
|
|
@@ -834,6 +948,9 @@ function topoSort(defs) {
|
|
|
834
948
|
if (isShadowDef(def)) {
|
|
835
949
|
visit(def.bg);
|
|
836
950
|
if (def.fg) visit(def.fg);
|
|
951
|
+
} else if (isMixDef(def)) {
|
|
952
|
+
visit(def.base);
|
|
953
|
+
visit(def.target);
|
|
837
954
|
} else {
|
|
838
955
|
const regDef = def;
|
|
839
956
|
if (regDef.base) visit(regDef.base);
|
|
@@ -843,21 +960,33 @@ function topoSort(defs) {
|
|
|
843
960
|
for (const name of Object.keys(defs)) visit(name);
|
|
844
961
|
return result;
|
|
845
962
|
}
|
|
846
|
-
function mapLightnessLight(l, mode) {
|
|
847
|
-
if (mode === "static") return l;
|
|
963
|
+
function mapLightnessLight(l, mode, isHighContrast) {
|
|
964
|
+
if (mode === "static" || isHighContrast) return l;
|
|
848
965
|
const [lo, hi] = globalConfig.lightLightness;
|
|
849
966
|
return l * (hi - lo) / 100 + lo;
|
|
850
967
|
}
|
|
851
|
-
function mapLightnessDark(l, mode) {
|
|
968
|
+
function mapLightnessDark(l, mode, isHighContrast) {
|
|
852
969
|
if (mode === "static") return l;
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
970
|
+
if (isHighContrast) {
|
|
971
|
+
if (mode === "fixed") return l;
|
|
972
|
+
const t = (100 - l) / 100;
|
|
973
|
+
return 100 * Math.pow(t, globalConfig.darkCurve);
|
|
974
|
+
}
|
|
975
|
+
const [darkLo, darkHi] = globalConfig.darkLightness;
|
|
976
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
977
|
+
const [lightLo, lightHi] = globalConfig.lightLightness;
|
|
978
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
979
|
+
return darkLo + (darkHi - darkLo) * Math.pow(t, globalConfig.darkCurve);
|
|
856
980
|
}
|
|
857
981
|
function mapSaturationDark(s, mode) {
|
|
858
982
|
if (mode === "static") return s;
|
|
859
983
|
return s * (1 - globalConfig.darkDesaturation);
|
|
860
984
|
}
|
|
985
|
+
function schemeLightnessRange(isDark, mode, isHighContrast) {
|
|
986
|
+
if (mode === "static" || isHighContrast) return [0, 1];
|
|
987
|
+
const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
988
|
+
return [lo / 100, hi / 100];
|
|
989
|
+
}
|
|
861
990
|
function clamp(v, min, max) {
|
|
862
991
|
return Math.max(min, Math.min(max, v));
|
|
863
992
|
}
|
|
@@ -917,21 +1046,23 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
917
1046
|
let delta = parsed.value;
|
|
918
1047
|
if (isDark && mode === "auto") delta = -delta;
|
|
919
1048
|
preferredL = clamp(baseL + delta, 0, 100);
|
|
920
|
-
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
|
|
921
|
-
else preferredL =
|
|
1049
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
|
|
1050
|
+
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
|
|
922
1051
|
}
|
|
923
1052
|
const rawContrast = def.contrast;
|
|
924
1053
|
if (rawContrast !== void 0) {
|
|
925
1054
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
926
1055
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
927
1056
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1057
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
|
|
928
1058
|
return {
|
|
929
1059
|
l: findLightnessForContrast({
|
|
930
1060
|
hue: effectiveHue,
|
|
931
1061
|
saturation: effectiveSat,
|
|
932
|
-
preferredLightness: preferredL / 100,
|
|
1062
|
+
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
933
1063
|
baseLinearRgb,
|
|
934
|
-
contrast: minCr
|
|
1064
|
+
contrast: minCr,
|
|
1065
|
+
lightnessRange: [0, 1]
|
|
935
1066
|
}).lightness * 100,
|
|
936
1067
|
satFactor
|
|
937
1068
|
};
|
|
@@ -949,6 +1080,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
|
|
|
949
1080
|
}
|
|
950
1081
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
951
1082
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1083
|
+
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
952
1084
|
const regDef = def;
|
|
953
1085
|
const mode = regDef.mode ?? "auto";
|
|
954
1086
|
const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
|
|
@@ -967,13 +1099,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
967
1099
|
let finalL;
|
|
968
1100
|
let finalSat;
|
|
969
1101
|
if (isDark && isRoot) {
|
|
970
|
-
finalL = mapLightnessDark(lightL, mode);
|
|
1102
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast);
|
|
971
1103
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
972
1104
|
} else if (isDark && !isRoot) {
|
|
973
1105
|
finalL = lightL;
|
|
974
1106
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
975
1107
|
} else if (isRoot) {
|
|
976
|
-
finalL = mapLightnessLight(lightL, mode);
|
|
1108
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast);
|
|
977
1109
|
finalSat = satFactor * ctx.saturation / 100;
|
|
978
1110
|
} else {
|
|
979
1111
|
finalL = lightL;
|
|
@@ -994,6 +1126,83 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
994
1126
|
const tuning = resolveShadowTuning(def.tuning);
|
|
995
1127
|
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
996
1128
|
}
|
|
1129
|
+
function variantToLinearRgb(v) {
|
|
1130
|
+
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Resolve hue for OKHSL mixing, handling achromatic colors.
|
|
1134
|
+
* When one color has no saturation, its hue is meaningless —
|
|
1135
|
+
* use the hue from the color that has saturation (matches CSS
|
|
1136
|
+
* color-mix "missing component" behavior).
|
|
1137
|
+
*/
|
|
1138
|
+
function mixHue(base, target, t) {
|
|
1139
|
+
const SAT_EPSILON = 1e-6;
|
|
1140
|
+
const baseHasSat = base.s > SAT_EPSILON;
|
|
1141
|
+
const targetHasSat = target.s > SAT_EPSILON;
|
|
1142
|
+
if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
|
|
1143
|
+
if (targetHasSat) return target.h;
|
|
1144
|
+
return base.h;
|
|
1145
|
+
}
|
|
1146
|
+
function linearSrgbLerp(base, target, t) {
|
|
1147
|
+
return [
|
|
1148
|
+
base[0] + (target[0] - base[0]) * t,
|
|
1149
|
+
base[1] + (target[1] - base[1]) * t,
|
|
1150
|
+
base[2] + (target[2] - base[2]) * t
|
|
1151
|
+
];
|
|
1152
|
+
}
|
|
1153
|
+
function linearRgbToVariant(rgb) {
|
|
1154
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1155
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
|
|
1156
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
|
|
1157
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
|
|
1158
|
+
]);
|
|
1159
|
+
return {
|
|
1160
|
+
h,
|
|
1161
|
+
s,
|
|
1162
|
+
l,
|
|
1163
|
+
alpha: 1
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
1167
|
+
const baseResolved = ctx.resolved.get(def.base);
|
|
1168
|
+
const targetResolved = ctx.resolved.get(def.target);
|
|
1169
|
+
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1170
|
+
const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
|
|
1171
|
+
let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
|
|
1172
|
+
const blend = def.blend ?? "opaque";
|
|
1173
|
+
const space = def.space ?? "okhsl";
|
|
1174
|
+
const baseLinear = variantToLinearRgb(baseVariant);
|
|
1175
|
+
const targetLinear = variantToLinearRgb(targetVariant);
|
|
1176
|
+
if (def.contrast !== void 0) {
|
|
1177
|
+
const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
|
|
1178
|
+
let luminanceAt;
|
|
1179
|
+
if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1180
|
+
else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1181
|
+
else luminanceAt = (v) => {
|
|
1182
|
+
return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
|
|
1183
|
+
};
|
|
1184
|
+
t = findValueForMixContrast({
|
|
1185
|
+
preferredValue: t,
|
|
1186
|
+
baseLinearRgb: baseLinear,
|
|
1187
|
+
targetLinearRgb: targetLinear,
|
|
1188
|
+
contrast: minCr,
|
|
1189
|
+
luminanceAtValue: luminanceAt
|
|
1190
|
+
}).value;
|
|
1191
|
+
}
|
|
1192
|
+
if (blend === "transparent") return {
|
|
1193
|
+
h: targetVariant.h,
|
|
1194
|
+
s: targetVariant.s,
|
|
1195
|
+
l: targetVariant.l,
|
|
1196
|
+
alpha: clamp(t, 0, 1)
|
|
1197
|
+
};
|
|
1198
|
+
if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
|
|
1199
|
+
return {
|
|
1200
|
+
h: mixHue(baseVariant, targetVariant, t),
|
|
1201
|
+
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
1202
|
+
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
1203
|
+
alpha: 1
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
997
1206
|
function resolveAllColors(hue, saturation, defs) {
|
|
998
1207
|
validateColorDefs(defs);
|
|
999
1208
|
const order = topoSort(defs);
|
|
@@ -1004,7 +1213,8 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
1004
1213
|
resolved: /* @__PURE__ */ new Map()
|
|
1005
1214
|
};
|
|
1006
1215
|
function defMode(def) {
|
|
1007
|
-
|
|
1216
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1217
|
+
return def.mode ?? "auto";
|
|
1008
1218
|
}
|
|
1009
1219
|
const lightMap = /* @__PURE__ */ new Map();
|
|
1010
1220
|
for (const name of order) {
|
|
@@ -1217,26 +1427,40 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
1217
1427
|
}
|
|
1218
1428
|
};
|
|
1219
1429
|
}
|
|
1220
|
-
function resolvePrefix(options, themeName) {
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1430
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
1431
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
1432
|
+
if (prefix === true) return `${themeName}-`;
|
|
1433
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
1223
1434
|
return "";
|
|
1224
1435
|
}
|
|
1436
|
+
function validatePrimaryTheme(primary, themes) {
|
|
1437
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
1438
|
+
const available = Object.keys(themes).join(", ");
|
|
1439
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1225
1442
|
function createPalette(themes) {
|
|
1226
1443
|
return {
|
|
1227
1444
|
tokens(options) {
|
|
1445
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1228
1446
|
const modes = resolveModes(options?.modes);
|
|
1229
1447
|
const allTokens = {};
|
|
1230
1448
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1231
|
-
const
|
|
1449
|
+
const resolved = theme.resolve();
|
|
1450
|
+
const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
|
|
1232
1451
|
for (const variant of Object.keys(tokens)) {
|
|
1233
1452
|
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1234
1453
|
Object.assign(allTokens[variant], tokens[variant]);
|
|
1235
1454
|
}
|
|
1455
|
+
if (themeName === options?.primary) {
|
|
1456
|
+
const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
|
|
1457
|
+
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1458
|
+
}
|
|
1236
1459
|
}
|
|
1237
1460
|
return allTokens;
|
|
1238
1461
|
},
|
|
1239
1462
|
tasty(options) {
|
|
1463
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1240
1464
|
const states = {
|
|
1241
1465
|
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1242
1466
|
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1244,8 +1468,13 @@ function createPalette(themes) {
|
|
|
1244
1468
|
const modes = resolveModes(options?.modes);
|
|
1245
1469
|
const allTokens = {};
|
|
1246
1470
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1247
|
-
const
|
|
1471
|
+
const resolved = theme.resolve();
|
|
1472
|
+
const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
|
|
1248
1473
|
Object.assign(allTokens, tokens);
|
|
1474
|
+
if (themeName === options?.primary) {
|
|
1475
|
+
const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
|
|
1476
|
+
Object.assign(allTokens, unprefixed);
|
|
1477
|
+
}
|
|
1249
1478
|
}
|
|
1250
1479
|
return allTokens;
|
|
1251
1480
|
},
|
|
@@ -1256,6 +1485,7 @@ function createPalette(themes) {
|
|
|
1256
1485
|
return result;
|
|
1257
1486
|
},
|
|
1258
1487
|
css(options) {
|
|
1488
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1259
1489
|
const suffix = options?.suffix ?? "-color";
|
|
1260
1490
|
const format = options?.format ?? "rgb";
|
|
1261
1491
|
const allLines = {
|
|
@@ -1265,13 +1495,23 @@ function createPalette(themes) {
|
|
|
1265
1495
|
darkContrast: []
|
|
1266
1496
|
};
|
|
1267
1497
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1268
|
-
const
|
|
1498
|
+
const resolved = theme.resolve();
|
|
1499
|
+
const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
|
|
1269
1500
|
for (const key of [
|
|
1270
1501
|
"light",
|
|
1271
1502
|
"dark",
|
|
1272
1503
|
"lightContrast",
|
|
1273
1504
|
"darkContrast"
|
|
1274
1505
|
]) if (css[key]) allLines[key].push(css[key]);
|
|
1506
|
+
if (themeName === options?.primary) {
|
|
1507
|
+
const unprefixed = buildCssMap(resolved, "", suffix, format);
|
|
1508
|
+
for (const key of [
|
|
1509
|
+
"light",
|
|
1510
|
+
"dark",
|
|
1511
|
+
"lightContrast",
|
|
1512
|
+
"darkContrast"
|
|
1513
|
+
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1514
|
+
}
|
|
1275
1515
|
}
|
|
1276
1516
|
return {
|
|
1277
1517
|
light: allLines.light.join("\n"),
|
|
@@ -1331,6 +1571,7 @@ glaze.configure = function configure(config) {
|
|
|
1331
1571
|
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
1332
1572
|
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
1333
1573
|
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
1574
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
1334
1575
|
states: {
|
|
1335
1576
|
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
1336
1577
|
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1430,6 +1671,7 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1430
1671
|
lightLightness: [10, 100],
|
|
1431
1672
|
darkLightness: [15, 95],
|
|
1432
1673
|
darkDesaturation: .1,
|
|
1674
|
+
darkCurve: .5,
|
|
1433
1675
|
states: {
|
|
1434
1676
|
dark: "@dark",
|
|
1435
1677
|
highContrast: "@high-contrast"
|
|
@@ -1442,5 +1684,5 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1442
1684
|
};
|
|
1443
1685
|
|
|
1444
1686
|
//#endregion
|
|
1445
|
-
export { contrastRatioFromLuminance, findLightnessForContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
|
|
1687
|
+
export { contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
|
|
1446
1688
|
//# sourceMappingURL=index.mjs.map
|