You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
75 lines
3.5 KiB
75 lines
3.5 KiB
|
3 months ago
|
# `$libs/gradient` — the canonical gradient model
|
||
|
|
|
||
|
|
A pure, zero-dependency gradient **model** + CSS **serializer**, shared by two
|
||
|
|
consumers so a gradient is one thing in both places:
|
||
|
|
|
||
|
|
- the **eidos theming axis** ([`uix/eidos/lib/build-gradient.ts`](../../uix/eidos/lib/build-gradient.ts))
|
||
|
|
— derives named `--gradient-*` tokens from color roles;
|
||
|
|
- the **GradientBuilder component** (soma, Phase 2) — the interactive editor.
|
||
|
|
|
||
|
|
A gradient built in the editor is therefore also a themeable token, and a token
|
||
|
|
re-opens in the editor. No other system unifies "gradient derived from a color
|
||
|
|
system" with "hand-edited gradient" — that unification is the whole point of
|
||
|
|
keeping the model in a lib both layers can import (below soma, no layer
|
||
|
|
violation).
|
||
|
|
|
||
|
|
## The model (`model.ts`)
|
||
|
|
|
||
|
|
```ts
|
||
|
|
type Gradient = LinearGradient | RadialGradient | ConicGradient | MeshGradient
|
||
|
|
|
||
|
|
interface GradientStop {
|
||
|
|
color: StopColor // role ref | OKLCH literal | raw css
|
||
|
|
position?: number // 0..1 along the line
|
||
|
|
alpha?: number // 0..1
|
||
|
|
}
|
||
|
|
|
||
|
|
type StopColor =
|
||
|
|
| { kind: 'role'; role: string; slot?: string; step?: number } // → var(--color-…) — RE-TINTS
|
||
|
|
| { kind: 'oklch'; value: Oklch } // stored perceptual literal
|
||
|
|
| { kind: 'css'; value: string } // transparent / currentColor
|
||
|
|
```
|
||
|
|
|
||
|
|
Geometry is **explicit per kind** (`LinearGradient.angle`, `RadialGradient.shape/size/at`,
|
||
|
|
`ConicGradient.from/at`, `MeshGradient.points`) so it round-trips losslessly to
|
||
|
|
CSS and maps trivially to a future on-canvas gizmo + keyboard editing — no opaque
|
||
|
|
affine matrix. Colors are stored as **`Oklch`** (not hex): the "store OKLCH, not
|
||
|
|
hex" property that keeps gradients gamut-honest and perceptually interpolated.
|
||
|
|
|
||
|
|
### Why role stops are the headline
|
||
|
|
|
||
|
|
A `{ kind: 'role', … }` stop serializes to `var(--color-{role}-{slot})` rather
|
||
|
|
than a frozen color. So a single `--gradient-{name}` token **re-tints with the
|
||
|
|
brand seed and flips light/dark for free** — Tailwind / Open Props / Panda blend
|
||
|
|
two literal endpoints; Material 3 has no gradient axis at all. Nobody derives the
|
||
|
|
stops themselves from a color system.
|
||
|
|
|
||
|
|
## The serializer (`to-css.ts`)
|
||
|
|
|
||
|
|
```ts
|
||
|
|
gradientToCss(g: Gradient): string // model → a CSS <gradient> (or stacked-radial mesh) string
|
||
|
|
meshToCss(m: MeshGradient): string
|
||
|
|
stopColorToCss(c: StopColor, alpha?): string
|
||
|
|
```
|
||
|
|
|
||
|
|
- Role stops → `var(--color-…)`; OKLCH → `oklch(L% C H / a)`; css → verbatim.
|
||
|
|
- Interpolation defaults to **`in oklch`** (perceptual; not the muddy sRGB
|
||
|
|
midpoint dead-zone) — emitted geometry-first then `in <space> [<hue>]`, the
|
||
|
|
ordering Tailwind v4 ships in production. Hue paths (`longer`/`shorter`) drive
|
||
|
|
aurora / iridescent sweeps from two role endpoints.
|
||
|
|
- Mesh serializes to **layered `radial-gradient`s over a base** — the only
|
||
|
|
portable CSS mesh today (native CSS mesh is unshipped). Re-tints because each
|
||
|
|
blob is a role/OKLCH color in the model.
|
||
|
|
|
||
|
|
The OKLCH→hex **fallback** sibling (wide-gamut honesty for pinned literals) is a
|
||
|
|
*consumer* concern (eidos has `$color`); this lib stays pure and emits the
|
||
|
|
`oklch()` form.
|
||
|
|
|
||
|
|
## Status
|
||
|
|
|
||
|
|
- ✅ model + `gradientToCss` (this lib) — tested in [`to-css.test.ts`](./to-css.test.ts).
|
||
|
|
- ✅ eidos axis (`buildGradient` / `applyGradients`) — see `uix/eidos`.
|
||
|
|
- ⏳ `parseGradient` (CSS → model, lossless round-trip) — Phase 2, what the
|
||
|
|
GradientBuilder needs (paste-to-edit). The model is shaped for it (`hints`,
|
||
|
|
explicit geometry) but it is not implemented yet.
|