feat(design): the scheme set, the colour contract and the token utilities (E28 S3a)
Four schemes (Midnight; Forest cyan, Slate fuchsia, Matrix lime), YouTube and Starship retired with no data migration: normalizeTheme maps a stored retired scheme to Midnight. A vitest contract measures every scheme x mode (text 4.5:1, control edge 3:1, status text, the accent's 30-degree hue gap); it caught light-mode danger at 4.43:1, now red-700. The state layers, the strong edge, the touch target and the fades are index.css tokens and utilities the lab now reads. Backdrops recoloured to the new accents, with a grey overlay grain that actually breaks the banding (longest flat run 15-26px -> 4-8px). The glass tuner is retired.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// WCAG contrast and OKLCH hue distance on literal hex colours. The design constitution's colour rules
|
||||
// (docs/DESIGN.md, Tokens) are measured with these: theme.contract.test.ts runs them over every scheme
|
||||
// in index.css, and the design lab's scheme tiles render the same numbers. OKLCH, because equal hue
|
||||
// angles there look equally far apart, unlike HSL's.
|
||||
|
||||
const rgb = (hex: string) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
||||
const lin2 = (v: number) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
||||
|
||||
export function hexContrast(a: string, b: string): number {
|
||||
const l = (h: string) => {
|
||||
const [r = 0, g = 0, bl = 0] = rgb(h).map(lin2);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * bl;
|
||||
};
|
||||
const x = l(a);
|
||||
const y = l(b);
|
||||
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
||||
}
|
||||
|
||||
function hue(hex: string): number {
|
||||
const [r = 0, g = 0, b = 0] = rgb(hex).map(lin2);
|
||||
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
||||
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
||||
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
||||
const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
||||
const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
||||
return ((Math.atan2(B, A) * 180) / Math.PI + 360) % 360;
|
||||
}
|
||||
|
||||
/** The OKLCH hue angle between two colours, 0..180°. */
|
||||
export function hueGap(a: string, b: string): number {
|
||||
const d = Math.abs(hue(a) - hue(b)) % 360;
|
||||
return Math.min(d, 360 - d);
|
||||
}
|
||||
|
||||
/** `color-mix(in srgb, a w, b)` for two opaque hex colours: the channels mix gamma-encoded, as the
|
||||
* browser does in the srgb space. */
|
||||
export function mixHex(a: string, b: string, w: number): string {
|
||||
const [x, y] = [rgb(a), rgb(b)];
|
||||
return `#${x
|
||||
.map((v, i) => Math.round((v * w + (y[i] ?? 0) * (1 - w)) * 255))
|
||||
.map((n) => n.toString(16).padStart(2, "0"))
|
||||
.join("")}`;
|
||||
}
|
||||
Reference in New Issue
Block a user