@tenphi/glaze 0.0.0-snapshot.c3d484c → 0.0.0-snapshot.c61bbdf
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/AGENTS.md +42 -16
- package/README.md +41 -22
- package/dist/index.cjs +311 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -43
- package/dist/index.d.mts +122 -43
- package/dist/index.mjs +311 -139
- package/dist/index.mjs.map +1 -1
- package/docs/contrast.mdx +702 -0
- package/docs/exporting.mdx +624 -0
- package/docs/getting-started.mdx +486 -0
- package/docs/methodology.mdx +973 -0
- package/docs/migration.md +212 -285
- package/docs/mixes.mdx +268 -0
- package/docs/okhst.mdx +596 -0
- package/docs/reference/color-definitions.md +498 -0
- package/docs/reference/color-tokens.md +390 -0
- package/docs/reference/configuration.md +437 -0
- package/docs/reference/contrast.md +276 -0
- package/docs/reference/index.md +218 -0
- package/docs/reference/output.md +594 -0
- package/docs/reference/palette.md +387 -0
- package/docs/reference/theme.md +326 -0
- package/docs/reference/utilities.md +499 -0
- package/docs/schemes.mdx +546 -0
- package/docs/shadows-and-glows.mdx +509 -0
- package/package.json +9 -1
- package/docs/api.md +0 -2021
- package/docs/methodology.md +0 -686
- package/docs/okhst.md +0 -207
package/dist/index.mjs
CHANGED
|
@@ -368,6 +368,27 @@ const sRGBGammaToLinear = (val) => {
|
|
|
368
368
|
const abs = Math.abs(val);
|
|
369
369
|
return abs <= .04045 ? val / 12.92 : sign * Math.pow((abs + .055) / 1.055, 2.4);
|
|
370
370
|
};
|
|
371
|
+
/** Gamma-encoded sRGB clamped to the display gamut — what a browser paints. */
|
|
372
|
+
function toDisplayGamma(linearRgb) {
|
|
373
|
+
return [
|
|
374
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))),
|
|
375
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))),
|
|
376
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2])))
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Browser alpha compositing: `source-over` interpolates the gamma-encoded
|
|
381
|
+
* channels, not linear light (50% black over white paints `#808080`).
|
|
382
|
+
* Takes display-gamma inputs and returns the composite as linear sRGB for
|
|
383
|
+
* the luminance pipeline.
|
|
384
|
+
*/
|
|
385
|
+
function compositeOverGamma(baseGamma, overlayGamma, alpha) {
|
|
386
|
+
return [
|
|
387
|
+
sRGBGammaToLinear(baseGamma[0] + (overlayGamma[0] - baseGamma[0]) * alpha),
|
|
388
|
+
sRGBGammaToLinear(baseGamma[1] + (overlayGamma[1] - baseGamma[1]) * alpha),
|
|
389
|
+
sRGBGammaToLinear(baseGamma[2] + (overlayGamma[2] - baseGamma[2]) * alpha)
|
|
390
|
+
];
|
|
391
|
+
}
|
|
371
392
|
/**
|
|
372
393
|
* Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
|
|
373
394
|
* h: 0–360, s: 0–1, l: 0–1. Returns [r, g, b] with each channel 0–1.
|
|
@@ -777,6 +798,7 @@ function configure(config) {
|
|
|
777
798
|
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
778
799
|
},
|
|
779
800
|
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
|
|
801
|
+
glowTuning: config.glowTuning ?? globalConfig.glowTuning,
|
|
780
802
|
autoFlip: config.autoFlip ?? globalConfig.autoFlip,
|
|
781
803
|
pastel: false,
|
|
782
804
|
inferRole: config.inferRole ?? globalConfig.inferRole,
|
|
@@ -808,6 +830,7 @@ function mergeConfig(base, override) {
|
|
|
808
830
|
states: base.states,
|
|
809
831
|
modes: base.modes,
|
|
810
832
|
shadowTuning: override.shadowTuning ?? base.shadowTuning,
|
|
833
|
+
glowTuning: override.glowTuning ?? base.glowTuning,
|
|
811
834
|
autoFlip: override.autoFlip ?? base.autoFlip,
|
|
812
835
|
pastel: override.pastel ?? false,
|
|
813
836
|
inferRole: override.inferRole ?? base.inferRole,
|
|
@@ -817,8 +840,8 @@ function mergeConfig(base, override) {
|
|
|
817
840
|
/**
|
|
818
841
|
* Freeze `getConfig() ∪ instanceLocal ∪ exportArg` as a plain, resolve-relevant
|
|
819
842
|
* override for authoring export, so restored snapshots pin these fields. Later
|
|
820
|
-
* wins; nested objects (`shadowTuning`) replace wholesale. The
|
|
821
|
-
* `structuredClone` detaches tone-window /
|
|
843
|
+
* wins; nested objects (`shadowTuning` / `glowTuning`) replace wholesale. The
|
|
844
|
+
* final `structuredClone` detaches tone-window / tuning objects that `mergeConfig`
|
|
822
845
|
* may still share with the live global config.
|
|
823
846
|
*/
|
|
824
847
|
function freezeConfigForExport(instanceLocal, exportArg) {
|
|
@@ -835,6 +858,7 @@ function freezeConfigForExport(instanceLocal, exportArg) {
|
|
|
835
858
|
inferRole: effective.inferRole
|
|
836
859
|
};
|
|
837
860
|
if (effective.shadowTuning !== void 0) out.shadowTuning = effective.shadowTuning;
|
|
861
|
+
if (effective.glowTuning !== void 0) out.glowTuning = effective.glowTuning;
|
|
838
862
|
const authoredLevel = exportArg?.contrastLevel ?? instanceLocal?.contrastLevel;
|
|
839
863
|
if (authoredLevel !== void 0) out.contrastLevel = normalizeContrastLevel(authoredLevel);
|
|
840
864
|
return structuredClone(out);
|
|
@@ -845,8 +869,13 @@ function freezeConfigForExport(instanceLocal, exportArg) {
|
|
|
845
869
|
/**
|
|
846
870
|
* Current authoring-export schema version. Bump when the export shape
|
|
847
871
|
* changes in a non-compatible way. Written on every `.export()` snapshot.
|
|
872
|
+
*
|
|
873
|
+
* - `1` — Glaze 2.x.
|
|
874
|
+
* - `2` — Glaze 3: `shadow` `intensity` / `fg` / `tuning` changed meaning and
|
|
875
|
+
* `glow` was added. Version-1 theme snapshots that carry shadow defs or a
|
|
876
|
+
* `shadowTuning` are rejected on restore.
|
|
848
877
|
*/
|
|
849
|
-
const GLAZE_EXPORT_VERSION =
|
|
878
|
+
const GLAZE_EXPORT_VERSION = 2;
|
|
850
879
|
|
|
851
880
|
//#endregion
|
|
852
881
|
//#region src/serialize.ts
|
|
@@ -868,6 +897,22 @@ function assertExportVersion(data, factory) {
|
|
|
868
897
|
if (data.version > GLAZE_EXPORT_VERSION) throw new Error(`${factory}: unsupported export version ${data.version} (this library supports version ${GLAZE_EXPORT_VERSION}). Upgrade @tenphi/glaze to load this snapshot.`);
|
|
869
898
|
}
|
|
870
899
|
/**
|
|
900
|
+
* Reject a version-1 (Glaze 2) theme snapshot that carries shadow colors or a
|
|
901
|
+
* `shadowTuning`. Glaze 3 changed what shadow `intensity`, `fg` and tuning
|
|
902
|
+
* mean, so restoring one would silently re-render every shadow differently.
|
|
903
|
+
* Version-1 snapshots without shadows load as before. `fallbackVersion` is the
|
|
904
|
+
* enclosing palette's version, for a theme entry that carries none of its own.
|
|
905
|
+
*/
|
|
906
|
+
function assertNoLegacyShadows(data, factory, fallbackVersion) {
|
|
907
|
+
if ((data.version ?? fallbackVersion) !== 1) return;
|
|
908
|
+
const colors = isPlainObject(data.colors) ? data.colors : {};
|
|
909
|
+
const shadows = Object.keys(colors).filter((name) => isPlainObject(colors[name]) && colors[name].type === "shadow");
|
|
910
|
+
const hasTuning = isPlainObject(data.config) && data.config.shadowTuning !== void 0;
|
|
911
|
+
if (shadows.length === 0 && !hasTuning) return;
|
|
912
|
+
const what = shadows.length > 0 ? `shadow colors (${shadows.map((n) => JSON.stringify(n)).join(", ")})` : "a \"shadowTuning\" config";
|
|
913
|
+
throw new Error(`${factory}: this snapshot is export version 1 (Glaze 2) and contains ${what}. Glaze 3 changed what shadow "intensity", "fg" and tuning mean, so it would restore with different shadows. Re-author them as described in docs/migration.md ("3.0 — shadows and glows") and export again.`);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
871
916
|
* When `kind` is present, it must match the factory. Missing `kind` is
|
|
872
917
|
* allowed for legacy snapshots.
|
|
873
918
|
*/
|
|
@@ -960,7 +1005,7 @@ function hcMirrorsNormal(config) {
|
|
|
960
1005
|
return contrastFraction(config) === 1;
|
|
961
1006
|
}
|
|
962
1007
|
/**
|
|
963
|
-
* A numeric HC pair at a blend fraction (shadow `intensity`, mix `value`).
|
|
1008
|
+
* A numeric HC pair at a blend fraction (shadow / glow `intensity`, mix `value`).
|
|
964
1009
|
*
|
|
965
1010
|
* The endpoints return the authored entry *by identity* rather than through
|
|
966
1011
|
* float arithmetic — `a + (b - a) * 1` is not bit-exactly `b` in IEEE 754, and
|
|
@@ -1083,7 +1128,7 @@ function isAbsoluteTone(tone) {
|
|
|
1083
1128
|
* - the scheme tone window, including the high-contrast bypass and its
|
|
1084
1129
|
* continuous generalization under a manual contrast level.
|
|
1085
1130
|
*
|
|
1086
|
-
* See `docs/okhst.
|
|
1131
|
+
* See `docs/okhst.mdx` for the full specification and the calibrated
|
|
1087
1132
|
* default constants.
|
|
1088
1133
|
*/
|
|
1089
1134
|
/**
|
|
@@ -1439,8 +1484,8 @@ function validateOkhstColor(value) {
|
|
|
1439
1484
|
}
|
|
1440
1485
|
/**
|
|
1441
1486
|
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1442
|
-
* `glaze.shadow()` so all
|
|
1443
|
-
* literal objects) go through one parser.
|
|
1487
|
+
* `glaze.shadow()` / `glaze.glow()` so all their inputs (hex, color
|
|
1488
|
+
* functions, OKHSL, literal objects) go through one parser.
|
|
1444
1489
|
*/
|
|
1445
1490
|
function extractOkhslFromValue(value) {
|
|
1446
1491
|
if (typeof value === "string") return parseColorString(value);
|
|
@@ -2072,36 +2117,46 @@ function oppositeRole(role) {
|
|
|
2072
2117
|
}
|
|
2073
2118
|
|
|
2074
2119
|
//#endregion
|
|
2075
|
-
//#region src/
|
|
2120
|
+
//#region src/lighting.ts
|
|
2076
2121
|
/**
|
|
2077
|
-
* Shadow color computation.
|
|
2122
|
+
* Shadow and glow color computation.
|
|
2078
2123
|
*
|
|
2079
|
-
* Owns the shadow / mix def predicates, default tuning
|
|
2080
|
-
*
|
|
2081
|
-
*
|
|
2082
|
-
*
|
|
2124
|
+
* Owns the shadow / glow / mix def predicates, the default lighting tuning
|
|
2125
|
+
* and its merge, and the exact lighting solver (`computeLighting`). Both
|
|
2126
|
+
* types emit a translucent pigment over a known surface (`bg`) whose opacity
|
|
2127
|
+
* is solved so the composite lands an exact tone step away from it: darker
|
|
2128
|
+
* for `shadow`, lighter for `glow`, in every scheme. Compositing is modeled
|
|
2129
|
+
* the way browsers paint it — `source-over` on gamma-encoded sRGB — and the
|
|
2130
|
+
* step is measured in the WCAG-shaped tone of the composite's luminance, so
|
|
2131
|
+
* it holds on chromatic surfaces too. An optional object (`fg`) scales the
|
|
2132
|
+
* step the way bounce light does. The resolver consumes this module per
|
|
2133
|
+
* scheme variant; `glaze.shadow()` / `glaze.glow()` call it directly.
|
|
2083
2134
|
*/
|
|
2084
2135
|
function isShadowDef(def) {
|
|
2085
2136
|
return def.type === "shadow";
|
|
2086
2137
|
}
|
|
2138
|
+
function isGlowDef(def) {
|
|
2139
|
+
return def.type === "glow";
|
|
2140
|
+
}
|
|
2141
|
+
/** A `shadow` or `glow` def — the translucent overlay types. */
|
|
2142
|
+
function isLightingDef(def) {
|
|
2143
|
+
return isShadowDef(def) || isGlowDef(def);
|
|
2144
|
+
}
|
|
2087
2145
|
function isMixDef(def) {
|
|
2088
2146
|
return def.type === "mix";
|
|
2089
2147
|
}
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
minGapTarget: .05,
|
|
2096
|
-
alphaMax: 1,
|
|
2097
|
-
bgHueBlend: .2
|
|
2148
|
+
const DEFAULT_LIGHTING_TUNING = {
|
|
2149
|
+
objectInfluence: .5,
|
|
2150
|
+
tint: 0,
|
|
2151
|
+
reach: 1,
|
|
2152
|
+
hueBleed: 0
|
|
2098
2153
|
};
|
|
2099
|
-
|
|
2154
|
+
/** Defaults ← global (`shadowTuning` / `glowTuning`) ← per-color, field by field. */
|
|
2155
|
+
function resolveLightingTuning(perColor, globalTuning) {
|
|
2100
2156
|
return {
|
|
2101
|
-
...
|
|
2157
|
+
...DEFAULT_LIGHTING_TUNING,
|
|
2102
2158
|
...globalTuning,
|
|
2103
|
-
...perColor
|
|
2104
|
-
lightnessBounds: perColor?.lightnessBounds ?? globalTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
|
|
2159
|
+
...perColor
|
|
2105
2160
|
};
|
|
2106
2161
|
}
|
|
2107
2162
|
function circularLerp(a, b, t) {
|
|
@@ -2110,47 +2165,122 @@ function circularLerp(a, b, t) {
|
|
|
2110
2165
|
else if (diff < -180) diff += 360;
|
|
2111
2166
|
return ((a + diff * t) % 360 + 360) % 360;
|
|
2112
2167
|
}
|
|
2168
|
+
const SAT_EPSILON = 1e-6;
|
|
2169
|
+
const BISECT_ITERATIONS = 40;
|
|
2170
|
+
const GOLDEN = (Math.sqrt(5) - 1) / 2;
|
|
2113
2171
|
/**
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2118
|
-
*/
|
|
2119
|
-
function
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
const t = deltaL / Math.max(bg.l - lSh, EPSILON);
|
|
2135
|
-
const tRef = computeRefT(tuning);
|
|
2136
|
-
const norm = Math.tanh(tRef / tuning.alphaMax);
|
|
2137
|
-
const alpha = Math.min(tuning.alphaMax * Math.tanh(t / tuning.alphaMax) / norm, tuning.alphaMax);
|
|
2172
|
+
* Object factor `k = max(0, 1 + β · d · (t_bg − t_fg) / 100)`, with `d = +1`
|
|
2173
|
+
* for a shadow and `−1` for a glow. Calibrated so an object at the surface's
|
|
2174
|
+
* own tone — or no object at all — gives exactly the authored step: a darker
|
|
2175
|
+
* object deepens its shadow, a lighter one strengthens its glow.
|
|
2176
|
+
*/
|
|
2177
|
+
function objectFactor(kind, bgTone, fgTone, objectInfluence) {
|
|
2178
|
+
if (fgTone === void 0) return 1;
|
|
2179
|
+
const d = kind === "shadow" ? 1 : -1;
|
|
2180
|
+
return Math.max(0, 1 + objectInfluence * d * (bgTone - fgTone) / 100);
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* The pigment: pure black (shadow) / pure white (glow) by default. `reach`
|
|
2184
|
+
* below 1 parks it part of the way from the surface's tone to that extreme so
|
|
2185
|
+
* a `tint` — saturation in the surface's hue, optionally pulled toward the
|
|
2186
|
+
* object's hue by `hueBleed` — can show.
|
|
2187
|
+
*/
|
|
2188
|
+
function pigmentFor(kind, bg, fg, bgTone, tuning) {
|
|
2189
|
+
const reach = clamp(tuning.reach, 0, 1);
|
|
2190
|
+
const tone = kind === "shadow" ? bgTone * (1 - reach) : bgTone + (100 - bgTone) * reach;
|
|
2191
|
+
const bleed = clamp(tuning.hueBleed, 0, 1);
|
|
2138
2192
|
return {
|
|
2139
|
-
h,
|
|
2140
|
-
s,
|
|
2141
|
-
l:
|
|
2142
|
-
|
|
2193
|
+
h: fg !== void 0 && fg.s > SAT_EPSILON && bleed > 0 ? circularLerp(bg.h, fg.h, bleed) : bg.h,
|
|
2194
|
+
s: clamp(tuning.tint, 0, 1),
|
|
2195
|
+
l: clamp(fromTone(tone), 0, 1)
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Solve the pigment opacity that moves `bg` exactly `intensity × k` tones
|
|
2200
|
+
* darker (shadow) or lighter (glow) once the browser composites it.
|
|
2201
|
+
*
|
|
2202
|
+
* The composite's luminance is convex in alpha (each gamma channel moves
|
|
2203
|
+
* linearly and the sRGB decode is convex), so the "has reached the step"
|
|
2204
|
+
* predicate is monotone whenever full opacity reaches it and a bisection
|
|
2205
|
+
* lands it exactly. When it doesn't, the step is out of reach — a glow on
|
|
2206
|
+
* white, a deep shadow on near-black — and the result pins to the pigment's
|
|
2207
|
+
* strongest composite, reporting what it reached in `reached`.
|
|
2208
|
+
*/
|
|
2209
|
+
function computeLighting(kind, bg, fg, intensity, tuning, pastel) {
|
|
2210
|
+
const bgTone = toTone(bg.l);
|
|
2211
|
+
const k = objectFactor(kind, bgTone, fg === void 0 ? void 0 : toTone(fg.l), tuning.objectInfluence);
|
|
2212
|
+
const step = clamp(intensity, 0, 100) * k;
|
|
2213
|
+
const pigment = pigmentFor(kind, bg, fg, bgTone, tuning);
|
|
2214
|
+
const bgGamma = toDisplayGamma(okhslToLinearSrgb(bg.h, bg.s, bg.l, bg.pastel ?? pastel));
|
|
2215
|
+
const pigmentGamma = toDisplayGamma(okhslToLinearSrgb(pigment.h, pigment.s, pigment.l, pastel));
|
|
2216
|
+
const toneAt = (alpha) => toneFromY(relativeLuminanceFromLinearRgb(compositeOverGamma(bgGamma, pigmentGamma, alpha)));
|
|
2217
|
+
const surfaceTone = toneAt(0);
|
|
2218
|
+
const d = kind === "shadow" ? 1 : -1;
|
|
2219
|
+
const gain = (alpha) => d * (surfaceTone - toneAt(alpha));
|
|
2220
|
+
if (step <= 0) return {
|
|
2221
|
+
...pigment,
|
|
2222
|
+
alpha: 0,
|
|
2223
|
+
step,
|
|
2224
|
+
reached: 0
|
|
2225
|
+
};
|
|
2226
|
+
let hi = 1;
|
|
2227
|
+
if (gain(1) < step) {
|
|
2228
|
+
hi = kind === "shadow" ? strongestShadowAlpha(gain) : 1;
|
|
2229
|
+
const reached = gain(hi);
|
|
2230
|
+
if (reached < step) return reached > 0 ? {
|
|
2231
|
+
...pigment,
|
|
2232
|
+
alpha: hi,
|
|
2233
|
+
step,
|
|
2234
|
+
reached
|
|
2235
|
+
} : {
|
|
2236
|
+
...pigment,
|
|
2237
|
+
alpha: 0,
|
|
2238
|
+
step,
|
|
2239
|
+
reached: 0
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
let lo = 0;
|
|
2243
|
+
for (let i = 0; i < BISECT_ITERATIONS; i++) {
|
|
2244
|
+
const mid = (lo + hi) / 2;
|
|
2245
|
+
if (gain(mid) < step) lo = mid;
|
|
2246
|
+
else hi = mid;
|
|
2247
|
+
}
|
|
2248
|
+
return {
|
|
2249
|
+
...pigment,
|
|
2250
|
+
alpha: hi,
|
|
2251
|
+
step,
|
|
2252
|
+
reached: gain(hi)
|
|
2143
2253
|
};
|
|
2144
2254
|
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Opacity of a shadow pigment's darkest composite. Full opacity for pure
|
|
2257
|
+
* black; a tinted pigment whose channels pull in different directions can
|
|
2258
|
+
* bottom out earlier, so a golden-section search over the unimodal gain finds
|
|
2259
|
+
* it and the better of that and full opacity wins. (A glow needs no search:
|
|
2260
|
+
* convex luminance peaks at an endpoint, and full opacity is the one that
|
|
2261
|
+
* lightens.)
|
|
2262
|
+
*/
|
|
2263
|
+
function strongestShadowAlpha(gain) {
|
|
2264
|
+
let a = 0;
|
|
2265
|
+
let b = 1;
|
|
2266
|
+
for (let i = 0; i < BISECT_ITERATIONS; i++) {
|
|
2267
|
+
const c = b - GOLDEN * (b - a);
|
|
2268
|
+
const e = a + GOLDEN * (b - a);
|
|
2269
|
+
if (gain(c) < gain(e)) a = c;
|
|
2270
|
+
else b = e;
|
|
2271
|
+
}
|
|
2272
|
+
const peak = (a + b) / 2;
|
|
2273
|
+
return gain(peak) > gain(1) ? peak : 1;
|
|
2274
|
+
}
|
|
2145
2275
|
|
|
2146
2276
|
//#endregion
|
|
2147
2277
|
//#region src/validation.ts
|
|
2148
2278
|
/**
|
|
2149
2279
|
* Color graph validation and topological sort.
|
|
2150
2280
|
*
|
|
2151
|
-
* `validateColorDefs` rejects bad references (missing / shadow-
|
|
2152
|
-
* base/contrast/tone mismatches) and detects cycles before
|
|
2153
|
-
* resolver runs. `topoSort` orders defs so each color is processed after
|
|
2281
|
+
* `validateColorDefs` rejects bad references (missing / shadow- or
|
|
2282
|
+
* glow-referencing / base/contrast/tone mismatches) and detects cycles before
|
|
2283
|
+
* the resolver runs. `topoSort` orders defs so each color is processed after
|
|
2154
2284
|
* its base / bg / fg / target dependencies.
|
|
2155
2285
|
*/
|
|
2156
2286
|
/**
|
|
@@ -2179,24 +2309,37 @@ function assertConsistentContrastMetric(name, contrast) {
|
|
|
2179
2309
|
if (normal === hc) return;
|
|
2180
2310
|
throw new Error(`glaze: color "${name}" has a "contrast" pair that switches metric (${normal} → ${hc}). A WCAG ratio and an APCA Lc are different scales; use one metric for both entries, e.g. { ${normal}: [normal, highContrast] }.`);
|
|
2181
2311
|
}
|
|
2312
|
+
/**
|
|
2313
|
+
* `'shadow'` / `'glow'` when `ref` names a local translucent overlay — which
|
|
2314
|
+
* nothing may build on — else `undefined`.
|
|
2315
|
+
*/
|
|
2316
|
+
function overlayKindOf(defs, ref) {
|
|
2317
|
+
const def = Object.hasOwn(defs, ref) ? defs[ref] : void 0;
|
|
2318
|
+
return def !== void 0 && isLightingDef(def) ? def.type : void 0;
|
|
2319
|
+
}
|
|
2182
2320
|
function validateColorDefs(defs, externalBases) {
|
|
2183
2321
|
const localNames = new Set(Object.keys(defs));
|
|
2184
2322
|
const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
|
|
2185
2323
|
for (const [name, def] of Object.entries(defs)) {
|
|
2186
|
-
if (
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
if (
|
|
2191
|
-
if (
|
|
2324
|
+
if (isLightingDef(def)) {
|
|
2325
|
+
const kind = def.type;
|
|
2326
|
+
for (const field of ["bg", "fg"]) {
|
|
2327
|
+
const ref = def[field];
|
|
2328
|
+
if (ref === void 0) continue;
|
|
2329
|
+
if (!allNames.has(ref)) throw new Error(`glaze: ${kind} "${name}" references non-existent ${field} "${ref}".`);
|
|
2330
|
+
const refKind = overlayKindOf(defs, ref);
|
|
2331
|
+
if (refKind !== void 0) throw new Error(`glaze: ${kind} "${name}" ${field} "${ref}" references a ${refKind} color.`);
|
|
2192
2332
|
}
|
|
2193
2333
|
continue;
|
|
2194
2334
|
}
|
|
2195
2335
|
if (isMixDef(def)) {
|
|
2196
2336
|
if (!allNames.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
2197
2337
|
if (!allNames.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
2198
|
-
|
|
2199
|
-
|
|
2338
|
+
for (const field of ["base", "target"]) {
|
|
2339
|
+
const ref = def[field];
|
|
2340
|
+
const refKind = overlayKindOf(defs, ref);
|
|
2341
|
+
if (refKind !== void 0) throw new Error(`glaze: mix "${name}" ${field} "${ref}" references a ${refKind} color.`);
|
|
2342
|
+
}
|
|
2200
2343
|
assertConsistentContrastMetric(name, def.contrast);
|
|
2201
2344
|
continue;
|
|
2202
2345
|
}
|
|
@@ -2210,7 +2353,8 @@ function validateColorDefs(defs, externalBases) {
|
|
|
2210
2353
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
2211
2354
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
2212
2355
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
2213
|
-
|
|
2356
|
+
const baseKind = regDef.base && overlayKindOf(defs, regDef.base);
|
|
2357
|
+
if (baseKind) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a ${baseKind} color.`);
|
|
2214
2358
|
if (!hasAbsoluteTone(regDef) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).`);
|
|
2215
2359
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived tone unpredictable.`);
|
|
2216
2360
|
}
|
|
@@ -2222,7 +2366,7 @@ function validateColorDefs(defs, externalBases) {
|
|
|
2222
2366
|
if (visited.has(name)) return;
|
|
2223
2367
|
inStack.add(name);
|
|
2224
2368
|
const def = defs[name];
|
|
2225
|
-
if (
|
|
2369
|
+
if (isLightingDef(def)) {
|
|
2226
2370
|
dfs(def.bg);
|
|
2227
2371
|
if (def.fg) dfs(def.fg);
|
|
2228
2372
|
} else if (isMixDef(def)) {
|
|
@@ -2245,7 +2389,7 @@ function topoSort(defs) {
|
|
|
2245
2389
|
visited.add(name);
|
|
2246
2390
|
const def = defs[name];
|
|
2247
2391
|
if (def === void 0) return;
|
|
2248
|
-
if (
|
|
2392
|
+
if (isLightingDef(def)) {
|
|
2249
2393
|
visit(def.bg);
|
|
2250
2394
|
if (def.fg) visit(def.fg);
|
|
2251
2395
|
} else if (isMixDef(def)) {
|
|
@@ -2264,7 +2408,8 @@ function topoSort(defs) {
|
|
|
2264
2408
|
//#endregion
|
|
2265
2409
|
//#region src/warnings.ts
|
|
2266
2410
|
/**
|
|
2267
|
-
*
|
|
2411
|
+
* Resolve-time warning dispatcher: unmet contrast floors, chromatic contrast
|
|
2412
|
+
* drift, and out-of-reach shadow / glow steps.
|
|
2268
2413
|
*
|
|
2269
2414
|
* Tokens memoize their resolution, but a long-lived process (e.g. a dev
|
|
2270
2415
|
* server with HMR) can re-resolve the same theme many times. The cache
|
|
@@ -2304,10 +2449,11 @@ function warnContrastUnmet(name, isDark, isHighContrast, contrast, actual) {
|
|
|
2304
2449
|
console.warn(`glaze: color "${name}" cannot meet ${metricLabel(contrast)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the tone window, lowering the contrast target, or picking a base color further from this color's tone.`);
|
|
2305
2450
|
}
|
|
2306
2451
|
/**
|
|
2307
|
-
* Verification (
|
|
2308
|
-
* lightness but drifts in real luminance, so a
|
|
2309
|
-
* land slightly under the contrast its tone
|
|
2310
|
-
* warning when the actual measured contrast
|
|
2452
|
+
* Verification (the drift check in docs/okhst.mdx): a chromatic swatch
|
|
2453
|
+
* inherits the gray tone's lightness but drifts in real luminance, so a
|
|
2454
|
+
* contrast-floored color may land slightly under the contrast its tone
|
|
2455
|
+
* implies. Emit an advisory warning when the actual measured contrast
|
|
2456
|
+
* drifts below the target.
|
|
2311
2457
|
*/
|
|
2312
2458
|
function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase) {
|
|
2313
2459
|
const actual = contrast.metric === "apca" ? Math.abs(contrast.polarity === "bg" ? apcaContrast(yBase, yColor) : apcaContrast(yColor, yBase)) : contrastRatioFromLuminance(yColor, yBase);
|
|
@@ -2316,6 +2462,23 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
|
|
|
2316
2462
|
if (dedupe(`drift|${name}|${scheme}|${contrast.metric}|${contrast.target.toFixed(2)}|${actual.toFixed(2)}`)) return;
|
|
2317
2463
|
console.warn(`glaze: color "${name}" drifts below ${metricLabel(contrast)} in ${scheme} scheme (measured ${actual.toFixed(2)}). Chromatic luminance differs from the gray tone; nudge the tone or saturation if the floor matters.`);
|
|
2318
2464
|
}
|
|
2465
|
+
/** Tone slack before an out-of-reach shadow / glow step is worth a warning. */
|
|
2466
|
+
const LIGHTING_WARN_SLACK = .05;
|
|
2467
|
+
/**
|
|
2468
|
+
* Warn when a `shadow` / `glow` step is out of reach of its pigment — a glow
|
|
2469
|
+
* on white, a deep shadow on near-black — so the composite pins to the
|
|
2470
|
+
* pigment's strongest and lands short. `name` is omitted for the standalone
|
|
2471
|
+
* `glaze.shadow()` / `glaze.glow()` factories, which have no scheme either.
|
|
2472
|
+
*/
|
|
2473
|
+
function warnLightingUnreachable(kind, name, scheme, step, reached) {
|
|
2474
|
+
if (reached >= step - LIGHTING_WARN_SLACK) return;
|
|
2475
|
+
const label = scheme ? schemeLabel(scheme.isDark, scheme.isHighContrast) : void 0;
|
|
2476
|
+
if (dedupe(`lighting|${kind}|${name ?? ""}|${label ?? ""}|${step.toFixed(2)}|${reached.toFixed(2)}`)) return;
|
|
2477
|
+
const subject = name === void 0 ? `glaze.${kind}()` : `${kind} "${name}"`;
|
|
2478
|
+
const where = label === void 0 ? "" : ` in ${label} scheme`;
|
|
2479
|
+
const extreme = kind === "shadow" ? "black" : "white";
|
|
2480
|
+
console.warn(`glaze: ${subject} cannot reach a ${step.toFixed(1)}-tone step${where} (reached ${reached.toFixed(1)} at full strength). Lower the intensity, move the surface away from ${extreme}, or raise a tuned-down \`reach\`.`);
|
|
2481
|
+
}
|
|
2319
2482
|
|
|
2320
2483
|
//#endregion
|
|
2321
2484
|
//#region src/resolver.ts
|
|
@@ -2329,13 +2492,13 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
|
|
|
2329
2492
|
* passes bypass the level entirely, so the high-contrast slots are the same
|
|
2330
2493
|
* values `'auto'` resolves. At level 100 the normal passes already are the
|
|
2331
2494
|
* high-contrast ones, so the solver drops to two passes (light → dark) and
|
|
2332
|
-
* mirrors the slots. Owns the per-scheme resolve helpers for regular, shadow
|
|
2333
|
-
* and mix color defs.
|
|
2495
|
+
* mirrors the slots. Owns the per-scheme resolve helpers for regular, shadow /
|
|
2496
|
+
* glow, and mix color defs.
|
|
2334
2497
|
*
|
|
2335
2498
|
* Variants are stored in OKHST: `h` / `s` are OKHSL hue/saturation and
|
|
2336
2499
|
* `t` is the canonical contrast-uniform tone (0–1, reference eps). The
|
|
2337
2500
|
* resolver works in tone for regular colors and converts to/from OKHSL
|
|
2338
|
-
* lightness only at the mix/shadow and luminance edges.
|
|
2501
|
+
* lightness only at the mix / shadow / glow and luminance edges.
|
|
2339
2502
|
*
|
|
2340
2503
|
* Every function receives a single `GlazeConfigResolved` so the full
|
|
2341
2504
|
* per-instance config (including overrides) is available without
|
|
@@ -2394,7 +2557,7 @@ function resolveRoleInMap(name, def, defs, inferRole, roles) {
|
|
|
2394
2557
|
const cached = roles.get(name);
|
|
2395
2558
|
if (cached) return cached;
|
|
2396
2559
|
let role;
|
|
2397
|
-
if (
|
|
2560
|
+
if (isLightingDef(def)) role = "surface";
|
|
2398
2561
|
else if (isMixDef(def)) role = normalizeRole(def.role) ?? (inferRole ? inferRoleFromName(name) : void 0) ?? resolveBaseRoleInMap(def.base, defs, inferRole, roles) ?? "text";
|
|
2399
2562
|
else {
|
|
2400
2563
|
const regDef = def;
|
|
@@ -2412,7 +2575,7 @@ function resolveRoleInMap(name, def, defs, inferRole, roles) {
|
|
|
2412
2575
|
* 4. `'text'` (foreground) default
|
|
2413
2576
|
*
|
|
2414
2577
|
* Memoized on `ctx.roles` so the four scheme passes share one resolution.
|
|
2415
|
-
* Shadows have no contrast participation and default to `'surface'`.
|
|
2578
|
+
* Shadows and glows have no contrast participation and default to `'surface'`.
|
|
2416
2579
|
*/
|
|
2417
2580
|
function resolveRole(name, def, ctx) {
|
|
2418
2581
|
return resolveRoleInMap(name, def, ctx.defs, ctx.config.inferRole, ctx.roles);
|
|
@@ -2431,7 +2594,7 @@ function passTone(tone, isHighContrast, config) {
|
|
|
2431
2594
|
const f = contrastFraction(config);
|
|
2432
2595
|
return f === void 0 || isHighContrast ? parseToneValue(isHighContrast ? pairHC(tone) : pairNormal(tone)) : parseToneValueAt(tone, f);
|
|
2433
2596
|
}
|
|
2434
|
-
/** The authored numeric pair for this pass (shadow `intensity`, mix `value`). */
|
|
2597
|
+
/** The authored numeric pair for this pass (shadow / glow `intensity`, mix `value`). */
|
|
2435
2598
|
function passNumber(p, isHighContrast, config) {
|
|
2436
2599
|
const f = contrastFraction(config);
|
|
2437
2600
|
if (f === void 0 || isHighContrast) return isHighContrast ? pairHC(p) : pairNormal(p);
|
|
@@ -2609,7 +2772,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2609
2772
|
return clamp(preferredTone, 0, 100);
|
|
2610
2773
|
}
|
|
2611
2774
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
2612
|
-
if (
|
|
2775
|
+
if (isLightingDef(def)) return resolveLightingForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2613
2776
|
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2614
2777
|
const regDef = def;
|
|
2615
2778
|
const mode = regDef.mode ?? "auto";
|
|
@@ -2627,15 +2790,33 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2627
2790
|
pastel
|
|
2628
2791
|
};
|
|
2629
2792
|
}
|
|
2630
|
-
|
|
2793
|
+
/**
|
|
2794
|
+
* Shadow / glow: solve the pigment opacity that lands this scheme's `bg`
|
|
2795
|
+
* exactly `intensity × k` tones darker / lighter. `bg` and `fg` are read per
|
|
2796
|
+
* scheme, so the object factor `k` adapts on its own in dark and high contrast.
|
|
2797
|
+
*/
|
|
2798
|
+
function resolveLightingForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
2631
2799
|
const bgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast));
|
|
2632
|
-
|
|
2633
|
-
if (def.fg) fgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast));
|
|
2800
|
+
const fgVariant = def.fg ? toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast)) : void 0;
|
|
2634
2801
|
const intensity = passNumber(def.intensity, isHighContrast, ctx.config);
|
|
2635
|
-
const tuning =
|
|
2802
|
+
const tuning = resolveLightingTuning(def.tuning, def.type === "shadow" ? ctx.config.shadowTuning : ctx.config.glowTuning);
|
|
2803
|
+
const pastel = def.pastel ?? ctx.config.pastel;
|
|
2804
|
+
const result = computeLighting(def.type, {
|
|
2805
|
+
...bgVariant,
|
|
2806
|
+
pastel: bgVariant.pastel ?? ctx.config.pastel
|
|
2807
|
+
}, fgVariant, intensity, tuning, pastel);
|
|
2808
|
+
warnLightingUnreachable(def.type, name, {
|
|
2809
|
+
isDark,
|
|
2810
|
+
isHighContrast
|
|
2811
|
+
}, result.step, result.reached);
|
|
2636
2812
|
return {
|
|
2637
|
-
...toToneVariant(
|
|
2638
|
-
|
|
2813
|
+
...toToneVariant({
|
|
2814
|
+
h: result.h,
|
|
2815
|
+
s: result.s,
|
|
2816
|
+
l: result.l,
|
|
2817
|
+
alpha: result.alpha
|
|
2818
|
+
}),
|
|
2819
|
+
pastel
|
|
2639
2820
|
};
|
|
2640
2821
|
}
|
|
2641
2822
|
function okhslVariantToLinearRgb(v, pastel) {
|
|
@@ -2691,7 +2872,11 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2691
2872
|
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, ctx.config, polarity);
|
|
2692
2873
|
const metric = resolvedContrast.metric;
|
|
2693
2874
|
let luminanceAt;
|
|
2694
|
-
if (blend === "transparent"
|
|
2875
|
+
if (blend === "transparent") {
|
|
2876
|
+
const baseGamma = toDisplayGamma(baseLinear);
|
|
2877
|
+
const targetGamma = toDisplayGamma(targetLinear);
|
|
2878
|
+
luminanceAt = (v) => metricLuminance(metric, compositeOverGamma(baseGamma, targetGamma, v));
|
|
2879
|
+
} else if (space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
|
|
2695
2880
|
else luminanceAt = (v) => {
|
|
2696
2881
|
return metricLuminance(metric, okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v, pastel));
|
|
2697
2882
|
};
|
|
@@ -2728,7 +2913,7 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2728
2913
|
};
|
|
2729
2914
|
}
|
|
2730
2915
|
function defMode(def) {
|
|
2731
|
-
if (
|
|
2916
|
+
if (isLightingDef(def) || isMixDef(def)) return void 0;
|
|
2732
2917
|
return def.mode ?? "auto";
|
|
2733
2918
|
}
|
|
2734
2919
|
/**
|
|
@@ -2772,9 +2957,10 @@ function seedField(order, ctx, field, source) {
|
|
|
2772
2957
|
}
|
|
2773
2958
|
}
|
|
2774
2959
|
/**
|
|
2775
|
-
* After the passes, surface chromatic contrast drift (
|
|
2776
|
-
* resolved with a `base` + `contrast` may land
|
|
2777
|
-
* its tone implies because chromatic luminance
|
|
2960
|
+
* After the passes, surface chromatic contrast drift (the drift check in
|
|
2961
|
+
* docs/okhst.mdx): a color resolved with a `base` + `contrast` may land
|
|
2962
|
+
* slightly under the contrast its tone implies because chromatic luminance
|
|
2963
|
+
* drifts from the gray tone.
|
|
2778
2964
|
*
|
|
2779
2965
|
* Under a manual `contrastLevel` the spec is resolved at the level, so the check
|
|
2780
2966
|
* measures each normal variant against the target it was actually solved for. At
|
|
@@ -2786,7 +2972,7 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2786
2972
|
const roles = /* @__PURE__ */ new Map();
|
|
2787
2973
|
for (const name of order) {
|
|
2788
2974
|
const def = defs[name];
|
|
2789
|
-
if (
|
|
2975
|
+
if (isLightingDef(def) || isMixDef(def)) continue;
|
|
2790
2976
|
const regDef = def;
|
|
2791
2977
|
if (regDef.contrast === void 0 || !regDef.base) continue;
|
|
2792
2978
|
const color = result.get(name);
|
|
@@ -2896,7 +3082,7 @@ function schemeSeedHue(ctx, scheme) {
|
|
|
2896
3082
|
return scheme === "dark" ? ctx.darkSeedHue ?? ctx.seedHue : ctx.seedHue;
|
|
2897
3083
|
}
|
|
2898
3084
|
function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
2899
|
-
if (def === void 0 ||
|
|
3085
|
+
if (def === void 0 || isLightingDef(def) || isMixDef(def) || isAchromatic(variant)) return {
|
|
2900
3086
|
hueVar: "",
|
|
2901
3087
|
inline: true,
|
|
2902
3088
|
declarations: []
|
|
@@ -4080,10 +4266,11 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
|
|
|
4080
4266
|
return acc;
|
|
4081
4267
|
}
|
|
4082
4268
|
/** Rebuild a theme from a `theme.export()` snapshot. */
|
|
4083
|
-
function createThemeFromExport(data, factory = "glaze.themeFrom") {
|
|
4269
|
+
function createThemeFromExport(data, factory = "glaze.themeFrom", fallbackVersion) {
|
|
4084
4270
|
if (data === null || typeof data !== "object") throw new Error(`${factory}: expected an object from theme.export(), got ${data === null ? "null" : typeof data}.`);
|
|
4085
4271
|
assertExportKind(data, "theme", factory);
|
|
4086
4272
|
assertExportVersion(data, factory);
|
|
4273
|
+
assertNoLegacyShadows(data, factory, fallbackVersion);
|
|
4087
4274
|
if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
|
|
4088
4275
|
for (const field of ["darkHue", "darkSaturation"]) if (data[field] !== void 0 && typeof data[field] !== "number") throw new Error(`${factory}: expected "${field}" to be a number when present (got ${JSON.stringify(data[field])}).`);
|
|
4089
4276
|
return createTheme({
|
|
@@ -4102,7 +4289,7 @@ function createPaletteFromExport(data) {
|
|
|
4102
4289
|
assertExportVersion(data, "glaze.paletteFrom");
|
|
4103
4290
|
if (data.themes === null || typeof data.themes !== "object") throw new Error(`glaze.paletteFrom: expected a "themes" object map of theme exports.`);
|
|
4104
4291
|
const rebuilt = {};
|
|
4105
|
-
for (const [name, themeExport] of Object.entries(data.themes)) rebuilt[name] = createThemeFromExport(themeExport, `glaze.paletteFrom (theme "${name}")
|
|
4292
|
+
for (const [name, themeExport] of Object.entries(data.themes)) rebuilt[name] = createThemeFromExport(themeExport, `glaze.paletteFrom (theme "${name}")`, data.version);
|
|
4106
4293
|
return createPalette(rebuilt, { primary: data.primary });
|
|
4107
4294
|
}
|
|
4108
4295
|
function createPalette(themes, paletteOptions) {
|
|
@@ -4239,42 +4426,17 @@ function createPalette(themes, paletteOptions) {
|
|
|
4239
4426
|
* - `theme.ts` — single-theme factory
|
|
4240
4427
|
* - `palette.ts` — multi-theme composition
|
|
4241
4428
|
* - `color-token.ts` — standalone single-color tokens (`glaze.color`)
|
|
4242
|
-
* - `
|
|
4429
|
+
* - `lighting.ts` — shadow / glow solver (`glaze.shadow`, `glaze.glow`)
|
|
4243
4430
|
* - `formatters.ts` — variant → string (`glaze.format`)
|
|
4244
4431
|
* - `config.ts` — global config singleton
|
|
4245
4432
|
* - `serialize.ts` — authoring export type guards / version checks
|
|
4246
4433
|
*/
|
|
4247
|
-
|
|
4248
|
-
* Create a single-hue glaze theme.
|
|
4249
|
-
*
|
|
4250
|
-
* An optional `config` override can be supplied to customize the resolve
|
|
4251
|
-
* behavior for this theme (tone windows, etc.). The
|
|
4252
|
-
* override is **merged over the live global config at resolve time** —
|
|
4253
|
-
* the theme still reacts to later `configure()` calls for fields it
|
|
4254
|
-
* didn't override.
|
|
4255
|
-
*
|
|
4256
|
-
* The object form also accepts `darkHue` / `darkSaturation` — a separate
|
|
4257
|
-
* seed pair for the dark and dark-high-contrast schemes. Both default to
|
|
4258
|
-
* their light counterparts, and an explicit `darkSaturation` opts the theme
|
|
4259
|
-
* out of the global `darkDesaturation` reduction.
|
|
4260
|
-
*
|
|
4261
|
-
* @example
|
|
4262
|
-
* ```ts
|
|
4263
|
-
* const primary = glaze(280, 80);
|
|
4264
|
-
* // or shorthand:
|
|
4265
|
-
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
4266
|
-
* // with a retuned dark seed:
|
|
4267
|
-
* const primary = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
|
|
4268
|
-
* // with config override:
|
|
4269
|
-
* const raw = glaze(280, 80, { lightTone: false });
|
|
4270
|
-
* ```
|
|
4271
|
-
*/
|
|
4272
|
-
function glaze(hueOrOptions, saturation, config) {
|
|
4434
|
+
function glaze(hueOrOptions, saturationOrConfig, config) {
|
|
4273
4435
|
if (typeof hueOrOptions === "number") return createTheme({
|
|
4274
4436
|
hue: hueOrOptions,
|
|
4275
|
-
saturation:
|
|
4437
|
+
saturation: (typeof saturationOrConfig === "number" ? saturationOrConfig : void 0) ?? 100
|
|
4276
4438
|
}, void 0, config);
|
|
4277
|
-
return createTheme(hueOrOptions, void 0, config);
|
|
4439
|
+
return createTheme(hueOrOptions, void 0, typeof saturationOrConfig === "object" ? saturationOrConfig : config);
|
|
4278
4440
|
}
|
|
4279
4441
|
/** Configure global glaze settings. */
|
|
4280
4442
|
glaze.configure = function configure$1(config) {
|
|
@@ -4345,25 +4507,14 @@ glaze.color = function color(input, config) {
|
|
|
4345
4507
|
if ("hue" in obj) return createColorToken(input, config);
|
|
4346
4508
|
return createColorTokenFromValue(input, void 0, config);
|
|
4347
4509
|
};
|
|
4348
|
-
/**
|
|
4349
|
-
|
|
4350
|
-
*
|
|
4351
|
-
* Both `bg` and `fg` accept any `GlazeColorValue` form: hex (`#rgb` /
|
|
4352
|
-
* `#rrggbb` / `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `oklch()`
|
|
4353
|
-
* strings, or `{ r, g, b }` / `{ h, s, l }` / `{ l, c, h }` objects.
|
|
4354
|
-
*/
|
|
4355
|
-
glaze.shadow = function shadow(input) {
|
|
4510
|
+
/** Shared body of `glaze.shadow()` / `glaze.glow()`. */
|
|
4511
|
+
function standaloneLighting(kind, input) {
|
|
4356
4512
|
const bg = extractOkhslFromValue(input.bg);
|
|
4357
4513
|
const fg = input.fg ? extractOkhslFromValue(input.fg) : void 0;
|
|
4358
4514
|
const cfg = getConfig();
|
|
4359
|
-
const tuning =
|
|
4360
|
-
const result =
|
|
4361
|
-
|
|
4362
|
-
alpha: 1
|
|
4363
|
-
}, fg ? {
|
|
4364
|
-
...fg,
|
|
4365
|
-
alpha: 1
|
|
4366
|
-
} : void 0, input.intensity, tuning);
|
|
4515
|
+
const tuning = resolveLightingTuning(input.tuning, kind === "shadow" ? cfg.shadowTuning : cfg.glowTuning);
|
|
4516
|
+
const result = computeLighting(kind, bg, fg, input.intensity, tuning, false);
|
|
4517
|
+
warnLightingUnreachable(kind, void 0, void 0, result.step, result.reached);
|
|
4367
4518
|
const { h, s, t } = okhslToOkhst({
|
|
4368
4519
|
h: result.h,
|
|
4369
4520
|
s: result.s,
|
|
@@ -4375,6 +4526,27 @@ glaze.shadow = function shadow(input) {
|
|
|
4375
4526
|
t,
|
|
4376
4527
|
alpha: result.alpha
|
|
4377
4528
|
};
|
|
4529
|
+
}
|
|
4530
|
+
/**
|
|
4531
|
+
* Compute a shadow over a surface: a translucent pigment whose opacity lands
|
|
4532
|
+
* `bg` exactly `intensity` tones darker once the browser composites it. The
|
|
4533
|
+
* optional object `fg` scales the step (a darker object casts a deeper
|
|
4534
|
+
* shadow).
|
|
4535
|
+
*
|
|
4536
|
+
* Both `bg` and `fg` accept any `GlazeColorValue` form: hex (`#rgb` /
|
|
4537
|
+
* `#rrggbb` / `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `oklch()`
|
|
4538
|
+
* strings, or `{ r, g, b }` / `{ h, s, l }` / `{ l, c, h }` objects.
|
|
4539
|
+
*/
|
|
4540
|
+
glaze.shadow = function shadow(input) {
|
|
4541
|
+
return standaloneLighting("shadow", input);
|
|
4542
|
+
};
|
|
4543
|
+
/**
|
|
4544
|
+
* Compute a glow over a surface — the mirror of `glaze.shadow()`: the
|
|
4545
|
+
* composite lands `bg` exactly `intensity` tones lighter, and a lighter
|
|
4546
|
+
* object `fg` glows stronger. Accepts the same inputs.
|
|
4547
|
+
*/
|
|
4548
|
+
glaze.glow = function glow(input) {
|
|
4549
|
+
return standaloneLighting("glow", input);
|
|
4378
4550
|
};
|
|
4379
4551
|
/** Format a resolved color variant as a CSS string. */
|
|
4380
4552
|
glaze.format = function format(variant, colorFormat, pastel) {
|