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