UNPKG

alouette

Version:

A modern, customizable design system built on top of NativeWind v5 with configurable defaults

86 lines (78 loc) 3.55 kB
/* eslint-disable import-x/extensions */ // OKLCH color-scale generator. Palettes are generated in OKLCH so a single // lightness ramp is perceptually uniform across every hue (HSL lightness is not // — yellow at HSL L=56 reads far brighter than blue at the same L). Chroma is // specified *relative to the sRGB gamut*: each step requests a fraction of the // maximum chroma sRGB can render at that step's lightness and hue. The fraction // curve is shared by every palette and tiered by usage (muted surface tints, // medium highlight mids, near-vivid interactive and text steps), so all hues are // equally saturated relative to what's possible — no per-palette chroma table. import Color from "colorjs.io"; import type { PaletteSpec } from "./paletteSpecs.ts"; import type { Mode, ScaleNum } from "./tokenScaleMap.ts"; export type ColorScale = Record<ScaleNum, string>; // step: 1 2 3 4 5 6 7 8 9 10 11 const lightnessRamps = { grayscale: { dark: [0.18, 0.24, 0.28, 0.32, 0.36, 0.4, 0.45, 0.795, 0.865, 0.96, 1], // only diff is on the first 2 values light: [1, 0.98, 0.948, 0.89, 0.85, 0.79, 0.61, 0.54, 0.48, 0.42, 0.27], }, accent: { dark: [0.18, 0.22, 0.26, 0.3, 0.34, 0.4, 0.5, 0.7, 0.82, 0.865, 0.955], light: [ 0.988, 0.968, 0.948, 0.89, 0.85, 0.79, 0.62, 0.54, 0.48, 0.42, 0.27, ], }, brightAccent: { dark: [0.28, 0.32, 0.4, 0.45, 0.5, 0.53, 0.56, 0.8, 0.86, 0.9, 0.955], light: [0.988, 0.968, 0.948, 0.928, 0.89, 0.82, 0.8, 0.6, 0.56, 0.44, 0.27], }, } as const; // Fraction of the max in-gamut chroma requested at each step. Usage tiers // mirror the old HSL generator's 56/82/96 saturation bands: steps 1-3 pale // surfaces, 4-6 highlight/message tints, 7-11 interactive fills and text. const relativeChromaCurve: Record<Mode, number[]> = { dark: [0.6, 0.68, 0.72, 0.76, 0.78, 0.84, 0.88, 0.88, 0.85, 0.82, 0.86], light: [0.5, 0.52, 0.55, 0.6, 0.75, 0.78, 0.92, 0.97, 0.97, 0.97, 0.95], }; // Largest OKLCH chroma still inside the sRGB gamut at this lightness/hue. export const maxSrgbChroma = (lightness: number, hue: number): number => { let low = 0; let high = 0.5; for (let i = 0; i < 20; i++) { const mid = (low + high) / 2; if (new Color("oklch", [lightness, mid, hue]).to("srgb").inGamut()) { low = mid; } else { high = mid; } } return low; }; const toHex = (lightness: number, chroma: number, hue: number): string => { const color = new Color("oklch", [lightness, chroma, hue]).to("srgb"); const hex = color.toGamut({ method: "css" }).toString({ format: "hex" }); // colorjs collapses to 3-digit shorthand (#050); expand so appended alpha // suffixes (e.g. selection = step + "40") stay valid 8-digit hex. const full = hex.length === 4 ? hex.replace(/^#(.)(.)(.)$/, "#$1$1$2$2$3$3") : hex; return full.toUpperCase(); }; export const createColorScale = (spec: PaletteSpec, mode: Mode): ColorScale => { const hueHi = spec.hueHi ?? spec.hue; const hueLo = spec.hueLo ?? spec.hue; const intensity = spec.intensity ?? 1; const ramp = lightnessRamps[spec.type][mode]; const steps = ramp.map((lightness, index) => { const hue = hueLo + (hueHi - hueLo) * lightness; const chroma = relativeChromaCurve[mode][index]! * intensity * maxSrgbChroma(lightness, hue); return toHex(lightness, chroma, hue); }); return Object.fromEntries( steps.map((hex, index) => [index + 1, hex]), ) as unknown as ColorScale; };