@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.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);
|
|
@@ -452,12 +433,13 @@ function formatOkhsl(h, s, l) {
|
|
|
452
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.
|
|
@@ -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
|
|
@@ -513,17 +495,6 @@ function resolveMinContrast(value) {
|
|
|
513
495
|
const CACHE_SIZE = 512;
|
|
514
496
|
const luminanceCache = /* @__PURE__ */ new Map();
|
|
515
497
|
const cacheOrder = [];
|
|
516
|
-
/**
|
|
517
|
-
* Compute WCAG 2 relative luminance from linear sRGB, matching the browser
|
|
518
|
-
* rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
|
|
519
|
-
* This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
|
|
520
|
-
*/
|
|
521
|
-
function gamutClampedLuminance(linearRgb) {
|
|
522
|
-
const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
|
|
523
|
-
const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
|
|
524
|
-
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
525
|
-
return .2126 * r + .7152 * g + .0722 * b;
|
|
526
|
-
}
|
|
527
498
|
function cachedLuminance(h, s, l) {
|
|
528
499
|
const lRounded = Math.round(l * 1e4) / 1e4;
|
|
529
500
|
const key = `${h}|${s}|${lRounded}`;
|
|
@@ -649,7 +620,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
|
649
620
|
function findLightnessForContrast(options) {
|
|
650
621
|
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
651
622
|
const target = resolveMinContrast(contrastInput);
|
|
652
|
-
const searchTarget = target
|
|
623
|
+
const searchTarget = target * 1.007;
|
|
653
624
|
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
654
625
|
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
655
626
|
if (crPref >= searchTarget) return {
|
|
@@ -701,6 +672,135 @@ function findLightnessForContrast(options) {
|
|
|
701
672
|
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
702
673
|
return candidates[0];
|
|
703
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
|
+
}
|
|
704
804
|
|
|
705
805
|
//#endregion
|
|
706
806
|
//#region src/glaze.ts
|
|
@@ -714,6 +814,7 @@ let globalConfig = {
|
|
|
714
814
|
lightLightness: [10, 100],
|
|
715
815
|
darkLightness: [15, 95],
|
|
716
816
|
darkDesaturation: .1,
|
|
817
|
+
darkCurve: .5,
|
|
717
818
|
states: {
|
|
718
819
|
dark: "@dark",
|
|
719
820
|
highContrast: "@high-contrast"
|
|
@@ -732,6 +833,9 @@ function pairHC(p) {
|
|
|
732
833
|
function isShadowDef(def) {
|
|
733
834
|
return def.type === "shadow";
|
|
734
835
|
}
|
|
836
|
+
function isMixDef(def) {
|
|
837
|
+
return def.type === "mix";
|
|
838
|
+
}
|
|
735
839
|
const DEFAULT_SHADOW_TUNING = {
|
|
736
840
|
saturationFactor: .18,
|
|
737
841
|
maxSaturation: .25,
|
|
@@ -799,6 +903,13 @@ function validateColorDefs(defs) {
|
|
|
799
903
|
}
|
|
800
904
|
continue;
|
|
801
905
|
}
|
|
906
|
+
if (isMixDef(def)) {
|
|
907
|
+
if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
908
|
+
if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
909
|
+
if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
910
|
+
if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
802
913
|
const regDef = def;
|
|
803
914
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
804
915
|
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
@@ -817,6 +928,9 @@ function validateColorDefs(defs) {
|
|
|
817
928
|
if (isShadowDef(def)) {
|
|
818
929
|
dfs(def.bg);
|
|
819
930
|
if (def.fg) dfs(def.fg);
|
|
931
|
+
} else if (isMixDef(def)) {
|
|
932
|
+
dfs(def.base);
|
|
933
|
+
dfs(def.target);
|
|
820
934
|
} else {
|
|
821
935
|
const regDef = def;
|
|
822
936
|
if (regDef.base) dfs(regDef.base);
|
|
@@ -836,6 +950,9 @@ function topoSort(defs) {
|
|
|
836
950
|
if (isShadowDef(def)) {
|
|
837
951
|
visit(def.bg);
|
|
838
952
|
if (def.fg) visit(def.fg);
|
|
953
|
+
} else if (isMixDef(def)) {
|
|
954
|
+
visit(def.base);
|
|
955
|
+
visit(def.target);
|
|
839
956
|
} else {
|
|
840
957
|
const regDef = def;
|
|
841
958
|
if (regDef.base) visit(regDef.base);
|
|
@@ -845,21 +962,33 @@ function topoSort(defs) {
|
|
|
845
962
|
for (const name of Object.keys(defs)) visit(name);
|
|
846
963
|
return result;
|
|
847
964
|
}
|
|
848
|
-
function mapLightnessLight(l, mode) {
|
|
849
|
-
if (mode === "static") return l;
|
|
965
|
+
function mapLightnessLight(l, mode, isHighContrast) {
|
|
966
|
+
if (mode === "static" || isHighContrast) return l;
|
|
850
967
|
const [lo, hi] = globalConfig.lightLightness;
|
|
851
968
|
return l * (hi - lo) / 100 + lo;
|
|
852
969
|
}
|
|
853
|
-
function mapLightnessDark(l, mode) {
|
|
970
|
+
function mapLightnessDark(l, mode, isHighContrast) {
|
|
854
971
|
if (mode === "static") return l;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
972
|
+
if (isHighContrast) {
|
|
973
|
+
if (mode === "fixed") return l;
|
|
974
|
+
const t = (100 - l) / 100;
|
|
975
|
+
return 100 * Math.pow(t, globalConfig.darkCurve);
|
|
976
|
+
}
|
|
977
|
+
const [darkLo, darkHi] = globalConfig.darkLightness;
|
|
978
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
979
|
+
const [lightLo, lightHi] = globalConfig.lightLightness;
|
|
980
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
981
|
+
return darkLo + (darkHi - darkLo) * Math.pow(t, globalConfig.darkCurve);
|
|
858
982
|
}
|
|
859
983
|
function mapSaturationDark(s, mode) {
|
|
860
984
|
if (mode === "static") return s;
|
|
861
985
|
return s * (1 - globalConfig.darkDesaturation);
|
|
862
986
|
}
|
|
987
|
+
function schemeLightnessRange(isDark, mode, isHighContrast) {
|
|
988
|
+
if (mode === "static" || isHighContrast) return [0, 1];
|
|
989
|
+
const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
990
|
+
return [lo / 100, hi / 100];
|
|
991
|
+
}
|
|
863
992
|
function clamp(v, min, max) {
|
|
864
993
|
return Math.max(min, Math.min(max, v));
|
|
865
994
|
}
|
|
@@ -919,21 +1048,23 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
919
1048
|
let delta = parsed.value;
|
|
920
1049
|
if (isDark && mode === "auto") delta = -delta;
|
|
921
1050
|
preferredL = clamp(baseL + delta, 0, 100);
|
|
922
|
-
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
|
|
923
|
-
else preferredL =
|
|
1051
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
|
|
1052
|
+
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
|
|
924
1053
|
}
|
|
925
1054
|
const rawContrast = def.contrast;
|
|
926
1055
|
if (rawContrast !== void 0) {
|
|
927
1056
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
928
1057
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
929
1058
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1059
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
|
|
930
1060
|
return {
|
|
931
1061
|
l: findLightnessForContrast({
|
|
932
1062
|
hue: effectiveHue,
|
|
933
1063
|
saturation: effectiveSat,
|
|
934
|
-
preferredLightness: preferredL / 100,
|
|
1064
|
+
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
935
1065
|
baseLinearRgb,
|
|
936
|
-
contrast: minCr
|
|
1066
|
+
contrast: minCr,
|
|
1067
|
+
lightnessRange: [0, 1]
|
|
937
1068
|
}).lightness * 100,
|
|
938
1069
|
satFactor
|
|
939
1070
|
};
|
|
@@ -951,6 +1082,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
|
|
|
951
1082
|
}
|
|
952
1083
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
953
1084
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1085
|
+
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
954
1086
|
const regDef = def;
|
|
955
1087
|
const mode = regDef.mode ?? "auto";
|
|
956
1088
|
const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
|
|
@@ -969,13 +1101,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
969
1101
|
let finalL;
|
|
970
1102
|
let finalSat;
|
|
971
1103
|
if (isDark && isRoot) {
|
|
972
|
-
finalL = mapLightnessDark(lightL, mode);
|
|
1104
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast);
|
|
973
1105
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
974
1106
|
} else if (isDark && !isRoot) {
|
|
975
1107
|
finalL = lightL;
|
|
976
1108
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
977
1109
|
} else if (isRoot) {
|
|
978
|
-
finalL = mapLightnessLight(lightL, mode);
|
|
1110
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast);
|
|
979
1111
|
finalSat = satFactor * ctx.saturation / 100;
|
|
980
1112
|
} else {
|
|
981
1113
|
finalL = lightL;
|
|
@@ -996,6 +1128,83 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
996
1128
|
const tuning = resolveShadowTuning(def.tuning);
|
|
997
1129
|
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
998
1130
|
}
|
|
1131
|
+
function variantToLinearRgb(v) {
|
|
1132
|
+
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Resolve hue for OKHSL mixing, handling achromatic colors.
|
|
1136
|
+
* When one color has no saturation, its hue is meaningless —
|
|
1137
|
+
* use the hue from the color that has saturation (matches CSS
|
|
1138
|
+
* color-mix "missing component" behavior).
|
|
1139
|
+
*/
|
|
1140
|
+
function mixHue(base, target, t) {
|
|
1141
|
+
const SAT_EPSILON = 1e-6;
|
|
1142
|
+
const baseHasSat = base.s > SAT_EPSILON;
|
|
1143
|
+
const targetHasSat = target.s > SAT_EPSILON;
|
|
1144
|
+
if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
|
|
1145
|
+
if (targetHasSat) return target.h;
|
|
1146
|
+
return base.h;
|
|
1147
|
+
}
|
|
1148
|
+
function linearSrgbLerp(base, target, t) {
|
|
1149
|
+
return [
|
|
1150
|
+
base[0] + (target[0] - base[0]) * t,
|
|
1151
|
+
base[1] + (target[1] - base[1]) * t,
|
|
1152
|
+
base[2] + (target[2] - base[2]) * t
|
|
1153
|
+
];
|
|
1154
|
+
}
|
|
1155
|
+
function linearRgbToVariant(rgb) {
|
|
1156
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1157
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
|
|
1158
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
|
|
1159
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
|
|
1160
|
+
]);
|
|
1161
|
+
return {
|
|
1162
|
+
h,
|
|
1163
|
+
s,
|
|
1164
|
+
l,
|
|
1165
|
+
alpha: 1
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
1169
|
+
const baseResolved = ctx.resolved.get(def.base);
|
|
1170
|
+
const targetResolved = ctx.resolved.get(def.target);
|
|
1171
|
+
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1172
|
+
const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
|
|
1173
|
+
let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
|
|
1174
|
+
const blend = def.blend ?? "opaque";
|
|
1175
|
+
const space = def.space ?? "okhsl";
|
|
1176
|
+
const baseLinear = variantToLinearRgb(baseVariant);
|
|
1177
|
+
const targetLinear = variantToLinearRgb(targetVariant);
|
|
1178
|
+
if (def.contrast !== void 0) {
|
|
1179
|
+
const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
|
|
1180
|
+
let luminanceAt;
|
|
1181
|
+
if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1182
|
+
else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1183
|
+
else luminanceAt = (v) => {
|
|
1184
|
+
return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
|
|
1185
|
+
};
|
|
1186
|
+
t = findValueForMixContrast({
|
|
1187
|
+
preferredValue: t,
|
|
1188
|
+
baseLinearRgb: baseLinear,
|
|
1189
|
+
targetLinearRgb: targetLinear,
|
|
1190
|
+
contrast: minCr,
|
|
1191
|
+
luminanceAtValue: luminanceAt
|
|
1192
|
+
}).value;
|
|
1193
|
+
}
|
|
1194
|
+
if (blend === "transparent") return {
|
|
1195
|
+
h: targetVariant.h,
|
|
1196
|
+
s: targetVariant.s,
|
|
1197
|
+
l: targetVariant.l,
|
|
1198
|
+
alpha: clamp(t, 0, 1)
|
|
1199
|
+
};
|
|
1200
|
+
if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
|
|
1201
|
+
return {
|
|
1202
|
+
h: mixHue(baseVariant, targetVariant, t),
|
|
1203
|
+
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
1204
|
+
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
1205
|
+
alpha: 1
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
999
1208
|
function resolveAllColors(hue, saturation, defs) {
|
|
1000
1209
|
validateColorDefs(defs);
|
|
1001
1210
|
const order = topoSort(defs);
|
|
@@ -1006,7 +1215,8 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
1006
1215
|
resolved: /* @__PURE__ */ new Map()
|
|
1007
1216
|
};
|
|
1008
1217
|
function defMode(def) {
|
|
1009
|
-
|
|
1218
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1219
|
+
return def.mode ?? "auto";
|
|
1010
1220
|
}
|
|
1011
1221
|
const lightMap = /* @__PURE__ */ new Map();
|
|
1012
1222
|
for (const name of order) {
|
|
@@ -1219,26 +1429,40 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
1219
1429
|
}
|
|
1220
1430
|
};
|
|
1221
1431
|
}
|
|
1222
|
-
function resolvePrefix(options, themeName) {
|
|
1223
|
-
|
|
1224
|
-
if (
|
|
1432
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
1433
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
1434
|
+
if (prefix === true) return `${themeName}-`;
|
|
1435
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
1225
1436
|
return "";
|
|
1226
1437
|
}
|
|
1438
|
+
function validatePrimaryTheme(primary, themes) {
|
|
1439
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
1440
|
+
const available = Object.keys(themes).join(", ");
|
|
1441
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1227
1444
|
function createPalette(themes) {
|
|
1228
1445
|
return {
|
|
1229
1446
|
tokens(options) {
|
|
1447
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1230
1448
|
const modes = resolveModes(options?.modes);
|
|
1231
1449
|
const allTokens = {};
|
|
1232
1450
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1233
|
-
const
|
|
1451
|
+
const resolved = theme.resolve();
|
|
1452
|
+
const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
|
|
1234
1453
|
for (const variant of Object.keys(tokens)) {
|
|
1235
1454
|
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1236
1455
|
Object.assign(allTokens[variant], tokens[variant]);
|
|
1237
1456
|
}
|
|
1457
|
+
if (themeName === options?.primary) {
|
|
1458
|
+
const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
|
|
1459
|
+
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1460
|
+
}
|
|
1238
1461
|
}
|
|
1239
1462
|
return allTokens;
|
|
1240
1463
|
},
|
|
1241
1464
|
tasty(options) {
|
|
1465
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1242
1466
|
const states = {
|
|
1243
1467
|
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1244
1468
|
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1246,8 +1470,13 @@ function createPalette(themes) {
|
|
|
1246
1470
|
const modes = resolveModes(options?.modes);
|
|
1247
1471
|
const allTokens = {};
|
|
1248
1472
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1249
|
-
const
|
|
1473
|
+
const resolved = theme.resolve();
|
|
1474
|
+
const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
|
|
1250
1475
|
Object.assign(allTokens, tokens);
|
|
1476
|
+
if (themeName === options?.primary) {
|
|
1477
|
+
const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
|
|
1478
|
+
Object.assign(allTokens, unprefixed);
|
|
1479
|
+
}
|
|
1251
1480
|
}
|
|
1252
1481
|
return allTokens;
|
|
1253
1482
|
},
|
|
@@ -1258,6 +1487,7 @@ function createPalette(themes) {
|
|
|
1258
1487
|
return result;
|
|
1259
1488
|
},
|
|
1260
1489
|
css(options) {
|
|
1490
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1261
1491
|
const suffix = options?.suffix ?? "-color";
|
|
1262
1492
|
const format = options?.format ?? "rgb";
|
|
1263
1493
|
const allLines = {
|
|
@@ -1267,13 +1497,23 @@ function createPalette(themes) {
|
|
|
1267
1497
|
darkContrast: []
|
|
1268
1498
|
};
|
|
1269
1499
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1270
|
-
const
|
|
1500
|
+
const resolved = theme.resolve();
|
|
1501
|
+
const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
|
|
1271
1502
|
for (const key of [
|
|
1272
1503
|
"light",
|
|
1273
1504
|
"dark",
|
|
1274
1505
|
"lightContrast",
|
|
1275
1506
|
"darkContrast"
|
|
1276
1507
|
]) if (css[key]) allLines[key].push(css[key]);
|
|
1508
|
+
if (themeName === options?.primary) {
|
|
1509
|
+
const unprefixed = buildCssMap(resolved, "", suffix, format);
|
|
1510
|
+
for (const key of [
|
|
1511
|
+
"light",
|
|
1512
|
+
"dark",
|
|
1513
|
+
"lightContrast",
|
|
1514
|
+
"darkContrast"
|
|
1515
|
+
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1516
|
+
}
|
|
1277
1517
|
}
|
|
1278
1518
|
return {
|
|
1279
1519
|
light: allLines.light.join("\n"),
|
|
@@ -1333,6 +1573,7 @@ glaze.configure = function configure(config) {
|
|
|
1333
1573
|
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
1334
1574
|
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
1335
1575
|
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
1576
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
1336
1577
|
states: {
|
|
1337
1578
|
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
1338
1579
|
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1432,6 +1673,7 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1432
1673
|
lightLightness: [10, 100],
|
|
1433
1674
|
darkLightness: [15, 95],
|
|
1434
1675
|
darkDesaturation: .1,
|
|
1676
|
+
darkCurve: .5,
|
|
1435
1677
|
states: {
|
|
1436
1678
|
dark: "@dark",
|
|
1437
1679
|
highContrast: "@high-contrast"
|
|
@@ -1446,10 +1688,12 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1446
1688
|
//#endregion
|
|
1447
1689
|
exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
|
|
1448
1690
|
exports.findLightnessForContrast = findLightnessForContrast;
|
|
1691
|
+
exports.findValueForMixContrast = findValueForMixContrast;
|
|
1449
1692
|
exports.formatHsl = formatHsl;
|
|
1450
1693
|
exports.formatOkhsl = formatOkhsl;
|
|
1451
1694
|
exports.formatOklch = formatOklch;
|
|
1452
1695
|
exports.formatRgb = formatRgb;
|
|
1696
|
+
exports.gamutClampedLuminance = gamutClampedLuminance;
|
|
1453
1697
|
exports.glaze = glaze;
|
|
1454
1698
|
exports.okhslToLinearSrgb = okhslToLinearSrgb;
|
|
1455
1699
|
exports.okhslToOklab = okhslToOklab;
|