export interface Rgba { r: number; g: number; b: number; a: number; } const NAMED: Record = { black: "#000000", white: "#ffffff", red: "#ff0000", green: "#008000", blue: "#0000ff", yellow: "#ffff00", orange: "#ffa500", purple: "#800080", gray: "#808080", grey: "#808080", silver: "#c0c0c0", maroon: "#800000", olive: "#808000", lime: "#00ff00", aqua: "#00ffff", teal: "#008080", navy: "#000080", fuchsia: "#ff00ff", cyan: "#00ffff", gold: "#ffd700", }; export function parseColor(value: string): Rgba | null { const v = value.trim().toLowerCase(); if (v === "transparent") { return { r: 0, g: 0, b: 0, a: 0 }; } const named = NAMED[v]; if (named) { return parseColor(named); } const hexMatch = v.match(/^#([0-9a-f]{3,8})$/); if (hexMatch) { const hex = hexMatch[1] ?? ""; if (hex.length === 3 || hex.length === 4) { return { r: Number.parseInt(`${hex[0]}${hex[0]}`, 16), g: Number.parseInt(`${hex[1]}${hex[1]}`, 16), b: Number.parseInt(`${hex[2]}${hex[2]}`, 16), a: hex.length === 4 ? Number.parseInt(`${hex[3]}${hex[3]}`, 16) / 255 : 1, }; } if (hex.length === 6 || hex.length === 8) { return { r: Number.parseInt(hex.slice(0, 2), 16), g: Number.parseInt(hex.slice(2, 4), 16), b: Number.parseInt(hex.slice(4, 6), 16), a: hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1, }; } return null; } const fnMatch = v.match(/^rgba?\(([^)]*)\)$/); if (fnMatch) { const parts = (fnMatch[1] ?? "").split(/[\s,/]+/).filter(Boolean); if (parts.length < 3) { return null; } const channel = (raw: string): number => raw.endsWith("%") ? Math.round((Number.parseFloat(raw) / 100) * 255) : Math.round(Number.parseFloat(raw)); const r = channel(parts[0] ?? ""); const g = channel(parts[1] ?? ""); const b = channel(parts[2] ?? ""); const alphaRaw = parts[3]; const a = alphaRaw === undefined ? 1 : alphaRaw.endsWith("%") ? Number.parseFloat(alphaRaw) / 100 : Number.parseFloat(alphaRaw); if ([r, g, b].some((n) => Number.isNaN(n)) || Number.isNaN(a)) { return null; } return { r, g, b, a: Math.min(Math.max(a, 0), 1) }; } return null; } export function blend(fg: Rgba, bg: Rgba): Rgba { const a = fg.a + bg.a * (1 - fg.a); if (a === 0) { return { r: 0, g: 0, b: 0, a: 0 }; } const mix = (f: number, b: number): number => Math.round((f * fg.a + b * bg.a * (1 - fg.a)) / a); return { r: mix(fg.r, bg.r), g: mix(fg.g, bg.g), b: mix(fg.b, bg.b), a }; } export function rgbDistance(a: Rgba, b: Rgba): number { return Math.hypot(a.r - b.r, a.g - b.g, a.b - b.b); } export function toHex(color: Rgba): string { const part = (n: number): string => n.toString(16).padStart(2, "0"); return `#${part(color.r)}${part(color.g)}${part(color.b)}`; }