export type Oklch = { type: 'oklch', l: number, c: number, h: number }; export type Oklab = { type: 'oklab', l: number, a: number, b: number }; export type Srgb = { type: 'srgb', r: number, g: number, b: number }; export type Lrgb = { type: 'lrgb', r: number, g: number, b: number }; export type AnyColor = Oklch | Oklab | Srgb; type Deg = number; type Rad = number; function deg2rad(θ: Deg): Rad { return θ / 180 * Math.PI; } function rad2deg(θ: Rad): Deg { return θ * 180 / Math.PI; } function dcos(θ: Deg): number { return Math.cos(deg2rad(θ)); } function dsin(θ: Deg): number { return Math.sin(deg2rad(θ)); } function datan2(b: number, a: number): Deg { return rad2deg(Math.atan2(b, a)); } export function normDeg(θ: Deg): Deg { θ %= 360; return θ < 0 ? θ + 360 : θ; } export function oklch2oklab({ l, c, h }: Oklch): Oklab { return { type: 'oklab', l, a: c * dcos(h), b: c * dsin(h) }; } export function oklab2oklch({ l, a, b }: Oklab): Oklch { return { type: 'oklch', l, c: Math.sqrt(a*a + b*b), h: datan2(b, a) } } // https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab export function lrgb2oklab({ r, g, b }: Lrgb): Oklab { const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; const l_ = Math.cbrt(l); const m_ = Math.cbrt(m); const s_ = Math.cbrt(s); return { type: 'oklab', l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, }; } export function oklab2lrgb({ l, a, b }: Oklab): Lrgb { const L_ = l + 0.3963377774 * a + 0.2158037573 * b; const M_ = l - 0.1055613458 * a - 0.0638541728 * b; const S_ = l - 0.0894841775 * a - 1.2914855480 * b; const L = L_ * L_ * L_; const M = M_ * M_ * M_; const S = S_ * S_ * S_; return { type: 'lrgb', r: clamp(+4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S), g: clamp(-1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S), b: clamp(-0.0041960863 * L - 0.7034186147 * M + 1.7076147010 * S), }; } function clamp(x: number): number { return Math.max(0, Math.min(1, x)); } // https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F function γ(x: number): number { return x >= 0.0031308 ? 1.055 * x ** (1/2.4) - 0.055 : 12.92 * x; } function γ̂(x: number): number { return x >= 0.04045 ? ((x + 0.055)/1.055) ** 2.4 : x / 12.92; } export function lrgb2srgb({ r, g, b }: Lrgb): Srgb { return { type: 'srgb', r: γ(r), g: γ(g), b: γ(b) }; } export function srgb2lrgb({ r, g, b }: Srgb): Lrgb { return { type: 'lrgb', r: γ̂(r), g: γ̂(g), b: γ̂(b) }; } export function oklab2srgb(c: Oklab): Srgb { return lrgb2srgb(oklab2lrgb(c)); } export function oklch2srgb(c: Oklch): Srgb { return oklab2srgb(oklch2oklab(c)); } export function srgb2oklab(c: Srgb): Oklab { return lrgb2oklab(srgb2lrgb(c)); } export function srgb2oklch(c: Srgb): Oklch { return oklab2oklch(srgb2oklab(c)); } export function toOklch(c: AnyColor): Oklch { switch (c.type) { case 'oklch': return c; case 'oklab': return oklab2oklch(c); case 'srgb': return srgb2oklch(c); } } export function toOklab(c: AnyColor): Oklab { switch (c.type) { case 'oklch': return oklch2oklab(c); case 'oklab': return c; case 'srgb': return srgb2oklab(c); } } export function toSrgb(c: AnyColor): Srgb { switch (c.type) { case 'oklch': return oklch2srgb(c); case 'oklab': return oklab2srgb(c); case 'srgb': return c; } }