@tenphi/glaze 0.0.0-snapshot.c84faa6 → 0.0.0-snapshot.e76f494
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 +157 -3
- package/dist/index.cjs +237 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -2
- package/dist/index.d.mts +70 -2
- package/dist/index.mjs +237 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Glaze generates robust **light**, **dark**, and **high-contrast** color schemes
|
|
|
22
22
|
|
|
23
23
|
- **OKHSL color space** — perceptually uniform hue and saturation
|
|
24
24
|
- **WCAG 2 contrast solving** — automatic lightness adjustment to meet AA/AAA targets
|
|
25
|
+
- **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
|
|
25
26
|
- **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
|
|
26
27
|
- **Light + Dark + High-Contrast** — all schemes from one definition
|
|
27
28
|
- **Per-color hue override** — absolute or relative hue shifts within a theme
|
|
@@ -413,6 +414,139 @@ const css = glaze.format(v, 'oklch');
|
|
|
413
414
|
}
|
|
414
415
|
```
|
|
415
416
|
|
|
417
|
+
## Mix Colors
|
|
418
|
+
|
|
419
|
+
Mix colors blend two existing colors together. Use them for hover overlays, tints, shades, and any derived color that sits between two reference colors.
|
|
420
|
+
|
|
421
|
+
### Opaque Mix
|
|
422
|
+
|
|
423
|
+
Produces a solid color by interpolating between `base` and `target`:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
theme.colors({
|
|
427
|
+
surface: { lightness: 95 },
|
|
428
|
+
accent: { lightness: 30 },
|
|
429
|
+
|
|
430
|
+
// 30% of the way from surface toward accent
|
|
431
|
+
tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
- `value` — mix ratio 0–100 (0 = pure base, 100 = pure target)
|
|
436
|
+
- The result is a fully opaque color (alpha = 1)
|
|
437
|
+
- Adapts to light/dark/HC schemes automatically via the resolved base and target
|
|
438
|
+
|
|
439
|
+
### Transparent Mix
|
|
440
|
+
|
|
441
|
+
Produces the target color with a controlled opacity — useful for hover overlays:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
theme.colors({
|
|
445
|
+
surface: { lightness: 95 },
|
|
446
|
+
black: { lightness: 0, saturation: 0 },
|
|
447
|
+
|
|
448
|
+
hover: {
|
|
449
|
+
type: 'mix',
|
|
450
|
+
base: 'surface',
|
|
451
|
+
target: 'black',
|
|
452
|
+
value: 8,
|
|
453
|
+
blend: 'transparent',
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
// hover → target color (black) with alpha = 0.08
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
|
|
460
|
+
|
|
461
|
+
### Blend Space
|
|
462
|
+
|
|
463
|
+
By default, opaque mixing interpolates in OKHSL (perceptually uniform, consistent with Glaze's model). Use `space: 'srgb'` for linear sRGB interpolation, which matches browser compositing:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
theme.colors({
|
|
467
|
+
surface: { lightness: 95 },
|
|
468
|
+
accent: { lightness: 30 },
|
|
469
|
+
|
|
470
|
+
// sRGB blend — matches what the browser would render
|
|
471
|
+
hover: { type: 'mix', base: 'surface', target: 'accent', value: 20, space: 'srgb' },
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
| Space | Behavior | Best for |
|
|
476
|
+
|---|---|---|
|
|
477
|
+
| `'okhsl'` (default) | Perceptually uniform OKHSL interpolation | Design token derivation |
|
|
478
|
+
| `'srgb'` | Linear sRGB channel interpolation | Matching browser compositing |
|
|
479
|
+
|
|
480
|
+
The `space` option only affects opaque blending. Transparent blending always composites in linear sRGB (matching browser alpha compositing).
|
|
481
|
+
|
|
482
|
+
### Contrast Solving
|
|
483
|
+
|
|
484
|
+
Mix colors support the same `contrast` prop as regular colors. The solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the WCAG target:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
theme.colors({
|
|
488
|
+
surface: { lightness: 95 },
|
|
489
|
+
accent: { lightness: 30 },
|
|
490
|
+
|
|
491
|
+
// Ensure the mixed color has at least AA contrast against surface
|
|
492
|
+
tint: {
|
|
493
|
+
type: 'mix',
|
|
494
|
+
base: 'surface',
|
|
495
|
+
target: 'accent',
|
|
496
|
+
value: 10,
|
|
497
|
+
contrast: 'AA',
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
// Ensure the transparent overlay has at least 3:1 contrast
|
|
501
|
+
overlay: {
|
|
502
|
+
type: 'mix',
|
|
503
|
+
base: 'surface',
|
|
504
|
+
target: 'accent',
|
|
505
|
+
value: 5,
|
|
506
|
+
blend: 'transparent',
|
|
507
|
+
contrast: 3,
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### High-Contrast Pairs
|
|
513
|
+
|
|
514
|
+
Both `value` and `contrast` support `[normal, highContrast]` pairs:
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
theme.colors({
|
|
518
|
+
surface: { lightness: 95 },
|
|
519
|
+
accent: { lightness: 30 },
|
|
520
|
+
|
|
521
|
+
tint: {
|
|
522
|
+
type: 'mix',
|
|
523
|
+
base: 'surface',
|
|
524
|
+
target: 'accent',
|
|
525
|
+
value: [20, 40], // stronger mix in high-contrast mode
|
|
526
|
+
contrast: [3, 'AAA'], // stricter contrast in high-contrast mode
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Achromatic Colors
|
|
532
|
+
|
|
533
|
+
When mixing with achromatic colors (saturation near zero, e.g., white or black) in `okhsl` space, the hue comes from whichever color has saturation. This prevents meaningless hue artifacts and matches CSS `color-mix()` "missing component" behavior. For purely achromatic mixes, prefer `space: 'srgb'` where hue is irrelevant.
|
|
534
|
+
|
|
535
|
+
### Mix Chaining
|
|
536
|
+
|
|
537
|
+
Mix colors can reference other mix colors, enabling multi-step derivations:
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
theme.colors({
|
|
541
|
+
white: { lightness: 100, saturation: 0 },
|
|
542
|
+
black: { lightness: 0, saturation: 0 },
|
|
543
|
+
gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
|
|
544
|
+
lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Mix colors cannot reference shadow colors (same restriction as regular dependent colors).
|
|
549
|
+
|
|
416
550
|
## Output Formats
|
|
417
551
|
|
|
418
552
|
Control the color format in exports with the `format` option:
|
|
@@ -758,10 +892,10 @@ glaze.configure({
|
|
|
758
892
|
|
|
759
893
|
## Color Definition Shape
|
|
760
894
|
|
|
761
|
-
`ColorDef` is a discriminated union of regular colors and
|
|
895
|
+
`ColorDef` is a discriminated union of regular colors, shadow colors, and mix colors:
|
|
762
896
|
|
|
763
897
|
```ts
|
|
764
|
-
type ColorDef = RegularColorDef | ShadowColorDef;
|
|
898
|
+
type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
|
|
765
899
|
|
|
766
900
|
interface RegularColorDef {
|
|
767
901
|
lightness?: HCPair<number | RelativeValue>;
|
|
@@ -780,9 +914,19 @@ interface ShadowColorDef {
|
|
|
780
914
|
intensity: HCPair<number>; // 0–100
|
|
781
915
|
tuning?: ShadowTuning;
|
|
782
916
|
}
|
|
917
|
+
|
|
918
|
+
interface MixColorDef {
|
|
919
|
+
type: 'mix';
|
|
920
|
+
base: string; // "from" color name
|
|
921
|
+
target: string; // "to" color name
|
|
922
|
+
value: HCPair<number>; // 0–100 (mix ratio or opacity)
|
|
923
|
+
blend?: 'opaque' | 'transparent'; // default: 'opaque'
|
|
924
|
+
space?: 'okhsl' | 'srgb'; // default: 'okhsl'
|
|
925
|
+
contrast?: HCPair<MinContrast>;
|
|
926
|
+
}
|
|
783
927
|
```
|
|
784
928
|
|
|
785
|
-
A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`. Shadow colors use `type: 'shadow'` and must reference a non-shadow `bg` color.
|
|
929
|
+
A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`. Shadow colors use `type: 'shadow'` and must reference a non-shadow `bg` color. Mix colors use `type: 'mix'` and must reference two non-shadow colors.
|
|
786
930
|
|
|
787
931
|
## Validation
|
|
788
932
|
|
|
@@ -801,6 +945,12 @@ A root color must have absolute `lightness` (a number). A dependent color must h
|
|
|
801
945
|
| Regular color `base` references a shadow color | Validation error |
|
|
802
946
|
| Shadow `intensity` outside 0–100 | Clamp silently |
|
|
803
947
|
| `contrast` + `opacity` combined | Warning |
|
|
948
|
+
| Mix `base` references non-existent color | Validation error |
|
|
949
|
+
| Mix `target` references non-existent color | Validation error |
|
|
950
|
+
| Mix `base` references a shadow color | Validation error |
|
|
951
|
+
| Mix `target` references a shadow color | Validation error |
|
|
952
|
+
| Mix `value` outside 0–100 | Clamp silently |
|
|
953
|
+
| Circular references involving mix colors | Validation error |
|
|
804
954
|
|
|
805
955
|
## Advanced: Color Math Utilities
|
|
806
956
|
|
|
@@ -846,6 +996,10 @@ primary.colors({
|
|
|
846
996
|
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
847
997
|
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
|
|
848
998
|
|
|
999
|
+
// Mix colors — hover overlays and tints
|
|
1000
|
+
'hover': { type: 'mix', base: 'surface', target: 'accent-fill', value: 8, blend: 'transparent' },
|
|
1001
|
+
'tint': { type: 'mix', base: 'surface', target: 'accent-fill', value: 20 },
|
|
1002
|
+
|
|
849
1003
|
// Fixed-alpha overlay
|
|
850
1004
|
overlay: { lightness: 0, opacity: 0.5 },
|
|
851
1005
|
});
|
package/dist/index.cjs
CHANGED
|
@@ -518,7 +518,7 @@ const cacheOrder = [];
|
|
|
518
518
|
* rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
|
|
519
519
|
* This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
|
|
520
520
|
*/
|
|
521
|
-
function gamutClampedLuminance(linearRgb) {
|
|
521
|
+
function gamutClampedLuminance$1(linearRgb) {
|
|
522
522
|
const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
|
|
523
523
|
const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
|
|
524
524
|
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
@@ -529,7 +529,7 @@ function cachedLuminance(h, s, l) {
|
|
|
529
529
|
const key = `${h}|${s}|${lRounded}`;
|
|
530
530
|
const cached = luminanceCache.get(key);
|
|
531
531
|
if (cached !== void 0) return cached;
|
|
532
|
-
const y = gamutClampedLuminance(okhslToLinearSrgb(h, s, lRounded));
|
|
532
|
+
const y = gamutClampedLuminance$1(okhslToLinearSrgb(h, s, lRounded));
|
|
533
533
|
if (luminanceCache.size >= CACHE_SIZE) {
|
|
534
534
|
const evict = cacheOrder.shift();
|
|
535
535
|
luminanceCache.delete(evict);
|
|
@@ -649,10 +649,10 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
|
649
649
|
function findLightnessForContrast(options) {
|
|
650
650
|
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
651
651
|
const target = resolveMinContrast(contrastInput);
|
|
652
|
-
const searchTarget = target + .
|
|
653
|
-
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
652
|
+
const searchTarget = target + .01;
|
|
653
|
+
const yBase = gamutClampedLuminance$1(baseLinearRgb);
|
|
654
654
|
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
655
|
-
if (crPref >=
|
|
655
|
+
if (crPref >= searchTarget) return {
|
|
656
656
|
lightness: preferredLightness,
|
|
657
657
|
contrast: crPref,
|
|
658
658
|
met: true,
|
|
@@ -701,6 +701,135 @@ function findLightnessForContrast(options) {
|
|
|
701
701
|
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
702
702
|
return candidates[0];
|
|
703
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Binary-search one branch [lo, hi] for the nearest passing mix value
|
|
706
|
+
* to `preferred`.
|
|
707
|
+
*/
|
|
708
|
+
function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
|
|
709
|
+
const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
|
|
710
|
+
const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
|
|
711
|
+
if (crLo < target && crHi < target) {
|
|
712
|
+
if (crLo >= crHi) return {
|
|
713
|
+
lightness: lo,
|
|
714
|
+
contrast: crLo,
|
|
715
|
+
met: false
|
|
716
|
+
};
|
|
717
|
+
return {
|
|
718
|
+
lightness: hi,
|
|
719
|
+
contrast: crHi,
|
|
720
|
+
met: false
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
let low = lo;
|
|
724
|
+
let high = hi;
|
|
725
|
+
for (let i = 0; i < maxIter; i++) {
|
|
726
|
+
if (high - low < epsilon) break;
|
|
727
|
+
const mid = (low + high) / 2;
|
|
728
|
+
if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
|
|
729
|
+
else high = mid;
|
|
730
|
+
else if (mid < preferred) high = mid;
|
|
731
|
+
else low = mid;
|
|
732
|
+
}
|
|
733
|
+
const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
|
|
734
|
+
const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
|
|
735
|
+
const lowPasses = crLow >= target;
|
|
736
|
+
const highPasses = crHigh >= target;
|
|
737
|
+
if (lowPasses && highPasses) {
|
|
738
|
+
if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
|
|
739
|
+
lightness: low,
|
|
740
|
+
contrast: crLow,
|
|
741
|
+
met: true
|
|
742
|
+
};
|
|
743
|
+
return {
|
|
744
|
+
lightness: high,
|
|
745
|
+
contrast: crHigh,
|
|
746
|
+
met: true
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
if (lowPasses) return {
|
|
750
|
+
lightness: low,
|
|
751
|
+
contrast: crLow,
|
|
752
|
+
met: true
|
|
753
|
+
};
|
|
754
|
+
if (highPasses) return {
|
|
755
|
+
lightness: high,
|
|
756
|
+
contrast: crHigh,
|
|
757
|
+
met: true
|
|
758
|
+
};
|
|
759
|
+
return crLow >= crHigh ? {
|
|
760
|
+
lightness: low,
|
|
761
|
+
contrast: crLow,
|
|
762
|
+
met: false
|
|
763
|
+
} : {
|
|
764
|
+
lightness: high,
|
|
765
|
+
contrast: crHigh,
|
|
766
|
+
met: false
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
|
|
771
|
+
* target against a base color, staying as close to `preferredValue` as possible.
|
|
772
|
+
*/
|
|
773
|
+
function findValueForMixContrast(options) {
|
|
774
|
+
const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
775
|
+
const target = resolveMinContrast(contrastInput);
|
|
776
|
+
const searchTarget = target + .01;
|
|
777
|
+
const yBase = gamutClampedLuminance$1(baseLinearRgb);
|
|
778
|
+
const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
|
|
779
|
+
if (crPref >= searchTarget) return {
|
|
780
|
+
value: preferredValue,
|
|
781
|
+
contrast: crPref,
|
|
782
|
+
met: true
|
|
783
|
+
};
|
|
784
|
+
const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
785
|
+
const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
786
|
+
if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
|
|
787
|
+
if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
|
|
788
|
+
const darkerPasses = darkerResult?.met ?? false;
|
|
789
|
+
const lighterPasses = lighterResult?.met ?? false;
|
|
790
|
+
if (darkerPasses && lighterPasses) {
|
|
791
|
+
if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
|
|
792
|
+
value: darkerResult.lightness,
|
|
793
|
+
contrast: darkerResult.contrast,
|
|
794
|
+
met: true
|
|
795
|
+
};
|
|
796
|
+
return {
|
|
797
|
+
value: lighterResult.lightness,
|
|
798
|
+
contrast: lighterResult.contrast,
|
|
799
|
+
met: true
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (darkerPasses) return {
|
|
803
|
+
value: darkerResult.lightness,
|
|
804
|
+
contrast: darkerResult.contrast,
|
|
805
|
+
met: true
|
|
806
|
+
};
|
|
807
|
+
if (lighterPasses) return {
|
|
808
|
+
value: lighterResult.lightness,
|
|
809
|
+
contrast: lighterResult.contrast,
|
|
810
|
+
met: true
|
|
811
|
+
};
|
|
812
|
+
const candidates = [];
|
|
813
|
+
if (darkerResult) candidates.push({
|
|
814
|
+
...darkerResult,
|
|
815
|
+
branch: "lower"
|
|
816
|
+
});
|
|
817
|
+
if (lighterResult) candidates.push({
|
|
818
|
+
...lighterResult,
|
|
819
|
+
branch: "upper"
|
|
820
|
+
});
|
|
821
|
+
if (candidates.length === 0) return {
|
|
822
|
+
value: preferredValue,
|
|
823
|
+
contrast: crPref,
|
|
824
|
+
met: false
|
|
825
|
+
};
|
|
826
|
+
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
827
|
+
return {
|
|
828
|
+
value: candidates[0].lightness,
|
|
829
|
+
contrast: candidates[0].contrast,
|
|
830
|
+
met: candidates[0].met
|
|
831
|
+
};
|
|
832
|
+
}
|
|
704
833
|
|
|
705
834
|
//#endregion
|
|
706
835
|
//#region src/glaze.ts
|
|
@@ -732,6 +861,9 @@ function pairHC(p) {
|
|
|
732
861
|
function isShadowDef(def) {
|
|
733
862
|
return def.type === "shadow";
|
|
734
863
|
}
|
|
864
|
+
function isMixDef(def) {
|
|
865
|
+
return def.type === "mix";
|
|
866
|
+
}
|
|
735
867
|
const DEFAULT_SHADOW_TUNING = {
|
|
736
868
|
saturationFactor: .18,
|
|
737
869
|
maxSaturation: .25,
|
|
@@ -799,6 +931,13 @@ function validateColorDefs(defs) {
|
|
|
799
931
|
}
|
|
800
932
|
continue;
|
|
801
933
|
}
|
|
934
|
+
if (isMixDef(def)) {
|
|
935
|
+
if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
936
|
+
if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
937
|
+
if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
938
|
+
if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
802
941
|
const regDef = def;
|
|
803
942
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
804
943
|
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
@@ -817,6 +956,9 @@ function validateColorDefs(defs) {
|
|
|
817
956
|
if (isShadowDef(def)) {
|
|
818
957
|
dfs(def.bg);
|
|
819
958
|
if (def.fg) dfs(def.fg);
|
|
959
|
+
} else if (isMixDef(def)) {
|
|
960
|
+
dfs(def.base);
|
|
961
|
+
dfs(def.target);
|
|
820
962
|
} else {
|
|
821
963
|
const regDef = def;
|
|
822
964
|
if (regDef.base) dfs(regDef.base);
|
|
@@ -836,6 +978,9 @@ function topoSort(defs) {
|
|
|
836
978
|
if (isShadowDef(def)) {
|
|
837
979
|
visit(def.bg);
|
|
838
980
|
if (def.fg) visit(def.fg);
|
|
981
|
+
} else if (isMixDef(def)) {
|
|
982
|
+
visit(def.base);
|
|
983
|
+
visit(def.target);
|
|
839
984
|
} else {
|
|
840
985
|
const regDef = def;
|
|
841
986
|
if (regDef.base) visit(regDef.base);
|
|
@@ -951,6 +1096,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
|
|
|
951
1096
|
}
|
|
952
1097
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
953
1098
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1099
|
+
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
954
1100
|
const regDef = def;
|
|
955
1101
|
const mode = regDef.mode ?? "auto";
|
|
956
1102
|
const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
|
|
@@ -996,6 +1142,89 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
996
1142
|
const tuning = resolveShadowTuning(def.tuning);
|
|
997
1143
|
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
998
1144
|
}
|
|
1145
|
+
function variantToLinearRgb(v) {
|
|
1146
|
+
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
1147
|
+
}
|
|
1148
|
+
function gamutClampedLuminance(linearRgb) {
|
|
1149
|
+
const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
|
|
1150
|
+
const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
|
|
1151
|
+
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
1152
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Resolve hue for OKHSL mixing, handling achromatic colors.
|
|
1156
|
+
* When one color has no saturation, its hue is meaningless —
|
|
1157
|
+
* use the hue from the color that has saturation (matches CSS
|
|
1158
|
+
* color-mix "missing component" behavior).
|
|
1159
|
+
*/
|
|
1160
|
+
function mixHue(base, target, t) {
|
|
1161
|
+
const SAT_EPSILON = 1e-6;
|
|
1162
|
+
const baseHasSat = base.s > SAT_EPSILON;
|
|
1163
|
+
const targetHasSat = target.s > SAT_EPSILON;
|
|
1164
|
+
if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
|
|
1165
|
+
if (targetHasSat) return target.h;
|
|
1166
|
+
return base.h;
|
|
1167
|
+
}
|
|
1168
|
+
function linearSrgbLerp(base, target, t) {
|
|
1169
|
+
return [
|
|
1170
|
+
base[0] + (target[0] - base[0]) * t,
|
|
1171
|
+
base[1] + (target[1] - base[1]) * t,
|
|
1172
|
+
base[2] + (target[2] - base[2]) * t
|
|
1173
|
+
];
|
|
1174
|
+
}
|
|
1175
|
+
function linearRgbToVariant(rgb) {
|
|
1176
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1177
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
|
|
1178
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
|
|
1179
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
|
|
1180
|
+
]);
|
|
1181
|
+
return {
|
|
1182
|
+
h,
|
|
1183
|
+
s,
|
|
1184
|
+
l,
|
|
1185
|
+
alpha: 1
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
1189
|
+
const baseResolved = ctx.resolved.get(def.base);
|
|
1190
|
+
const targetResolved = ctx.resolved.get(def.target);
|
|
1191
|
+
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1192
|
+
const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
|
|
1193
|
+
let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
|
|
1194
|
+
const blend = def.blend ?? "opaque";
|
|
1195
|
+
const space = def.space ?? "okhsl";
|
|
1196
|
+
const baseLinear = variantToLinearRgb(baseVariant);
|
|
1197
|
+
const targetLinear = variantToLinearRgb(targetVariant);
|
|
1198
|
+
if (def.contrast !== void 0) {
|
|
1199
|
+
const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
|
|
1200
|
+
let luminanceAt;
|
|
1201
|
+
if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1202
|
+
else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1203
|
+
else luminanceAt = (v) => {
|
|
1204
|
+
return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
|
|
1205
|
+
};
|
|
1206
|
+
t = findValueForMixContrast({
|
|
1207
|
+
preferredValue: t,
|
|
1208
|
+
baseLinearRgb: baseLinear,
|
|
1209
|
+
targetLinearRgb: targetLinear,
|
|
1210
|
+
contrast: minCr,
|
|
1211
|
+
luminanceAtValue: luminanceAt
|
|
1212
|
+
}).value;
|
|
1213
|
+
}
|
|
1214
|
+
if (blend === "transparent") return {
|
|
1215
|
+
h: targetVariant.h,
|
|
1216
|
+
s: targetVariant.s,
|
|
1217
|
+
l: targetVariant.l,
|
|
1218
|
+
alpha: clamp(t, 0, 1)
|
|
1219
|
+
};
|
|
1220
|
+
if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
|
|
1221
|
+
return {
|
|
1222
|
+
h: mixHue(baseVariant, targetVariant, t),
|
|
1223
|
+
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
1224
|
+
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
1225
|
+
alpha: 1
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
999
1228
|
function resolveAllColors(hue, saturation, defs) {
|
|
1000
1229
|
validateColorDefs(defs);
|
|
1001
1230
|
const order = topoSort(defs);
|
|
@@ -1006,7 +1235,8 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
1006
1235
|
resolved: /* @__PURE__ */ new Map()
|
|
1007
1236
|
};
|
|
1008
1237
|
function defMode(def) {
|
|
1009
|
-
|
|
1238
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1239
|
+
return def.mode ?? "auto";
|
|
1010
1240
|
}
|
|
1011
1241
|
const lightMap = /* @__PURE__ */ new Map();
|
|
1012
1242
|
for (const name of order) {
|
|
@@ -1446,6 +1676,7 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1446
1676
|
//#endregion
|
|
1447
1677
|
exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
|
|
1448
1678
|
exports.findLightnessForContrast = findLightnessForContrast;
|
|
1679
|
+
exports.findValueForMixContrast = findValueForMixContrast;
|
|
1449
1680
|
exports.formatHsl = formatHsl;
|
|
1450
1681
|
exports.formatOkhsl = formatOkhsl;
|
|
1451
1682
|
exports.formatOklch = formatOklch;
|