127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
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;
|
||
}
|
||
}
|