|
|
4 months ago | |
|---|---|---|
| .. | ||
| README.md | 4 months ago | |
| apca.ts | 4 months ago | |
| color.test.ts | 4 months ago | |
| convert.ts | 4 months ago | |
| generate.ts | 4 months ago | |
| index.ts | 4 months ago | |
| scheme.ts | 4 months ago | |
| types.ts | 4 months ago | |
README.md
color (uix.color)
Isomorphic color math engine. Pure, deterministic, DOM-free — the same
functions run at build (eidos render-css emits static CSS) and at runtime
(ActiveEidos.applyColorScheme for live / white-label theming). Because every
decision is computed in JS before a value is written, introspection (APCA
on-solid pick) and alpha fidelity are preserved in every mode.
Status — consumed (
COLOR_ENGINE_RFC.mdPhases 0/1/2/4). eidos'srender-cssconsumes it at build (APCA on-solid pick + OKLCH-native palette output);ActiveEidos.applyColorSchemeconsumes it at runtime (the live theme builder). Wide-gamut output (oklch()+ hex fallback) is default-on.
Why an art (and not an eidos lib)
The color math has two consumers — eidos's build-time generator (render-css) and
its runtime (ActiveEidos). Living in arts/ (below the UIX layers), like
uix.motion, lets both consume it with no cross-layer dependency. Per the arts
convention it imports no other art and touches no DOM.
Pipeline
hex ⇄ gamma sRGB ⇄ linear sRGB ⇄ OKLab ⇄ OKLCH (convert.ts)
│
APCA Lc / WCAG2 ratio │ (apca.ts)
seed + template → 12 OKLCH steps (morph) │ (generate.ts)
APCA on-solid pick · compositing-inverse alpha┘
OKLCH is the source / authoring space; wide gamut is preserved there. The sRGB
hex is a gamut-mapped fallback (chroma reduction, never channel clip — clipping
shifts the hue). The oklch(...) string keeps the full gamut for the browser.
API
| Function | Purpose |
|---|---|
parseColor(hex | 'oklch(...)' | [l,c,h]) |
→ Oklch |
oklchToHex / oklchToCss / oklchToGammaRgb |
OKLCH → sRGB fallback / oklch() string / gamma sRGB |
isInSrgbGamut |
does an OKLCH fit sRGB without chroma reduction? |
apcaLc(text, bg) / wcagContrastRatio(a, b) |
contrast (APCA Lc · WCAG2 ratio) — gamma sRGB inputs |
scaleToTemplate(hex[12]) |
hand-authored scale → OKLCH template (curve donor) |
pickNearestTemplate(seed, templates) |
choose the donor whose solid is closest (L-weighted) |
generateScale(seed, template, { solidStep? }) |
seed → 12 OKLCH steps (morph; solid anchored to seed) |
pickOnSolid(solid, { onSolid, onSolidContrast }, floor?) |
APCA pick of on-solid text + passes |
alphaOverBackground(solid, background) |
translucent fill (rgb, alpha) that over background = solid |
deriveScheme(seed, variant?) |
brand seed → hierarchy role seeds (Material 3 CorePalette) |
temper(color, reference, amount?) |
match a reference's L+C keeping hue (cohere intents) |
harmonize(color, toward, amount?) |
rotate hue toward a reference (M3 blend; brand accents) |
The generator (template morph)
Radix's generateRadixColors spirit: a hand-tuned scale donates its perceptual
L-curve and chroma profile; the seed re-hues it and rescales chroma
so the solid step lands on the seed exactly. Beats a naive l − k linear ramp
(constant chroma → muddy mids) because the donor's curve is already perceptually
placed. The 31 Radix scales eidos already ships become the template library.
Theme builder (scheme derivation)
deriveScheme(seed, variant?) ports Material 3's HCT CorePalette to OKLCH: one
brand seed → the hierarchy role seeds (primary / secondary / tertiary /
neutral / neutralVariant).
- secondary = same hue, low chroma · tertiary = hue + 60°, moderate chroma · neutral = same hue, near-zero chroma. (Structure is exact; chroma is recalibrated HCT→OKLCH. Tone→contrast is NOT ported — APCA decides contrast.)
- Variants (
SchemeVariant):tonal(default),vibrant,monochrome. - The 6 canonical intents are NOT derived (an error is always red). To cohere them
with a brand WITHOUT losing meaning,
temper(color, reference, amount?)matches the brand's perceptual temperature (L + C) while keeping the hue (red stays red) — the right tool for intents.harmonize(rotates hue) also exists, but for brand ACCENTS, not semantic intents (rotating an intent's hue erodes its meaning).
Composition + apply: buildScheme(seed, opts) (in eidos/lib, pure) composes
deriveScheme + generateScale + APCA + alpha into the --primitive-{role}-* token
map; ActiveEidos.applyColorScheme(seed, opts) writes it as a managed style block
(hex fallback + oklch() wide-gamut), follows light/dark, and returns the scheme for
introspection. Same math at build or runtime; only VALUES behind the frozen
--color-{role}-{slot} contract, so it touches no component. opts: variant
(tonal/vibrant/monochrome) + temper (intent cohesion) + per-role overrides. Full
spec: ../../uix/eidos/COLOR_ENGINE_RFC.md §6.2 + THEMING.md §26.
Notes
- APCA is a WCAG 3 draft, not legal conformance.
pickOnSoliddecides by APCA butonSolidWcagRatiois available as a documented cross-check / safety floor. - Wide-gamut output is live + default-on (RFC §7 strategy A): the palette emits a
hex fallback + an
oklch()sibling that wins where supported → wide-gamut on P3 with no@media. Explicitcolor(display-p3 …)(strategy B) stays deferred — only if an app needs hand-tuned P3 values. NOTE: an sRGB-authored palette renders identically inoklch()(no P3 data to recover); real P3 needs a wide-gamut SOURCE (a vivid seed / OKLCH-authored theme), which the generator preserves (raw OKLCH, never gamut-clamped).