@tenphi/glaze 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/dist/index.cjs +808 -483
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +271 -159
- package/dist/index.d.mts +271 -159
- package/dist/index.mjs +798 -483
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +205 -136
- package/docs/methodology.md +64 -54
- package/docs/migration.md +81 -10
- package/docs/okhst.md +224 -0
- package/package.json +1 -1
package/docs/api.md
CHANGED
|
@@ -39,7 +39,7 @@ const d = glaze.fromRgb(122, 77, 191);
|
|
|
39
39
|
const e = glaze.from(a.export());
|
|
40
40
|
|
|
41
41
|
// Per-theme config override:
|
|
42
|
-
const rawTheme = glaze(280, 80, {
|
|
42
|
+
const rawTheme = glaze(280, 80, { lightTone: false, darkTone: false });
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
The optional `config` parameter is a `GlazeConfigOverride` — see [Per-instance config override](#per-instance-config-override).
|
|
@@ -72,15 +72,15 @@ A `GlazeTheme` exposes:
|
|
|
72
72
|
### `theme.colors(defs)`
|
|
73
73
|
|
|
74
74
|
```ts
|
|
75
|
-
theme.colors({ surface: {
|
|
76
|
-
theme.colors({ text: {
|
|
75
|
+
theme.colors({ surface: { tone: 97 } });
|
|
76
|
+
theme.colors({ text: { tone: 30 } });
|
|
77
77
|
// Both 'surface' and 'text' are now defined.
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### `theme.color(name) / theme.color(name, def)`
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
|
-
theme.color('surface', {
|
|
83
|
+
theme.color('surface', { tone: 97, saturation: 0.75 }); // set
|
|
84
84
|
const def = theme.color('surface'); // get
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -91,11 +91,11 @@ Creates a new theme inheriting all color definitions, optionally replacing the h
|
|
|
91
91
|
```ts
|
|
92
92
|
const danger = primary.extend({
|
|
93
93
|
hue: 23,
|
|
94
|
-
colors: { 'accent-fill': {
|
|
94
|
+
colors: { 'accent-fill': { tone: 48, mode: 'fixed' } },
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
// Inherit parent's config override and
|
|
98
|
-
const highSat = base.extend({ config: {
|
|
97
|
+
// Inherit parent's config override and widen the dark window further:
|
|
98
|
+
const highSat = base.extend({ config: { darkTone: [10, 100] } });
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
`GlazeExtendOptions`:
|
|
@@ -211,56 +211,89 @@ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
|
|
|
211
211
|
|
|
212
212
|
| Field | Type | Description |
|
|
213
213
|
|---|---|---|
|
|
214
|
-
| `
|
|
214
|
+
| `tone` | `HCPair<ToneValue>` | Number = absolute (0–100, contrast-uniform). `'+N'`/`'-N'` = relative to base's tone (requires `base`). `'max'`/`'min'` = forced to the scheme's tone extreme (no `base`). Optional HC pair `[normal, hc]`. |
|
|
215
215
|
| `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. |
|
|
216
216
|
| `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
|
|
217
217
|
| `base` | `string` | Name of another color in the same theme — makes this a *dependent* color. |
|
|
218
|
-
| `contrast` | `HCPair<
|
|
218
|
+
| `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base`. Requires `base`. See [`contrast`](#contrast-floor). |
|
|
219
219
|
| `mode` | `'auto' \| 'fixed' \| 'static'` | Adaptation mode. Default: `'auto'`. See [Adaptation modes](#adaptation-modes). |
|
|
220
|
+
| `flip` | `boolean` | Flip out-of-bounds results (relative `tone` overshoot / unmet `contrast`) to the opposite side instead of clamping. Default: the global `autoFlip` (`true`). See [`flip`](#flip). |
|
|
220
221
|
| `opacity` | `number` | Fixed alpha 0–1. Output includes alpha in the CSS value. Combining with `contrast` is not recommended (a `console.warn` is emitted). |
|
|
221
222
|
| `inherit` | `boolean` | Whether this color is inherited by child themes via `extend()`. Default: `true`. Set to `false` to make the color local to the current theme. |
|
|
222
223
|
|
|
223
|
-
####
|
|
224
|
+
#### Tone values
|
|
225
|
+
|
|
226
|
+
`tone` (0–100) replaces OKHSL lightness with a contrast-uniform axis — equal tone steps give equal WCAG contrast. See [`docs/okhst.md`](okhst.md) for the math. (To port old `lightness` values, see [migration.md](migration.md).)
|
|
224
227
|
|
|
225
228
|
| Form | Example | Meaning |
|
|
226
229
|
|---|---|---|
|
|
227
|
-
| Number (absolute) | `
|
|
228
|
-
| String (relative) | `
|
|
229
|
-
|
|
|
230
|
+
| Number (absolute) | `tone: 45` | Absolute tone 0–100. |
|
|
231
|
+
| String (relative) | `tone: '-52'` | Relative to base color's tone (requires `base`). |
|
|
232
|
+
| Extreme | `tone: 'max'` / `'min'` | Force to the scheme's highest (`'max'` = 100) or lowest (`'min'` = 0) tone. No `base` needed. |
|
|
233
|
+
| HC pair | `tone: ['-7', '-20']` | `[normal, high-contrast]`. A single value applies to both. |
|
|
234
|
+
|
|
235
|
+
**Absolute tone** on a dependent color (`base` set) positions the color independently. In dark mode it is tone-mapped (inverted + windowed) on its own. The `contrast` solver acts as a safety net.
|
|
236
|
+
|
|
237
|
+
**Relative tone** applies a signed delta to the base color's resolved tone. Because tone is contrast-uniform, a fixed delta yields a fixed contrast step. In dark mode with `mode: 'auto'`, the offset is anchored to the base's per-scheme tone. If `base + delta` falls outside `[0, 100]`, the result is clamped to the boundary, or — with `flip` (default on) — mirrored to the other side of the base.
|
|
238
|
+
|
|
239
|
+
**Extreme tone** (`'max'` / `'min'`) forces the color to the scheme's tone extreme without a contrast hack or a magic number. `'max'` resolves to author tone 100 and `'min'` to 0; both flow through scheme mapping like an absolute tone, so under `mode: 'auto'` they invert in dark (`'max'` is lightest in light, darkest in dark). Use `mode: 'static'` to pin the same extreme across schemes, or `mode: 'fixed'` to keep the same end without inverting. No `base` required.
|
|
240
|
+
|
|
241
|
+
A dependent color with `base` but no `tone` inherits the base's tone (equivalent to a delta of 0).
|
|
230
242
|
|
|
231
|
-
|
|
243
|
+
#### `flip`
|
|
232
244
|
|
|
233
|
-
|
|
245
|
+
`flip` governs what happens when a result would fall outside its valid range:
|
|
234
246
|
|
|
235
|
-
|
|
247
|
+
- **Relative `tone` overshoot:** when `base ± delta` exceeds `[0, 100]`, `flip` mirrors the delta to the other side of the base (e.g. `'+30'` becomes `'-30'`) instead of clamping to the boundary.
|
|
248
|
+
- **`contrast` direction:** when the requested tone direction can't meet the floor, `flip` lets the solver try the opposite side (the same behavior as the global `autoFlip`).
|
|
236
249
|
|
|
237
|
-
|
|
250
|
+
`flip` defaults to the global `autoFlip` (`true`). Set `flip: false` on a color to clamp instead of mirror — useful when you want a relative offset to stay on the authored side of the base, or to keep an unmet contrast pinned to one direction's extreme.
|
|
251
|
+
|
|
252
|
+
#### `contrast` (floor)
|
|
238
253
|
|
|
239
254
|
```ts
|
|
240
|
-
type
|
|
255
|
+
type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
|
|
256
|
+
type ContrastSpec =
|
|
257
|
+
| number // bare WCAG ratio
|
|
258
|
+
| ContrastPreset // named WCAG preset
|
|
259
|
+
| { wcag: HCPair<number | ContrastPreset> }
|
|
260
|
+
| { apca: HCPair<number> }; // APCA Lc target
|
|
241
261
|
```
|
|
242
262
|
|
|
243
|
-
| Preset |
|
|
263
|
+
| Preset | WCAG ratio |
|
|
244
264
|
|---|---|
|
|
245
265
|
| `'AA-large'` | 3 |
|
|
246
266
|
| `'AA'` | 4.5 |
|
|
247
267
|
| `'AAA-large'` | 4.5 |
|
|
248
268
|
| `'AAA'` | 7 |
|
|
249
269
|
|
|
250
|
-
|
|
270
|
+
A bare number or preset means **WCAG**. Use `{ wcag }` / `{ apca }` to pick the metric explicitly. The `[normal, highContrast]` pair may live at the outer level (`[4.5, 7]`, `[{ wcag: 4.5 }, { wcag: 7 }]`) or inside the metric (`{ wcag: [4.5, 7] }`, `{ apca: [45, 60] }`).
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
contrast: 4.5 // WCAG 4.5
|
|
274
|
+
contrast: 'AAA' // WCAG 7
|
|
275
|
+
contrast: { wcag: 6 } // WCAG 6
|
|
276
|
+
contrast: { wcag: [4.5, 7] } // WCAG 4.5 normal / 7 high-contrast
|
|
277
|
+
contrast: { apca: 60 } // APCA Lc 60
|
|
278
|
+
contrast: { apca: [45, 60] } // APCA Lc 45 normal / 60 high-contrast
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The floor is applied independently per scheme — if the `tone` already satisfies it the tone is kept, otherwise the solver searches in tone (contrast-uniform → a closed-form WCAG seed and fast convergence) until the target is met.
|
|
251
282
|
|
|
252
|
-
By default,
|
|
283
|
+
By default, the solver crosses to the opposite side of the base color when the requested tone direction cannot satisfy the floor. This is controlled per-color by [`flip`](#flip) (which defaults to the global `autoFlip`). Set `glaze.configure({ autoFlip: false })` — or `flip: false` on a single color — to keep strict directionality: unmet colors pin to that direction's 0 or 100 tone extreme instead of falling back to the original requested value.
|
|
253
284
|
|
|
254
|
-
**Full
|
|
285
|
+
**Full tone spectrum in HC mode:** in high-contrast variants the `lightTone` and `darkTone` window constraints are bypassed entirely (the window is forced to `[0, 100]`). Colors can reach the full range, maximizing perceivable contrast.
|
|
286
|
+
|
|
287
|
+
**Chromatic drift (verification):** tone is contrast-uniform for grays. A chromatic swatch at a given tone shares its OKHSL lightness with the equivalent gray but drifts in real luminance, so a contrast-floored color may land slightly under its gray-tone expectation. Glaze measures the resolved result against the base and emits a deduped advisory `console.warn` when it drifts below the target. See [`docs/okhst.md`](okhst.md) §Verification.
|
|
255
288
|
|
|
256
289
|
#### Per-color hue override
|
|
257
290
|
|
|
258
291
|
```ts
|
|
259
292
|
const theme = glaze(280, 80);
|
|
260
293
|
theme.colors({
|
|
261
|
-
surface: {
|
|
262
|
-
gradientEnd: {
|
|
263
|
-
warning: {
|
|
294
|
+
surface: { tone: 97 },
|
|
295
|
+
gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
|
|
296
|
+
warning: { tone: 60, hue: 40 }, // absolute
|
|
264
297
|
});
|
|
265
298
|
```
|
|
266
299
|
|
|
@@ -289,7 +322,7 @@ See [Shadows](#shadows) below for the algorithm and tuning details.
|
|
|
289
322
|
| `value` | `HCPair<number>` | Mix ratio 0–100 (0 = pure base, 100 = pure target). In `'transparent'` blend, this becomes the target's opacity. Supports HC pairs. |
|
|
290
323
|
| `blend` | `'opaque' \| 'transparent'` | Default `'opaque'`. |
|
|
291
324
|
| `space` | `'okhsl' \| 'srgb'` | Interpolation space for opaque blending. Default `'okhsl'`. Ignored for `'transparent'` (always composites in linear sRGB). |
|
|
292
|
-
| `contrast` | `HCPair<
|
|
325
|
+
| `contrast` | `HCPair<ContrastSpec>` | Optional contrast floor against `base` (WCAG or APCA — see [`contrast`](#contrast-floor)). The solver adjusts the mix ratio (opaque) or opacity (transparent). |
|
|
293
326
|
| `inherit` | `boolean` | Inheritance flag, default `true`. |
|
|
294
327
|
|
|
295
328
|
See [Mix colors](#mix-colors) below.
|
|
@@ -312,10 +345,10 @@ glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?:
|
|
|
312
345
|
|
|
313
346
|
| Shape | Example | Notes |
|
|
314
347
|
|---|---|---|
|
|
315
|
-
| **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `oklch()`). |
|
|
316
|
-
| **Value object** | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, `{ r, g, b }` (sRGB 0–255), or `{ l, c, h }` (OKLCh). |
|
|
348
|
+
| **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`). |
|
|
349
|
+
| **Value object** | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, OKHST (`{ h, s, t }`), `{ r, g, b }` (sRGB 0–255), or `{ l, c, h }` (OKLCh). |
|
|
317
350
|
| **`{ from, ...overrides }`** | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value + color overrides in one object. |
|
|
318
|
-
| **Structured** | `{ hue: 152, saturation: 95,
|
|
351
|
+
| **Structured** | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token (hue/saturation in 0–100, tone in 0–100). |
|
|
319
352
|
|
|
320
353
|
`GlazeColorValue` (bare string or value-object forms) accepts:
|
|
321
354
|
|
|
@@ -325,23 +358,26 @@ glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?:
|
|
|
325
358
|
| `rgb()` | `'rgb(38 252 178)'`, `'rgb(38 252 178 / 0.8)'` | Modern space syntax. Alpha dropped with warning. |
|
|
326
359
|
| `hsl()` | `'hsl(152 97% 57%)'` | Modern space syntax. Alpha dropped with warning. |
|
|
327
360
|
| `okhsl()` | `'okhsl(152 95% 74%)'` | Glaze's own emit format. Alpha dropped with warning. |
|
|
361
|
+
| `okhst()` | `'okhst(152 95% 70%)'` | OKHST tone input (third value is tone 0–100). **Input only** — never emitted. Alpha dropped with warning. |
|
|
328
362
|
| `oklch()` | `'oklch(0.85 0.18 152)'` | Glaze's own emit format. Alpha dropped with warning. |
|
|
329
|
-
| `OkhslColor` object | `{ h: 152, s: 0.95, l: 0.74 }` |
|
|
363
|
+
| `OkhslColor` object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL shape (h: 0–360, s/l: 0–1). Passing 0–100 for `s`/`l` throws with a hint to use the structured form. |
|
|
364
|
+
| `OkhstColor` object | `{ h: 152, s: 0.95, t: 0.70 }` | OKHST shape (h: 0–360, s/t: 0–1). The `t` key disambiguates it from `{ h, s, l }`. **Input only.** |
|
|
330
365
|
| `RgbColor` object | `{ r: 38, g: 252, b: 178 }` | sRGB 0–255. RGB tuple `[r, g, b]` is not supported — use this object form. |
|
|
331
366
|
| `OklchColor` object | `{ l: 0.85, c: 0.18, h: 152 }` | OKLCh (L/C: 0–1, H: degrees), same semantics as `oklch()` strings. |
|
|
332
367
|
|
|
333
|
-
`GlazeColorInput` (structured form) is `{ hue, saturation,
|
|
368
|
+
`GlazeColorInput` (structured form) is `{ hue, saturation, tone, ... }`:
|
|
334
369
|
|
|
335
370
|
| Field | Type | Description |
|
|
336
371
|
|---|---|---|
|
|
337
372
|
| `hue` | `number` | 0–360. |
|
|
338
373
|
| `saturation` | `number` | 0–100. |
|
|
339
|
-
| `
|
|
374
|
+
| `tone` | `HCPair<number \| ExtremeValue>` | 0–100 (contrast-uniform) or `'max'`/`'min'`, optional HC pair. |
|
|
340
375
|
| `saturationFactor` | `number` | Multiplier on the seed (0–1). Default: `1`. |
|
|
341
376
|
| `mode` | `AdaptationMode` | Default: `'auto'`. |
|
|
377
|
+
| `flip` | `boolean` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
|
|
342
378
|
| `opacity` | `number` | Fixed alpha 0–1. |
|
|
343
379
|
| `base` | `GlazeColorToken \| GlazeColorValue` | Optional dependency. See [Pairing colors](#pairing-colors). |
|
|
344
|
-
| `contrast` | `HCPair<
|
|
380
|
+
| `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base` (WCAG or APCA). Without `base`, anchored to the literal seed. |
|
|
345
381
|
| `name` | `string` | Debug label for warnings; doesn't change output keys. Reserved names (`'value'`, `'seed'`, `'externalBase'`) are rejected. |
|
|
346
382
|
|
|
347
383
|
`GlazeFromInput` (from form) is `{ from: GlazeColorValue, ...colorOverrides }`:
|
|
@@ -351,10 +387,11 @@ glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?:
|
|
|
351
387
|
| `from` | **Required.** The source color value — same forms as `GlazeColorValue`. |
|
|
352
388
|
| `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed, never to `base`). |
|
|
353
389
|
| `saturation` | Override seed saturation (0–100). |
|
|
354
|
-
| `
|
|
390
|
+
| `tone` | Number (absolute 0–100), `'+N'`/`'-N'`, or `'max'`/`'min'`. Without `base`, relative anchors to the seed; with `base`, anchors to `base`'s tone per scheme. |
|
|
355
391
|
| `saturationFactor` | Multiplier on the seed (0–1). |
|
|
356
392
|
| `mode` | `'auto'` (default) / `'fixed'` / `'static'`. |
|
|
357
|
-
| `
|
|
393
|
+
| `flip` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
|
|
394
|
+
| `contrast` | Contrast floor (WCAG or APCA). Without `base`, anchored to the literal seed; with `base`, solved per scheme. |
|
|
358
395
|
| `base` | `GlazeColorToken` or raw `GlazeColorValue`. See [Pairing colors](#pairing-colors). |
|
|
359
396
|
| `opacity` | Fixed alpha 0–1. Combining with `contrast` is not recommended — `console.warn` is emitted. |
|
|
360
397
|
| `name` | Debug label only — surfaces in warnings/errors. Does not change output keys. |
|
|
@@ -366,10 +403,10 @@ Named CSS colors (`'red'`, `'blueviolet'`) are not supported.
|
|
|
366
403
|
Every input form defaults to `mode: 'auto'` so the resolved token adapts between light and dark like an ordinary theme color. The config snapshot taken at create time differs by input form:
|
|
367
404
|
|
|
368
405
|
- **Value-shorthand** (bare strings, value objects, and `{ from, ...overrides }`):
|
|
369
|
-
- Light variant preserves the input
|
|
370
|
-
- All other config fields (`
|
|
371
|
-
- **Structured input** (`{ hue, saturation,
|
|
372
|
-
- Both
|
|
406
|
+
- Light variant preserves the input tone exactly (`lightTone: false`).
|
|
407
|
+
- All other config fields (`darkTone`, `darkDesaturation`, `saturationTaper`, `autoFlip`) snapshot from `globalConfig` at create time.
|
|
408
|
+
- **Structured input** (`{ hue, saturation, tone, ... }`):
|
|
409
|
+
- Both tone windows snapshot from `globalConfig` at create time (same as a theme color).
|
|
373
410
|
- All fields are **snapshotted at color-creation time** — later `glaze.configure()` calls don't retroactively change existing tokens.
|
|
374
411
|
|
|
375
412
|
```ts
|
|
@@ -379,11 +416,14 @@ glaze.color('#26fcb2')
|
|
|
379
416
|
// Value-object — same behavior
|
|
380
417
|
glaze.color({ h: 152, s: 0.95, l: 0.74 })
|
|
381
418
|
|
|
419
|
+
// OKHST value-object — tone axis
|
|
420
|
+
glaze.color({ h: 152, s: 0.95, t: 0.70 })
|
|
421
|
+
|
|
382
422
|
// From form — value + color overrides
|
|
383
423
|
glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' })
|
|
384
424
|
|
|
385
|
-
// Structured form — explicit hue/saturation/
|
|
386
|
-
glaze.color({ hue: 152, saturation: 95,
|
|
425
|
+
// Structured form — explicit hue/saturation/tone (0–100)
|
|
426
|
+
glaze.color({ hue: 152, saturation: 95, tone: 74 })
|
|
387
427
|
```
|
|
388
428
|
|
|
389
429
|
### Token methods
|
|
@@ -401,36 +441,36 @@ A `GlazeColorToken` exposes:
|
|
|
401
441
|
|
|
402
442
|
### Per-instance config override
|
|
403
443
|
|
|
404
|
-
The optional `config` second argument (`GlazeConfigOverride`) overrides the resolve-relevant global config fields for a single token or theme. Fields that are omitted fall through to the live global config at create time (and are snapshotted).
|
|
444
|
+
The optional `config` second argument (`GlazeConfigOverride`) overrides the resolve-relevant global config fields for a single token or theme. Fields that are omitted fall through to the live global config at create time (and are snapshotted). A tone window can be `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping).
|
|
405
445
|
|
|
406
446
|
`GlazeConfigOverride`:
|
|
407
447
|
|
|
408
448
|
| Field | Default (from global) | Description |
|
|
409
449
|
|---|---|---|
|
|
410
|
-
| `
|
|
411
|
-
| `
|
|
450
|
+
| `lightTone` | `[10, 100]` | Light tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
|
|
451
|
+
| `darkTone` | `[15, 95]` | Dark tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
|
|
412
452
|
| `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
|
|
413
|
-
| `
|
|
414
|
-
| `autoFlip` | `true` |
|
|
453
|
+
| `saturationTaper` | `0.15` | Saturation taper strength toward the tone extremes (0–1). |
|
|
454
|
+
| `autoFlip` | `true` | Default for each color's `flip`: when solving `contrast` (or applying a relative `tone` that overshoots), allow crossing to the opposite side instead of clamping. |
|
|
415
455
|
| `shadowTuning` | `undefined` | Default shadow tuning (meaningful for themes; harmless on color tokens). |
|
|
416
456
|
|
|
417
457
|
Config overrides apply to both `glaze.color()` tokens and `glaze()` themes:
|
|
418
458
|
|
|
419
459
|
```ts
|
|
420
|
-
// Standalone color — preserve raw
|
|
421
|
-
glaze.color('#26fcb2', {
|
|
460
|
+
// Standalone color — preserve raw tone in both schemes
|
|
461
|
+
glaze.color('#26fcb2', { darkTone: false })
|
|
422
462
|
|
|
423
463
|
// Restore the #000 → white dark flip (full dark range)
|
|
424
464
|
glaze.color('#000000', {
|
|
425
|
-
|
|
426
|
-
|
|
465
|
+
lightTone: false,
|
|
466
|
+
darkTone: [15, 100],
|
|
427
467
|
})
|
|
428
468
|
|
|
429
469
|
// Structured form with config override
|
|
430
|
-
glaze.color({ hue: 152, saturation: 95,
|
|
470
|
+
glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false })
|
|
431
471
|
|
|
432
472
|
// Theme with config override
|
|
433
|
-
const rawTheme = glaze(280, 80, {
|
|
473
|
+
const rawTheme = glaze(280, 80, { lightTone: false })
|
|
434
474
|
```
|
|
435
475
|
|
|
436
476
|
The override is **snapshotted at create time** so later `glaze.configure()` calls don't change already-created tokens or themes (for non-overridden fields, the snapshot captured the global value at creation time; for themes, non-overridden fields are re-read from the live global at resolve time — see [Theme config override](#theme-config-override)).
|
|
@@ -443,17 +483,18 @@ When a theme is created with a `GlazeConfigOverride`, the override is **merged o
|
|
|
443
483
|
- Fields you didn't override still react to later `glaze.configure()` calls.
|
|
444
484
|
|
|
445
485
|
```ts
|
|
446
|
-
const t = glaze(280, 80, {
|
|
447
|
-
t.colors({ text: {
|
|
448
|
-
// text.light
|
|
486
|
+
const t = glaze(280, 80, { lightTone: [0, 50] });
|
|
487
|
+
t.colors({ text: { tone: 50, saturation: 1 } });
|
|
488
|
+
// text.light lands inside the [0, 50] window — always, regardless of
|
|
489
|
+
// global lightTone changes.
|
|
449
490
|
// text.dark.s reacts to glaze.configure({ darkDesaturation }) since it's not overridden.
|
|
450
491
|
```
|
|
451
492
|
|
|
452
493
|
`extend` inherits the parent's override and shallow-merges the child's:
|
|
453
494
|
|
|
454
495
|
```ts
|
|
455
|
-
const child = t.extend({ config: {
|
|
456
|
-
// child:
|
|
496
|
+
const child = t.extend({ config: { darkTone: false } });
|
|
497
|
+
// child: lightTone { lo: 0, hi: 50 } (inherited) + darkTone: false (added)
|
|
457
498
|
```
|
|
458
499
|
|
|
459
500
|
`theme.export()` includes `config`; `glaze.from(data)` restores it.
|
|
@@ -473,7 +514,7 @@ Both value-form and structured-form tokens round-trip.
|
|
|
473
514
|
|
|
474
515
|
### Pairing colors
|
|
475
516
|
|
|
476
|
-
Set `base` to anchor a standalone color to another standalone color or raw value. The
|
|
517
|
+
Set `base` to anchor a standalone color to another standalone color or raw value. The contrast solver and relative `tone` offsets switch their anchor from the literal seed to the base's resolved variant per scheme — so the same text color automatically lands at AA against its background in light, dark, and high-contrast modes.
|
|
477
518
|
|
|
478
519
|
```ts
|
|
479
520
|
const bg = glaze.color('#1a1a2e');
|
|
@@ -481,10 +522,10 @@ const bg = glaze.color('#1a1a2e');
|
|
|
481
522
|
// Text guaranteed AA against `bg` in every scheme.
|
|
482
523
|
const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
|
|
483
524
|
|
|
484
|
-
// Border 8
|
|
525
|
+
// Border 8 tone units lighter than `bg` in each scheme.
|
|
485
526
|
const border = glaze.color({ from: '#000000',
|
|
486
527
|
base: bg,
|
|
487
|
-
|
|
528
|
+
tone: '+8',
|
|
488
529
|
mode: 'fixed',
|
|
489
530
|
});
|
|
490
531
|
|
|
@@ -495,11 +536,11 @@ const text2 = glaze.color({ from: '#ffffff', base: '#1a1a2e', contrast: 'AA' });
|
|
|
495
536
|
Behavior with `base`:
|
|
496
537
|
|
|
497
538
|
- `contrast` is solved per scheme against `base`'s resolved variant (light / dark / lightContrast / darkContrast).
|
|
498
|
-
- Relative `
|
|
539
|
+
- Relative `tone: '+N'` / `'-N'` is anchored to `base`'s tone per scheme (matches theme behavior).
|
|
499
540
|
- Relative `hue: '+N'` / `'-N'` still anchors to the **seed** (the value passed to `glaze.color()`), not the base.
|
|
500
541
|
- `mode` works as a per-pair knob.
|
|
501
542
|
- The base token's `.resolve()` is called lazily on the first resolve of the dependent and the result is captured by reference; later mutations to the base don't apply.
|
|
502
|
-
- **Structured bases are resolved at full range for linking math**: when a value/`from` color links to a base created via the structured form, the contrast/
|
|
543
|
+
- **Structured bases are resolved at full range for linking math**: when a value/`from` color links to a base created via the structured form, the contrast/tone anchor uses the raw input tone (not the windowed output). This ensures the anchor matches what you intended, not what the light window remapped it to. The base's own `.resolve()` output is unaffected.
|
|
503
544
|
- When the contrast target is physically unreachable, `glaze` emits a single `console.warn` per `(name, scheme, target)` triple and returns the closest passing variant. Use the `name` override to make the warning identifiable.
|
|
504
545
|
|
|
505
546
|
Chains compose:
|
|
@@ -522,8 +563,8 @@ The `name` override appears in `console.warn` / Error messages but **does not**
|
|
|
522
563
|
|
|
523
564
|
```ts
|
|
524
565
|
theme.colors({
|
|
525
|
-
surface: {
|
|
526
|
-
text: { base: 'surface',
|
|
566
|
+
surface: { tone: 95 },
|
|
567
|
+
text: { base: 'surface', tone: '-52', contrast: 'AAA' },
|
|
527
568
|
|
|
528
569
|
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
|
|
529
570
|
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
@@ -616,7 +657,7 @@ For a simple fixed-alpha color (no shadow algorithm), use `opacity` on a regular
|
|
|
616
657
|
|
|
617
658
|
```ts
|
|
618
659
|
theme.colors({
|
|
619
|
-
overlay: {
|
|
660
|
+
overlay: { tone: 0, opacity: 0.5 },
|
|
620
661
|
});
|
|
621
662
|
// → 'oklch(0 0 0 / 0.5)'
|
|
622
663
|
```
|
|
@@ -631,8 +672,8 @@ Produces a solid color by interpolating between `base` and `target`:
|
|
|
631
672
|
|
|
632
673
|
```ts
|
|
633
674
|
theme.colors({
|
|
634
|
-
surface: {
|
|
635
|
-
accent: {
|
|
675
|
+
surface: { tone: 95 },
|
|
676
|
+
accent: { tone: 30 },
|
|
636
677
|
tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
|
|
637
678
|
});
|
|
638
679
|
```
|
|
@@ -647,8 +688,8 @@ Produces the target color with controlled opacity — useful for hover overlays:
|
|
|
647
688
|
|
|
648
689
|
```ts
|
|
649
690
|
theme.colors({
|
|
650
|
-
surface: {
|
|
651
|
-
black: {
|
|
691
|
+
surface: { tone: 95 },
|
|
692
|
+
black: { tone: 0, saturation: 0 },
|
|
652
693
|
hover: {
|
|
653
694
|
type: 'mix', base: 'surface', target: 'black',
|
|
654
695
|
value: 8, blend: 'transparent',
|
|
@@ -695,8 +736,8 @@ Mix colors can reference other mix colors:
|
|
|
695
736
|
|
|
696
737
|
```ts
|
|
697
738
|
theme.colors({
|
|
698
|
-
white: {
|
|
699
|
-
black: {
|
|
739
|
+
white: { tone: 100, saturation: 0 },
|
|
740
|
+
black: { tone: 0, saturation: 0 },
|
|
700
741
|
gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
|
|
701
742
|
lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
|
|
702
743
|
});
|
|
@@ -877,78 +918,66 @@ The `format` option works on every export: `theme.tokens()`, `theme.tasty()`, `t
|
|
|
877
918
|
|
|
878
919
|
| Mode | Behavior |
|
|
879
920
|
|---|---|
|
|
880
|
-
| `'auto'` (default) | Full adaptation.
|
|
881
|
-
| `'fixed'` | Color stays recognizable.
|
|
882
|
-
| `'static'` | No adaptation. Same
|
|
921
|
+
| `'auto'` (default) | Full adaptation. Dark is a single tone inversion (`100 − t`) remapped into the dark window. High-contrast uses the full range. |
|
|
922
|
+
| `'fixed'` | Color stays recognizable. Tone is *mapped* (not inverted) into the dark window. Use for brand buttons, CTAs, status banners. |
|
|
923
|
+
| `'static'` | No adaptation. Same tone in every scheme. |
|
|
883
924
|
|
|
884
|
-
### How relative
|
|
925
|
+
### How relative tone adapts
|
|
885
926
|
|
|
886
|
-
**`auto`** —
|
|
927
|
+
**`auto`** — the offset is anchored to the base's per-scheme tone:
|
|
887
928
|
|
|
888
929
|
```
|
|
889
|
-
Light: surface
|
|
890
|
-
Dark: surface inverts to
|
|
891
|
-
|
|
930
|
+
Light: surface tone=97, text tone='-52' → tone 45 (dark text on light bg)
|
|
931
|
+
Dark: surface inverts to a low tone; the '-52' offset re-anchors to the
|
|
932
|
+
base's light tone and maps into the dark window (light text on dark bg)
|
|
892
933
|
```
|
|
893
934
|
|
|
894
|
-
**`fixed`** —
|
|
935
|
+
**`fixed`** — tone is mapped (not inverted), relative sign preserved:
|
|
895
936
|
|
|
896
937
|
```
|
|
897
|
-
Light: accent-fill
|
|
898
|
-
Dark: accent-fill maps
|
|
938
|
+
Light: accent-fill tone=52, accent-text tone='+20' → lighter than the fill
|
|
939
|
+
Dark: accent-fill maps into the dark window, sign preserved
|
|
899
940
|
```
|
|
900
941
|
|
|
901
|
-
|
|
942
|
+
Offsets that would push past `[0, 100]` clamp to the boundary, or — with `flip` (default on) — mirror to the other side of the base. Set `flip: false` to keep the authored side and clamp instead.
|
|
943
|
+
|
|
944
|
+
**`static`** — no adaptation, same tone in every scheme.
|
|
902
945
|
|
|
903
946
|
---
|
|
904
947
|
|
|
905
948
|
## Light / dark scheme mapping
|
|
906
949
|
|
|
907
|
-
|
|
950
|
+
The mapping is now a single tone pipeline; there is no Möbius curve. See [`docs/okhst.md`](okhst.md) for the full math and the calibrated default constants.
|
|
908
951
|
|
|
909
|
-
|
|
952
|
+
### Light scheme
|
|
910
953
|
|
|
911
|
-
|
|
912
|
-
const [lo, hi] = lightLightness; // default: [10, 100]
|
|
913
|
-
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
914
|
-
```
|
|
954
|
+
An authored tone (0–100) is remapped into the `lightTone` window. The window's `lo`/`hi` are OKHSL-lightness endpoints (0–100); the tone is positioned within the window's tone interval and converted to a final OKHSL lightness. `static` mode and HC variants use the full range.
|
|
915
955
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
| accent-fill (L=52) | 52 | 56.8 |
|
|
922
|
-
| near-black (L=0) | 0 | 10 |
|
|
956
|
+
```
|
|
957
|
+
window = lightTone // default [10, 100]
|
|
958
|
+
finalTone = remap(authorTone, window)
|
|
959
|
+
finalL = fromTone(finalTone) // OKHSL lightness
|
|
960
|
+
```
|
|
923
961
|
|
|
924
|
-
### Dark scheme
|
|
962
|
+
### Dark scheme
|
|
925
963
|
|
|
926
|
-
**`auto`** —
|
|
964
|
+
**`auto`** — invert the tone, then remap into the dark window:
|
|
927
965
|
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
// darkCurve default: 0.5
|
|
966
|
+
```
|
|
967
|
+
window = darkTone // default [15, 95]
|
|
968
|
+
inverted = 100 - authorTone
|
|
969
|
+
finalTone = remap(inverted, window)
|
|
933
970
|
```
|
|
934
971
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
Unlike a power curve, the Möbius transformation provides **proportional expansion** — small and large deltas are scaled by similar ratios, preserving the visual hierarchy of the light theme.
|
|
972
|
+
Because tone is contrast-uniform, the inversion preserves contrast steps without a fitted curve — the asymmetry between light and dark lives entirely in the two windows' `(lo, hi, eps)` scalars (`eps` defaults to the reference `0.05`).
|
|
938
973
|
|
|
939
|
-
**`fixed`** —
|
|
974
|
+
**`fixed`** — remap into the dark window without inversion:
|
|
940
975
|
|
|
941
|
-
```
|
|
942
|
-
|
|
976
|
+
```
|
|
977
|
+
finalTone = remap(authorTone, darkTone)
|
|
943
978
|
```
|
|
944
979
|
|
|
945
|
-
|
|
946
|
-
|---|---|---|---|---|
|
|
947
|
-
| surface (L=97) | 97 | 19.7 | 17.4 | 92.6 |
|
|
948
|
-
| accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
|
|
949
|
-
| accent-text (L=100) | 100 | 15 | 15 | 95 |
|
|
950
|
-
|
|
951
|
-
In high-contrast variants the `darkLightness` window is bypassed — `auto` uses the Möbius curve over the full `[0, 100]` range, `fixed` uses identity.
|
|
980
|
+
In high-contrast variants both windows are bypassed (forced to the full `[0, 100]` range): `auto` still inverts, `fixed`/`static` do not.
|
|
952
981
|
|
|
953
982
|
### Dark scheme — saturation
|
|
954
983
|
|
|
@@ -960,16 +989,20 @@ S_dark = S_light * (1 - darkDesaturation) // default: 0.1
|
|
|
960
989
|
|
|
961
990
|
`static` mode skips desaturation.
|
|
962
991
|
|
|
992
|
+
### Saturation taper
|
|
993
|
+
|
|
994
|
+
`saturationTaper` (default `0.15`) gently reduces saturation toward the tone extremes, where in-gamut chroma collapses. It is the *strength* (0–1) — the maximum fraction of saturation removed at the very edges — ramped in smoothly over the outer ~15% of tone on each end. Mid-tones are untouched; `0` disables it.
|
|
995
|
+
|
|
963
996
|
---
|
|
964
997
|
|
|
965
998
|
## Configuration
|
|
966
999
|
|
|
967
1000
|
```ts
|
|
968
1001
|
glaze.configure({
|
|
969
|
-
|
|
970
|
-
|
|
1002
|
+
lightTone: [10, 100], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
|
|
1003
|
+
darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
|
|
971
1004
|
darkDesaturation: 0.1,
|
|
972
|
-
|
|
1005
|
+
saturationTaper: 0.15,
|
|
973
1006
|
states: {
|
|
974
1007
|
dark: '@dark',
|
|
975
1008
|
highContrast: '@high-contrast',
|
|
@@ -985,20 +1018,22 @@ glaze.configure({
|
|
|
985
1018
|
});
|
|
986
1019
|
```
|
|
987
1020
|
|
|
1021
|
+
A `ToneWindow` is `[lo, hi]` (OKHSL-lightness endpoints, reference eps — the common form), `{ lo, hi, eps }` (advanced: explicit per-mode render eps), or `false` to disable clamping (full range `[0, 100]` at the reference eps). `false` removes the *boundaries*, not the contrast-uniform tone curve.
|
|
1022
|
+
|
|
988
1023
|
`GlazeConfig`:
|
|
989
1024
|
|
|
990
1025
|
| Field | Default | Description |
|
|
991
1026
|
|---|---|---|
|
|
992
|
-
| `
|
|
993
|
-
| `
|
|
1027
|
+
| `lightTone` | `[10, 100]` | Light scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
|
|
1028
|
+
| `darkTone` | `[15, 95]` | Dark scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
|
|
994
1029
|
| `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
|
|
995
|
-
| `
|
|
1030
|
+
| `saturationTaper` | `0.15` | Saturation taper strength toward the tone extremes (0–1). `0` disables. |
|
|
996
1031
|
| `states.dark` | `'@dark'` | State alias for dark mode tokens (Tasty export). |
|
|
997
1032
|
| `states.highContrast` | `'@high-contrast'` | State alias for HC tokens. |
|
|
998
1033
|
| `modes.dark` | `true` | Include dark variants in exports. |
|
|
999
1034
|
| `modes.highContrast` | `false` | Include HC variants. |
|
|
1000
1035
|
| `shadowTuning` | `undefined` | Default tuning for all shadow colors. Per-color tuning merges field-by-field. |
|
|
1001
|
-
| `autoFlip` | `true` | When solving `contrast
|
|
1036
|
+
| `autoFlip` | `true` | Default for each color's `flip`. When solving `contrast` (or applying a relative `tone` that overshoots `[0, 100]`), allow crossing to the opposite side instead of clamping. With `false`, only the requested direction is considered; unmet contrasts pin the tone to that direction's extreme (and emit a warning) and overshooting offsets clamp to the boundary. Override per color via [`flip`](#flip). |
|
|
1002
1037
|
|
|
1003
1038
|
| Method | Description |
|
|
1004
1039
|
|---|---|
|
|
@@ -1039,10 +1074,12 @@ Resolution priority (highest first):
|
|
|
1039
1074
|
| Condition | Behavior |
|
|
1040
1075
|
|---|---|
|
|
1041
1076
|
| `contrast` without `base` in a **theme** color | Validation error |
|
|
1042
|
-
| Relative `
|
|
1077
|
+
| Relative `tone` without `base` in a **theme** color | Validation error |
|
|
1043
1078
|
| `contrast` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
|
|
1044
|
-
| Relative `
|
|
1045
|
-
| `
|
|
1079
|
+
| Relative `tone` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
|
|
1080
|
+
| Relative `tone` overshoots `[0, 100]` | Mirror to the other side of the base (`flip` on, default), or clamp to the boundary (`flip` off) |
|
|
1081
|
+
| `tone` resolves outside 0–100 | Clamp silently |
|
|
1082
|
+
| `'max'` / `'min'` without `base` | Allowed — resolves to the scheme's tone extreme (root color) |
|
|
1046
1083
|
| `saturation` outside 0–1 | Clamp silently |
|
|
1047
1084
|
| Circular `base` references | Validation error |
|
|
1048
1085
|
| `base` references non-existent name | Validation error |
|
|
@@ -1112,35 +1149,67 @@ formatOklch(280, 60, 95); // 'oklch(0.95 ... 280)'
|
|
|
1112
1149
|
|
|
1113
1150
|
To attach an alpha component, use `glaze.format(variant, format)` on a `ResolvedColorVariant` (which carries the `alpha` channel) instead of these raw writers.
|
|
1114
1151
|
|
|
1152
|
+
### OKHST tone utilities
|
|
1153
|
+
|
|
1154
|
+
```ts
|
|
1155
|
+
import {
|
|
1156
|
+
toTone,
|
|
1157
|
+
fromTone,
|
|
1158
|
+
toneFromY,
|
|
1159
|
+
yFromTone,
|
|
1160
|
+
okhstToOkhsl,
|
|
1161
|
+
okhslToOkhst,
|
|
1162
|
+
variantToOkhsl,
|
|
1163
|
+
REF_EPS,
|
|
1164
|
+
} from '@tenphi/glaze';
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
| Function | Description |
|
|
1168
|
+
|---|---|
|
|
1169
|
+
| `toTone(l, eps?)` | OKHSL lightness (0–1) → tone (0–100). Defaults to `REF_EPS`. |
|
|
1170
|
+
| `fromTone(t, eps?)` | Tone (0–100) → OKHSL lightness (0–1). Inverse of `toTone`. |
|
|
1171
|
+
| `toneFromY(y, eps?)` / `yFromTone(t, eps?)` | Same transfer in luminance space (0–1). |
|
|
1172
|
+
| `okhstToOkhsl({ h, s, t })` | OKHST → OKHSL (`{ h, s, l }`). |
|
|
1173
|
+
| `okhslToOkhst({ h, s, l })` | OKHSL → OKHST (`{ h, s, t }`). |
|
|
1174
|
+
| `variantToOkhsl(variant)` | `ResolvedColorVariant` (stores `t`) → `{ h, s, l, alpha }` for rendering. |
|
|
1175
|
+
| `REF_EPS` | Reference epsilon (`0.05`) for the canonical tone axis. |
|
|
1176
|
+
|
|
1177
|
+
`ResolvedColorVariant` now stores `{ h, s, t, alpha }` (tone, not lightness). Use `variantToOkhsl(variant).l` to recover OKHSL lightness. See [`docs/okhst.md`](okhst.md) for the full model.
|
|
1178
|
+
|
|
1115
1179
|
### Contrast solver
|
|
1116
1180
|
|
|
1117
1181
|
```ts
|
|
1118
1182
|
import {
|
|
1119
|
-
|
|
1183
|
+
findToneForContrast,
|
|
1120
1184
|
findValueForMixContrast,
|
|
1185
|
+
resolveContrastForMode,
|
|
1121
1186
|
resolveMinContrast,
|
|
1187
|
+
apcaContrast,
|
|
1122
1188
|
} from '@tenphi/glaze';
|
|
1123
1189
|
```
|
|
1124
1190
|
|
|
1125
1191
|
| Function | Description |
|
|
1126
1192
|
|---|---|
|
|
1127
|
-
| `
|
|
1193
|
+
| `findToneForContrast(opts)` | Binary-search for the tone (0–1) that meets a contrast floor (WCAG or APCA) against a base color. Returns `{ tone, contrast, met, branch, flipped? }`. |
|
|
1128
1194
|
| `findValueForMixContrast(opts)` | Same, but searches for a mix `value` (0–1) that meets a contrast floor between a base and a target. |
|
|
1129
|
-
| `
|
|
1195
|
+
| `resolveContrastForMode(spec, isHC)` | Resolves a `ContrastSpec` to `{ metric: 'wcag' \| 'apca', target }` for the requested mode (picks the normal or HC entry of any pair). |
|
|
1196
|
+
| `resolveMinContrast(value)` | Resolves a `MinContrast` (WCAG preset or number) to a numeric ratio. |
|
|
1197
|
+
| `apcaContrast(yText, yBg)` | APCA Lc magnitude (0–106) for two relative luminances. |
|
|
1130
1198
|
|
|
1131
|
-
`
|
|
1199
|
+
`findToneForContrast` options:
|
|
1132
1200
|
|
|
1133
1201
|
| Option | Default | Description |
|
|
1134
1202
|
|---|---|---|
|
|
1135
1203
|
| `hue` | — | Candidate hue (0–360). |
|
|
1136
1204
|
| `saturation` | — | Candidate saturation (0–1). |
|
|
1137
|
-
| `
|
|
1205
|
+
| `preferredTone` | — | Preferred candidate tone (0–1). Kept if it already meets the target. |
|
|
1138
1206
|
| `baseLinearRgb` | — | Base color as linear sRGB tuple. |
|
|
1139
|
-
| `contrast` | — |
|
|
1140
|
-
| `
|
|
1207
|
+
| `contrast` | — | `ResolvedContrast` (`{ metric, target }`). |
|
|
1208
|
+
| `toneRange` | `[0, 1]` | Search bounds in tone. |
|
|
1141
1209
|
| `epsilon` | `1e-4` | Convergence threshold. |
|
|
1142
|
-
| `maxIterations` | `
|
|
1143
|
-
| `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`).
|
|
1210
|
+
| `maxIterations` | `18` | Max binary-search iterations per branch. |
|
|
1211
|
+
| `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`). |
|
|
1144
1212
|
| `flip` | `false` | When `true`, try the opposite direction if the initial one doesn't meet the target. When `false`, only the initial direction is searched — unmet contrasts pin the result to that direction's extreme. |
|
|
1213
|
+
| `saturationTaper` | `0` | When `> 0`, candidate saturation rolls off toward the tone extremes (same envelope the renderer applies), so the solved tone meets the floor at its rendered saturation. |
|
|
1145
1214
|
|
|
1146
|
-
Result: `{
|
|
1215
|
+
Result: `{ tone, contrast, met, branch: 'lighter' | 'darker' | 'preferred', flipped? }`. `flipped: true` indicates the initial direction failed and the opposite direction satisfied the target.
|