@tenphi/glaze 0.0.0-snapshot.c13086f → 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 +158 -0
- package/README.md +106 -1015
- package/dist/index.cjs +3991 -973
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1559 -157
- package/dist/index.d.mts +1559 -157
- package/dist/index.mjs +3959 -973
- 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 +278 -0
- 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 +18 -9
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Utilities
|
|
3
|
+
description: glaze.format, standalone glaze.shadow and glaze.glow, and the color math, tone, contrast-solver, role, and guard functions Glaze exports for custom pipelines.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Most code only needs `glaze()` and its output methods. The functions on this page are for formatting single values, computing an overlay outside a theme, and building your own pipeline on Glaze's math. Everything here is exported from `'@tenphi/glaze'`.
|
|
7
|
+
|
|
8
|
+
**Scales.** Hue is 0–360 everywhere. Saturation, OKHSL lightness, and stored tone are **0–1** in every converter, writer, and `ResolvedColorVariant`. Only the tone transfers `toTone` / `fromTone` / `toneFromY` / `yFromTone` speak the 0–100 tone axis that authoring uses. See [Scales](./index.md#scales).
|
|
9
|
+
|
|
10
|
+
## `glaze.format(variant, format?, pastel?)`
|
|
11
|
+
|
|
12
|
+
Formats one resolved variant as a color string, alpha included.
|
|
13
|
+
|
|
14
|
+
| Parameter | Type | Default | Description |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| `variant` | `ResolvedColorVariant` | — | `{ h, s, t, alpha, pastel? }` from `resolve()`, `glaze.shadow()`, or `glaze.glow()`. |
|
|
17
|
+
| `format` | `GlazeColorFormat` | `'oklch'` | `'oklch'`, `'rgb'`, `'hsl'`, `'okhsl'`, or `'okhst'`. Syntax per format: [Color formats](./output.md#color-formats). |
|
|
18
|
+
| `pastel` | `boolean` | `false` | Gamut mapping for a variant that carries no `pastel` flag. A variant's own `pastel` always wins. |
|
|
19
|
+
|
|
20
|
+
- All five formats are accepted. The native-format guard applies to the output methods, not to `glaze.format()`.
|
|
21
|
+
- An `alpha` below 1 is appended as `/ A` (up to 4 decimals).
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { glaze } from '@tenphi/glaze';
|
|
25
|
+
|
|
26
|
+
const shade = glaze.shadow({ bg: '#ffffff', intensity: 6 });
|
|
27
|
+
|
|
28
|
+
glaze.format(shade); // 'oklch(0 0 0 / 0.0814)'
|
|
29
|
+
glaze.format(shade, 'rgb'); // 'rgb(0 0 0 / 0.0814)'
|
|
30
|
+
glaze.format(shade, 'okhst'); // 'okhst(0 0% 0% / 0.0814)'
|
|
31
|
+
|
|
32
|
+
const accent = glaze.color(
|
|
33
|
+
{ hue: 280, saturation: 80, tone: 60 },
|
|
34
|
+
{ pastel: true },
|
|
35
|
+
);
|
|
36
|
+
const light = accent.resolve().light; // light.pastel === true
|
|
37
|
+
|
|
38
|
+
glaze.format(light); // 'oklch(0.6542 0.089 280)'
|
|
39
|
+
|
|
40
|
+
const plain = { ...light, pastel: undefined }; // read as non-pastel
|
|
41
|
+
glaze.format(plain); // 'oklch(0.6542 0.1604 280)'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## `glaze.shadow(input)` / `glaze.glow(input)`
|
|
45
|
+
|
|
46
|
+
Compute a shadow or glow overlay over one surface, outside a theme. The result is the pigment and the opacity that lands the composite exactly `intensity × k` tones darker (`shadow`) or lighter (`glow`) than `bg`, the same solve a theme runs per scheme. How it works: [Shadows and glows](../shadows-and-glows.mdx).
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
glaze.shadow(input: GlazeShadowInput): ResolvedColorVariant
|
|
50
|
+
glaze.glow(input: GlazeGlowInput): ResolvedColorVariant
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`GlazeShadowInput` and `GlazeGlowInput` are aliases of `GlazeLightingInput`:
|
|
54
|
+
|
|
55
|
+
| Field | Type | Description |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `bg` | `GlazeColorValue` | The surface. Any [color value](./color-tokens.md) form: hex (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `okhst()` / `oklch()` strings, or `{ r, g, b }` / `{ h, s, l }` / `{ h, s, t }` / `{ l, c, h }` objects. An alpha component is dropped with a warning. |
|
|
58
|
+
| `fg` | `GlazeColorValue` | Optional. The object casting the shadow or emitting the glow. Same forms. Without it, `k = 1`. |
|
|
59
|
+
| `intensity` | `number` | The tone step, 0–100. Clamped. |
|
|
60
|
+
| `tuning` | `LightingTuning` | Optional. Merges field by field over the global `shadowTuning` / `glowTuning` and the defaults. Fields: [`LightingTuning`](./color-definitions.md#lightingtuning). |
|
|
61
|
+
|
|
62
|
+
- The object factor is `k = max(0, 1 + β · d · (t_bg − t_fg) / 100)`, with `d = +1` for a shadow, `−1` for a glow, and `β = objectInfluence`.
|
|
63
|
+
- There are no schemes here: call it once per surface you composite onto. Per-instance config overrides don't apply; only the global tuning does.
|
|
64
|
+
- The result is `{ h, s, t, alpha }` with no `pastel` flag. Format it with `glaze.format()`.
|
|
65
|
+
- Compositing is modeled the way browsers paint (`source-over` on gamma-encoded sRGB), and the pigment is an sRGB color, so a glow can't go past sRGB white.
|
|
66
|
+
- An out-of-reach step pins to the pigment's strongest composite and warns `glaze: glaze.glow() cannot reach a 5.0-tone step …` ([details](./configuration.md#shadow-or-glow-out-of-reach)).
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
glaze.shadow({ bg: '#ffffff', intensity: 6 });
|
|
70
|
+
// → { h: 0, s: 0, t: 0, alpha: 0.0814… }
|
|
71
|
+
|
|
72
|
+
// The same 6-tone step needs more opacity on darker surfaces:
|
|
73
|
+
const mid = { h: 0, s: 0, t: 0.5 };
|
|
74
|
+
const deep = { h: 0, s: 0, t: 0.1 };
|
|
75
|
+
glaze.shadow({ bg: mid, intensity: 6 }).alpha; // 0.1066…
|
|
76
|
+
glaze.shadow({ bg: deep, intensity: 6 }).alpha; // 0.4769…
|
|
77
|
+
|
|
78
|
+
const lift = glaze.glow({ bg: '#1e1e22', fg: '#3a3a44', intensity: 4 });
|
|
79
|
+
glaze.format(lift); // 'oklch(1 0 0 / 0.0466)'
|
|
80
|
+
|
|
81
|
+
glaze.glow({
|
|
82
|
+
bg: 'oklch(0.3 0.02 280)',
|
|
83
|
+
intensity: 6,
|
|
84
|
+
tuning: { tint: 0.3, reach: 0.85 },
|
|
85
|
+
});
|
|
86
|
+
// → { h: 280, s: 0.3, t: 0.8712…, alpha: 0.0745… }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Color math
|
|
90
|
+
|
|
91
|
+
Glaze's color science, exported as plain functions over tuples. `pastel` (default `false`) selects the hue-independent "safe" chroma bound instead of the per-hue sRGB gamut, matching a color's [`pastel`](./color-definitions.md#per-color-pastel) flag.
|
|
92
|
+
|
|
93
|
+
### Conversions
|
|
94
|
+
|
|
95
|
+
| Function | Returns | Description |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `okhslToLinearSrgb(h, s, l, pastel?)` | `[r, g, b]` | OKHSL → linear sRGB. Channels can leave 0–1 near the gamut edge; clamp if you need to. |
|
|
98
|
+
| `okhslToSrgb(h, s, l, pastel?)` | `[r, g, b]` | OKHSL → gamma-encoded sRGB, clamped to 0–1. |
|
|
99
|
+
| `okhslToOklab(h, s, l, pastel?)` | `[L, a, b]` | OKHSL → OKLab. |
|
|
100
|
+
| `okhslToOklch(h, s, l, pastel?)` | `[L, C, H]` | OKHSL → OKLCh (`L` 0–1, `C` chroma, `H` degrees). Same math as the `oklch` output. |
|
|
101
|
+
| `oklabToOkhsl([L, a, b], pastel?)` | `[h, s, l]` | OKLab → OKHSL. Achromatic colors and the black / white extremes return `[0, 0, l]`. |
|
|
102
|
+
| `srgbToOkhsl([r, g, b], pastel?)` | `[h, s, l]` | Gamma-encoded sRGB (0–1) → OKHSL. |
|
|
103
|
+
| `hslToSrgb(h, s, l)` | `[r, g, b]` | CSS HSL (`s`, `l` 0–1) → gamma-encoded sRGB. CSS HSL is not OKHSL; chain with `srgbToOkhsl`. |
|
|
104
|
+
| `srgbToHex([r, g, b])` | `` `#${string}` `` | Gamma-encoded sRGB → `#rrggbb`. Channels are clamped and rounded to 8 bits; no alpha. |
|
|
105
|
+
| `parseHex(hex)` | `[r, g, b] \| null` | `#rgb` / `#rrggbb` → sRGB 0–1. The `#` is optional. Returns `null` for a wrong length, a non-hex digit, and forms with alpha. In the 6-digit form only the first digit of each pair is checked: `'#1z2z3z'` reads as `#010203`. |
|
|
106
|
+
| `parseHexAlpha(hex)` | `{ rgb, alpha? } \| null` | `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa`. `alpha` (0–1) is present only for the alpha forms. Same digit check as `parseHex`. |
|
|
107
|
+
| `cuspLightness(h)` | `number` | OKHSL lightness (0.001–0.999) where the sRGB gamut's chroma peaks for hue `h`. Cached per hue. |
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import {
|
|
111
|
+
okhslToSrgb,
|
|
112
|
+
okhslToOklch,
|
|
113
|
+
srgbToOkhsl,
|
|
114
|
+
srgbToHex,
|
|
115
|
+
parseHex,
|
|
116
|
+
parseHexAlpha,
|
|
117
|
+
cuspLightness,
|
|
118
|
+
} from '@tenphi/glaze';
|
|
119
|
+
|
|
120
|
+
okhslToSrgb(280, 0.6, 0.5); // [0.4118…, 0.4221…, 0.7644…]
|
|
121
|
+
okhslToOklch(280, 0.6, 0.5); // [0.5688…, 0.1326…, 280]
|
|
122
|
+
srgbToOkhsl(parseHex('#7a4dbf')!); // [298.5176…, 0.7040…, 0.4527…]
|
|
123
|
+
srgbToHex([122 / 255, 77 / 255, 191 / 255]); // '#7a4dbf'
|
|
124
|
+
parseHex('#7a4dbf80'); // null
|
|
125
|
+
parseHexAlpha('#7a4dbf80');
|
|
126
|
+
// → { rgb: [0.4784…, 0.3019…, 0.7490…], alpha: 0.5019… }
|
|
127
|
+
cuspLightness(110); // 0.9619… (yellow peaks light)
|
|
128
|
+
cuspLightness(265); // 0.3688… (blue peaks dark)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Luminance and contrast
|
|
132
|
+
|
|
133
|
+
| Function | Returns | Description |
|
|
134
|
+
| --- | --- | --- |
|
|
135
|
+
| `relativeLuminanceFromLinearRgb([r, g, b])` | `number` | WCAG relative luminance, `0.2126·R + 0.7152·G + 0.0722·B`, with no clamping. |
|
|
136
|
+
| `gamutClampedLuminance([r, g, b])` | `number` | The same after gamma-encoding, clamping to 0–1, and linearizing again — the luminance a browser actually paints. Glaze's WCAG solver uses this one. |
|
|
137
|
+
| `contrastRatioFromLuminance(yA, yB)` | `number` | WCAG ratio `(lighter + 0.05) / (darker + 0.05)`. Argument order doesn't matter. |
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
gamutClampedLuminance([1.2, 0.5, -0.1]); // 0.5702
|
|
141
|
+
relativeLuminanceFromLinearRgb([1.2, 0.5, -0.1]); // 0.6054…
|
|
142
|
+
contrastRatioFromLuminance(1, 0); // 21
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
APCA contrast is [`apcaContrast`](#apcacontrastytext-ybg) below.
|
|
146
|
+
|
|
147
|
+
### Format writers
|
|
148
|
+
|
|
149
|
+
Write one OKHSL (or OKHST) color as a string. Every writer takes `h` on 0–360 and `s` / `l` / `t` on **0–1** — the scale every converter returns — and scales to percentages itself.
|
|
150
|
+
|
|
151
|
+
| Function | Output |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `formatOkhsl(h, s, l, pastel?)` | `okhsl(H S% L%)` |
|
|
154
|
+
| `formatOkhst(h, s, t, pastel?)` | `okhst(H S% T%)` — `t` is stored tone on 0–1 |
|
|
155
|
+
| `formatRgb(h, s, l, pastel?)` | `rgb(R G B)`, channels 0–255 with up to 2 decimals |
|
|
156
|
+
| `formatHsl(h, s, l, pastel?)` | `hsl(H S% L%)` |
|
|
157
|
+
| `formatOklch(h, s, l, pastel?)` | `oklch(L C H)`, `L` / `C` to 4 decimals, `H` to 2 |
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import {
|
|
161
|
+
formatOkhsl,
|
|
162
|
+
formatOkhst,
|
|
163
|
+
formatRgb,
|
|
164
|
+
formatHsl,
|
|
165
|
+
formatOklch,
|
|
166
|
+
} from '@tenphi/glaze';
|
|
167
|
+
|
|
168
|
+
formatOkhsl(280, 0.6, 0.95); // 'okhsl(280 60% 95%)'
|
|
169
|
+
formatOkhst(280, 0.6, 0.95); // 'okhst(280 60% 95%)'
|
|
170
|
+
formatRgb(280, 0.6, 0.95); // 'rgb(238.45 239.95 251.1)'
|
|
171
|
+
formatHsl(280, 0.6, 0.95); // 'hsl(232.92 61.87% 95.99%)'
|
|
172
|
+
formatOklch(280, 0.6, 0.95); // 'oklch(0.9571 0.015 280)'
|
|
173
|
+
formatOklch(0, 0, 0.5); // 'oklch(0.5688 0 0)'
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- With `pastel: true`, `formatOkhsl` and `formatOkhst` re-express the saturation as its standard OKHSL equivalent, so parsers that don't know about pastel render the same color: `formatOkhsl(280, 0.6, 0.6, true)` → `'okhsl(280 34.46% 60%)'`. The other writers convert the pastel color directly.
|
|
177
|
+
- The writers emit no alpha. For a string with alpha, use [`glaze.format()`](#glazeformatvariant-format-pastel) on a variant.
|
|
178
|
+
- A value above 1 means pre-2.0 percentage input: each writer warns once per process ([Percentage scale](./configuration.md#percentage-scale)) and still formats the value.
|
|
179
|
+
|
|
180
|
+
A resolved variant composes with the writers directly; `variantToOkhsl` turns its tone into OKHSL lightness:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import {
|
|
184
|
+
glaze,
|
|
185
|
+
variantToOkhsl,
|
|
186
|
+
formatOkhst,
|
|
187
|
+
formatOkhsl,
|
|
188
|
+
formatRgb,
|
|
189
|
+
} from '@tenphi/glaze';
|
|
190
|
+
|
|
191
|
+
const v = glaze.color('#7A4DBF').resolve().light;
|
|
192
|
+
const { h, s, l } = variantToOkhsl(v);
|
|
193
|
+
|
|
194
|
+
formatOkhst(v.h, v.s, v.t); // 'okhst(298.52 70.41% 45.02%)'
|
|
195
|
+
formatOkhsl(h, s, l); // 'okhsl(298.52 70.41% 45.27%)'
|
|
196
|
+
formatRgb(h, s, l); // 'rgb(122 77 191)'
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## OKHST tone utilities
|
|
200
|
+
|
|
201
|
+
The tone transfer behind OKHST. The model and why `eps` is `0.05`: [Tone and OKHST](../okhst.mdx).
|
|
202
|
+
|
|
203
|
+
| Export | Description |
|
|
204
|
+
| --- | --- |
|
|
205
|
+
| `toTone(l, eps = REF_EPS)` | OKHSL lightness (0–1) → tone (**0–100**). Divide by 100 for `formatOkhst` or a variant's `t`. |
|
|
206
|
+
| `fromTone(t, eps = REF_EPS)` | Tone (**0–100**) → OKHSL lightness (0–1). Inverse of `toTone`. |
|
|
207
|
+
| `toneFromY(y, eps = REF_EPS)` | Luminance (0–1) → tone (0–100): `100 · ln((y + eps) / eps) / ln((1 + eps) / eps)`. |
|
|
208
|
+
| `yFromTone(t, eps = REF_EPS)` | Tone (0–100) → luminance (0–1). Inverse of `toneFromY`. |
|
|
209
|
+
| `okhstToOkhsl({ h, s, t })` | OKHST (`t` 0–1) → `{ h, s, l }`. |
|
|
210
|
+
| `okhslToOkhst({ h, s, l })` | OKHSL → `{ h, s, t }` (`t` 0–1). |
|
|
211
|
+
| `variantToOkhsl(variant)` | A `ResolvedColorVariant` → `{ h, s, l }` for rendering. `alpha` and `pastel` stay on the variant. |
|
|
212
|
+
| `REF_EPS` | `0.05` — the reference eps of the canonical tone axis. |
|
|
213
|
+
|
|
214
|
+
`toTone` / `fromTone` measure luminance as a gray of that lightness, so they are exact for neutrals; a chromatic color's real luminance drifts slightly from its tone.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import {
|
|
218
|
+
toTone,
|
|
219
|
+
fromTone,
|
|
220
|
+
toneFromY,
|
|
221
|
+
yFromTone,
|
|
222
|
+
okhslToOkhst,
|
|
223
|
+
} from '@tenphi/glaze';
|
|
224
|
+
|
|
225
|
+
toTone(0.5); // 50.6998…
|
|
226
|
+
fromTone(50); // 0.4941…
|
|
227
|
+
toneFromY(0.18); // 50.12…
|
|
228
|
+
yFromTone(50); // 0.1791…
|
|
229
|
+
okhslToOkhst({ h: 280, s: 0.6, l: 0.5 });
|
|
230
|
+
// → { h: 280, s: 0.6, t: 0.5069… }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Contrast solver
|
|
234
|
+
|
|
235
|
+
The solver Glaze runs for `contrast` floors, exported for tools that work outside a theme. Concepts: [Contrast solving](../contrast.mdx). The spec forms (`ContrastSpec`, presets, high-contrast pairs) are defined in [`ContrastSpec`](./contrast.md#contrastspec).
|
|
236
|
+
|
|
237
|
+
### Resolving a spec
|
|
238
|
+
|
|
239
|
+
| Function | Returns | Description |
|
|
240
|
+
| --- | --- | --- |
|
|
241
|
+
| `resolveMinContrast(value)` | `number` | A `MinContrast` → WCAG ratio. Presets: `'AA'` 4.5, `'AAA'` 7, `'AA-large'` 3, `'AAA-large'` 4.5. Numbers below 1 become 1. |
|
|
242
|
+
| `resolveApcaTarget(value)` | `number` | An APCA Lc number or `ApcaPreset` → Lc. Numbers are taken as magnitudes (`-60` → `60`). |
|
|
243
|
+
| `resolveContrastForMode(spec, isHighContrast, polarity?, outerExplicitHC?)` | `ResolvedContrast` | One `ContrastSpec` (already picked from any outer pair) → `{ metric, target, polarity? }` for normal or high contrast. Picks the entry of an inner `{ wcag: [...] }` / `{ apca: [...] }` pair. |
|
|
244
|
+
| `resolveContrastForLevel(spec, level, polarity?)` | `ResolvedContrast` | An `HCPair<ContrastSpec>` at a manual contrast level (0–100). Resolves both ends with `resolveContrastForMode` and interpolates the target linearly. |
|
|
245
|
+
|
|
246
|
+
High-contrast escalation in `resolveContrastForMode`, applied only when `isHighContrast` is `true`, `outerExplicitHC` is not set, and the spec has no inner pair:
|
|
247
|
+
|
|
248
|
+
- WCAG `'AA'` → `'AAA'` (7) and `'AA-large'` → `'AAA-large'` (4.5). AAA presets and bare numbers are unchanged.
|
|
249
|
+
- APCA: `+APCA_HC_ENHANCEMENT` (15) Lc, capped at `APCA_MAX_LC` (106).
|
|
250
|
+
|
|
251
|
+
`polarity` (`'fg'` default, or `'bg'`) is copied onto APCA results; WCAG results have no `polarity`. `resolveContrastForLevel` returns the plain normal / high-contrast result at levels 0 and 100, and throws for any level in between when the two ends use different metrics (`glaze: a "contrast" pair cannot switch metric between its normal (wcag) and high-contrast (apca) entry — …`).
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import {
|
|
255
|
+
resolveContrastForMode,
|
|
256
|
+
resolveContrastForLevel,
|
|
257
|
+
} from '@tenphi/glaze';
|
|
258
|
+
|
|
259
|
+
resolveContrastForMode('AA', false);
|
|
260
|
+
// → { metric: 'wcag', target: 4.5 }
|
|
261
|
+
resolveContrastForMode('AA', true);
|
|
262
|
+
// → { metric: 'wcag', target: 7 }
|
|
263
|
+
resolveContrastForMode('AA', true, undefined, true);
|
|
264
|
+
// → { metric: 'wcag', target: 4.5 }
|
|
265
|
+
resolveContrastForMode({ wcag: ['AA', 'AA'] }, true);
|
|
266
|
+
// → { metric: 'wcag', target: 4.5 }
|
|
267
|
+
resolveContrastForMode({ apca: 75 }, true);
|
|
268
|
+
// → { metric: 'apca', target: 90, polarity: 'fg' }
|
|
269
|
+
resolveContrastForMode({ apca: 'preferred' }, true, 'bg');
|
|
270
|
+
// → { metric: 'apca', target: 105, polarity: 'bg' }
|
|
271
|
+
resolveContrastForMode({ apca: [60, 80] }, true);
|
|
272
|
+
// → { metric: 'apca', target: 80, polarity: 'fg' }
|
|
273
|
+
|
|
274
|
+
resolveContrastForLevel('AA', 50);
|
|
275
|
+
// → { metric: 'wcag', target: 5.75 }
|
|
276
|
+
resolveContrastForLevel({ apca: 60 }, 50, 'bg');
|
|
277
|
+
// → { metric: 'apca', target: 67.5, polarity: 'bg' }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `apcaContrast(yText, yBg)`
|
|
281
|
+
|
|
282
|
+
APCA lightness contrast (APCA-W3 0.1.9, simplified) for two screen luminances. Signed: positive for dark text on a light background, negative for light text on a dark one, `0` below the noise floor.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { apcaContrast } from '@tenphi/glaze';
|
|
286
|
+
|
|
287
|
+
apcaContrast(0, 1); // 106.04… (black on white)
|
|
288
|
+
apcaContrast(1, 0); // -107.88… (white on black)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The inputs are APCA screen luminance `Ys`, not WCAG relative luminance: `Ys = 0.2126·R^2.4 + 0.7152·G^2.4 + 0.0722·B^2.4` over the gamma-encoded channels clamped to 0–1. Glaze's own solver computes `Ys` this way; the helper it uses isn't exported, so compute it yourself from `okhslToSrgb` output.
|
|
292
|
+
|
|
293
|
+
### `findToneForContrast(options)`
|
|
294
|
+
|
|
295
|
+
Searches tone for the value nearest `preferredTone` that meets a floor against a base color. All tones are on 0–1.
|
|
296
|
+
|
|
297
|
+
| Option | Type | Default | Description |
|
|
298
|
+
| --- | --- | --- | --- |
|
|
299
|
+
| `hue` | `number` | — | Candidate hue, 0–360. |
|
|
300
|
+
| `saturation` | `number` | — | Candidate OKHSL saturation, 0–1. |
|
|
301
|
+
| `preferredTone` | `number` | — | Starting tone. Returned as-is if it already passes. |
|
|
302
|
+
| `baseLinearRgb` | `[r, g, b]` | — | The base color, linear sRGB. |
|
|
303
|
+
| `contrast` | `ResolvedContrast` | — | `{ metric, target, polarity? }`, e.g. from `resolveContrastForMode`. |
|
|
304
|
+
| `toneRange` | `[number, number]` | `[0, 1]` | Search bounds. Glaze itself searches the full `[0, 1]` and only clamps `preferredTone` into the scheme's tone window (`'max'` / `'min'` excepted), so a floor can push a color past the window. |
|
|
305
|
+
| `epsilon` | `number` | `1e-4` | Convergence threshold. |
|
|
306
|
+
| `maxIterations` | `number` | `18` | Binary-search iterations per side. |
|
|
307
|
+
| `initialDirection` | `'lighter' \| 'darker'` | Side whose bound scores higher | Side to search first. When `preferredTone` sits on a bound, the only open side. |
|
|
308
|
+
| `flip` | `boolean` | `false` | Also search the opposite side. Without it, a side that can't reach the target pins the result to that side's bound. |
|
|
309
|
+
| `preferInitial` | `boolean` | `false` | With `flip`, when both sides pass, keep `initialDirection` instead of the result nearer `preferredTone`. The manual contrast level uses it so a color keeps its side across the ramp. |
|
|
310
|
+
| `pastel` | `boolean` | `false` | Use the pastel gamut for the candidate. |
|
|
311
|
+
|
|
312
|
+
`FindToneForContrastResult`:
|
|
313
|
+
|
|
314
|
+
| Field | Type | Description |
|
|
315
|
+
| --- | --- | --- |
|
|
316
|
+
| `tone` | `number` | The chosen tone, 0–1. |
|
|
317
|
+
| `contrast` | `number` | The achieved score: WCAG ratio or APCA Lc magnitude. |
|
|
318
|
+
| `met` | `boolean` | Whether `contrast` reaches the target. |
|
|
319
|
+
| `branch` | `'lighter' \| 'darker' \| 'preferred'` | Where the result came from. `'preferred'` means `preferredTone` already passed. |
|
|
320
|
+
| `flipped` | `true` (optional) | Present when the result is on the side opposite `initialDirection`. |
|
|
321
|
+
|
|
322
|
+
The search aims slightly past the target (WCAG ×1.01, APCA +0.5 Lc) to absorb rounding in the output formats; `met` is judged against the exact target. WCAG scores use `gamutClampedLuminance`, APCA scores use `Ys` with `polarity` deciding which side is the text.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import {
|
|
326
|
+
findToneForContrast,
|
|
327
|
+
okhslToLinearSrgb,
|
|
328
|
+
fromTone,
|
|
329
|
+
} from '@tenphi/glaze';
|
|
330
|
+
|
|
331
|
+
const surface = okhslToLinearSrgb(280, 0.1, fromTone(97));
|
|
332
|
+
|
|
333
|
+
findToneForContrast({
|
|
334
|
+
hue: 280,
|
|
335
|
+
saturation: 0.6,
|
|
336
|
+
preferredTone: 0.9,
|
|
337
|
+
baseLinearRgb: surface,
|
|
338
|
+
contrast: { metric: 'wcag', target: 4.5 },
|
|
339
|
+
});
|
|
340
|
+
// → { tone: 0.4758…, contrast: 4.67…, met: true, branch: 'darker' }
|
|
341
|
+
|
|
342
|
+
findToneForContrast({
|
|
343
|
+
hue: 280,
|
|
344
|
+
saturation: 0.6,
|
|
345
|
+
preferredTone: 0.9,
|
|
346
|
+
baseLinearRgb: surface,
|
|
347
|
+
contrast: { metric: 'wcag', target: 4.5 },
|
|
348
|
+
initialDirection: 'lighter',
|
|
349
|
+
});
|
|
350
|
+
// → { tone: 1, contrast: 1.09…, met: false, branch: 'lighter' }
|
|
351
|
+
|
|
352
|
+
// …with flip: true
|
|
353
|
+
// → {
|
|
354
|
+
// tone: 0.4846…,
|
|
355
|
+
// contrast: 4.54…,
|
|
356
|
+
// met: true,
|
|
357
|
+
// branch: 'darker',
|
|
358
|
+
// flipped: true,
|
|
359
|
+
// }
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `findValueForMixContrast(options)`
|
|
363
|
+
|
|
364
|
+
The same search over a mix parameter (0–1) between a base and a target — what [mix](../mixes.mdx) colors with `contrast` run.
|
|
365
|
+
|
|
366
|
+
| Option | Type | Default | Description |
|
|
367
|
+
| --- | --- | --- | --- |
|
|
368
|
+
| `preferredValue` | `number` | — | Starting mix value, 0–1. Returned as-is if it already passes. |
|
|
369
|
+
| `baseLinearRgb` | `[r, g, b]` | — | The base color, linear sRGB. Contrast is measured against it. |
|
|
370
|
+
| `targetLinearRgb` | `[r, g, b]` | — | The target color, linear sRGB. Required by the type; the search reads the mix only through `luminanceAtValue`. |
|
|
371
|
+
| `contrast` | `ResolvedContrast` | — | The floor. |
|
|
372
|
+
| `luminanceAtValue` | `(t: number) => number` | — | Luminance of the mixed color at `t`, in the metric's basis (WCAG Y or APCA `Ys`). You choose the blend model here. |
|
|
373
|
+
| `epsilon` | `number` | `1e-4` | Convergence threshold. |
|
|
374
|
+
| `maxIterations` | `number` | `20` | Binary-search iterations per side. |
|
|
375
|
+
| `flip` | `boolean` | `false` | Also search the opposite side of `preferredValue`. |
|
|
376
|
+
|
|
377
|
+
It starts toward whichever end (0 or 1) scores higher, or toward the only open side when `preferredValue` is 0 or 1, and on failure pins to the end it started toward. So a mix at value 1 that misses its floor pins to 0, the pure base. `FindValueForMixContrastResult` is `{ value, contrast, met, flipped? }`.
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
import {
|
|
381
|
+
findValueForMixContrast,
|
|
382
|
+
okhslToLinearSrgb,
|
|
383
|
+
relativeLuminanceFromLinearRgb,
|
|
384
|
+
fromTone,
|
|
385
|
+
} from '@tenphi/glaze';
|
|
386
|
+
|
|
387
|
+
const base = okhslToLinearSrgb(280, 0.1, fromTone(97));
|
|
388
|
+
const target = okhslToLinearSrgb(280, 0.6, fromTone(30));
|
|
389
|
+
const lerp = (t: number) =>
|
|
390
|
+
base.map((c, i) => c + (target[i] - c) * t) as [number, number, number];
|
|
391
|
+
|
|
392
|
+
findValueForMixContrast({
|
|
393
|
+
preferredValue: 0.1,
|
|
394
|
+
baseLinearRgb: base,
|
|
395
|
+
targetLinearRgb: target,
|
|
396
|
+
contrast: { metric: 'wcag', target: 3 },
|
|
397
|
+
luminanceAtValue: (t) => relativeLuminanceFromLinearRgb(lerp(t)),
|
|
398
|
+
});
|
|
399
|
+
// → { value: 0.766…, contrast: 3.03…, met: true }
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Constants and types
|
|
403
|
+
|
|
404
|
+
| Export | Value |
|
|
405
|
+
| --- | --- |
|
|
406
|
+
| `APCA_PRESETS` | `{ preferred: 90, body: 75, content: 60, large: 45, 'non-text': 30, min: 15 }` — use cases in [APCA presets](./contrast.md#apca-presets). |
|
|
407
|
+
| `APCA_HC_ENHANCEMENT` | `15` — Lc added to a bare APCA target in high contrast. |
|
|
408
|
+
| `APCA_MAX_LC` | `106` — cap on an escalated APCA target. |
|
|
409
|
+
|
|
410
|
+
| Type | Shape |
|
|
411
|
+
| --- | --- |
|
|
412
|
+
| `ContrastPreset` | `'AA' \| 'AAA' \| 'AA-large' \| 'AAA-large'` |
|
|
413
|
+
| `ApcaPreset` | `'preferred' \| 'body' \| 'content' \| 'large' \| 'non-text' \| 'min'` |
|
|
414
|
+
| `ResolvedContrast` | `{ metric: 'wcag' \| 'apca'; target: number; polarity?: 'fg' \| 'bg' }` — `target` is a WCAG ratio or an APCA Lc magnitude. |
|
|
415
|
+
| `FindToneForContrastOptions` / `FindToneForContrastResult` | [`findToneForContrast`](#findtoneforcontrastoptions) |
|
|
416
|
+
| `FindValueForMixContrastOptions` / `FindValueForMixContrastResult` | [`findValueForMixContrast`](#findvalueformixcontrastoptions) |
|
|
417
|
+
|
|
418
|
+
## Role helpers
|
|
419
|
+
|
|
420
|
+
A color's role fixes its APCA polarity. How Glaze picks one: [Roles and APCA polarity](../contrast.mdx#roles-and-apca-polarity). The `Role` and `RoleInput` types are in [Roles](./color-definitions.md#roles).
|
|
421
|
+
|
|
422
|
+
| Function | Returns | Description |
|
|
423
|
+
| --- | --- | --- |
|
|
424
|
+
| `normalizeRole(input)` | `Role \| undefined` | A role or alias → `'text'`, `'surface'`, or `'border'`. `undefined` for unknown strings and `undefined`. |
|
|
425
|
+
| `inferRoleFromName(name)` | `Role \| undefined` | Splits the name on non-alphanumerics and lowercase-to-uppercase humps and matches each token against the role keywords. The **last** match wins. |
|
|
426
|
+
| `roleToPolarity(role)` | `Polarity` | `'surface'` → `'bg'` (the base is the text); `'text'` and `'border'` → `'fg'`. |
|
|
427
|
+
| `oppositeRole(role)` | `Role` | `'surface'` → `'text'`; `'text'` and `'border'` → `'surface'`. Used for a dependent color with no role of its own. |
|
|
428
|
+
|
|
429
|
+
`Polarity` is `'fg' | 'bg'`.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
import {
|
|
433
|
+
normalizeRole,
|
|
434
|
+
inferRoleFromName,
|
|
435
|
+
roleToPolarity,
|
|
436
|
+
oppositeRole,
|
|
437
|
+
} from '@tenphi/glaze';
|
|
438
|
+
|
|
439
|
+
normalizeRole('ink'); // 'text'
|
|
440
|
+
normalizeRole('rule'); // 'border'
|
|
441
|
+
inferRoleFromName('button-text'); // 'text'
|
|
442
|
+
inferRoleFromName('inputBg'); // 'surface'
|
|
443
|
+
inferRoleFromName('card_border-outline'); // 'border'
|
|
444
|
+
inferRoleFromName('text-on-surface'); // 'surface' — last match wins
|
|
445
|
+
inferRoleFromName('BGColor'); // undefined — an all-caps run isn't split
|
|
446
|
+
inferRoleFromName('accent'); // undefined
|
|
447
|
+
roleToPolarity('border'); // 'fg'
|
|
448
|
+
oppositeRole('border'); // 'surface'
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Export guards
|
|
452
|
+
|
|
453
|
+
The checks the output methods run, exported for custom exporters.
|
|
454
|
+
|
|
455
|
+
| Function | Throws when |
|
|
456
|
+
| --- | --- |
|
|
457
|
+
| `assertNativeFormat(format, method)` | `format` is `'okhsl'` or `'okhst'`. `undefined` and native formats pass. `method` names the caller in the message. |
|
|
458
|
+
| `assertAllPastel(resolved, modes)` | Any variant included by `modes` is not pastel. `resolved` is a `Map<string, ResolvedColor>` (`theme.resolve()`); `modes` must have both `dark` and `highContrast` set. `light` is always checked, `dark` with `modes.dark`, `lightContrast` with `modes.highContrast`, `darkContrast` with both. |
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import {
|
|
462
|
+
glaze,
|
|
463
|
+
assertNativeFormat,
|
|
464
|
+
assertAllPastel,
|
|
465
|
+
} from '@tenphi/glaze';
|
|
466
|
+
|
|
467
|
+
assertNativeFormat('okhst', 'css');
|
|
468
|
+
// throws:
|
|
469
|
+
// glaze: okhst output is only supported by tasty() (not a native CSS
|
|
470
|
+
// color space).
|
|
471
|
+
// Use tasty({ format: 'okhst' }) or pick a native format (oklch|hsl|rgb)
|
|
472
|
+
// for css().
|
|
473
|
+
|
|
474
|
+
const theme = glaze(280, 80);
|
|
475
|
+
theme.colors({
|
|
476
|
+
surface: { tone: 97 },
|
|
477
|
+
accent: { tone: 50, pastel: true },
|
|
478
|
+
});
|
|
479
|
+
assertAllPastel(theme.resolve(), { dark: true, highContrast: false });
|
|
480
|
+
// throws:
|
|
481
|
+
// glaze: splitHue requires every color to be pastel (…).
|
|
482
|
+
// Non-pastel: surface.
|
|
483
|
+
// Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Where the output methods apply them: [Color formats](./output.md#color-formats) and [`splitHue` requirements](./output.md#requirements).
|
|
487
|
+
|
|
488
|
+
## Snapshot type guards
|
|
489
|
+
|
|
490
|
+
`isThemeExport`, `isColorTokenExport`, and `isPaletteExport` are exported as named functions and as `glaze.*` statics. They tell authoring snapshots apart by `kind` (or by shape for legacy snapshots) and don't validate `version`. Exact rules: [Type guards](./index.md#type-guards).
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
import { isThemeExport, glaze } from '@tenphi/glaze';
|
|
494
|
+
|
|
495
|
+
const data: unknown = JSON.parse(saved);
|
|
496
|
+
if (isThemeExport(data)) glaze.themeFrom(data);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The `splitHue` planning types `HuePlan`, `HueDeclaration`, and `ChannelCtx` are documented in [Output formats](./output.md#planning-types).
|