Skip to content

Instantly share code, notes, and snippets.

@pushkine
Last active August 26, 2024 04:35
Show Gist options
  • Save pushkine/c8ba98294233d32ab71b7e19a0ebdbb9 to your computer and use it in GitHub Desktop.
Save pushkine/c8ba98294233d32ab71b7e19a0ebdbb9 to your computer and use it in GitHub Desktop.
Javascript color conversion algorithms. Complete HEX, HSL, RGB and named css color parsing & interpolation in the HCL color space. All constants directly sourced from the google/chromium open source project. Play, compare and benchmark against d3 on https://svelte.dev/repl/0a40a8348f8841d0b7007c58e4d9b54c
type RGBA = [number, number, number, number];
const rgb255 = (v: number) => (v < 255 ? (v > 0 ? v : 0) : 255);
const b1 = (v: number) => (v > 0.0031308 ? v ** (1 / 2.4) * 269.025 - 14.025 : v * 3294.6);
const b2 = (v: number) => (v > 0.2068965 ? v ** 3 : (v - 4 / 29) * (108 / 841));
const a1 = (v: number) => (v > 10.314724 ? ((v + 14.025) / 269.025) ** 2.4 : v / 3294.6);
const a2 = (v: number) => (v > 0.0088564 ? v ** (1 / 3) : v / (108 / 841) + 4 / 29);
function fromHCL(h: number, c: number, l: number): RGB {
const y = b2((l = (l + 16) / 116));
const x = b2(l + (c / 500) * Math.cos((h *= Math.PI / 180)));
const z = b2(l - (c / 200) * Math.sin(h));
return [
rgb255(b1(x * 3.021973625 - y * 1.617392459 - z * 0.404875592)),
rgb255(b1(x * -0.943766287 + y * 1.916279586 + z * 0.027607165)),
rgb255(b1(x * 0.069407491 - y * 0.22898585 + z * 1.159737864)),
];
}
function toHCL(r: number, g: number, b: number) {
const y = a2((r = a1(r)) * 0.222488403 + (g = a1(g)) * 0.716873169 + (b = a1(b)) * 0.06060791);
const l = 500 * (a2(r * 0.452247074 + g * 0.399439023 + b * 0.148375274) - y);
const q = 200 * (y - a2(r * 0.016863605 + g * 0.117638439 + b * 0.865350722));
const h = Math.atan2(q, l) * (180 / Math.PI);
return [h < 0 ? h + 360 : h, Math.sqrt(l * l + q * q), 116 * y - 16];
}
function fromHEX([, p1, p2, p3, p4, p5, p6, p7, p8]: string): RGBA {
const f = (v: string) => parseInt(v, 16);
return p5 == null
? [f(p1) * 17, f(p2) * 17, f(p3) * 17, +(p4 == null) || f(p4) / 15]
: [f(p1 + p2), f(p3 + p4), f(p5 + p6), +(p7 == null) || f(p7 + p8) / 255];
}
function toHEX(has_alpha: boolean) {
return has_alpha
? ([r, g, b]: RGB, a: number = 1) =>
`#${(16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1)}${Math.round(a * 255).toString(16)}`
: ([r, g, b]: RGB) => `#${(16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1)}`;
}
function fromRGB(str: string): RGBA {
const [, r, g, b, a = 1.0] = /^rgba?\(([^,]*),([^,]*),([^,)]*),?([^)]*)?\)$/.exec(str);
const f = (v: string, p = -1) => (-1 === (p = v.lastIndexOf("%")) ? +v : +v.slice(0, p) * 2.55);
return [f(r), f(g), f(b), +a];
}
function toRGB(has_alpha: boolean, uses_pct: boolean) {
const f = uses_pct
? (v: number) => (v < 255 ? (v > 0 ? `${v / 2.55}%` : `0%`) : `100%`)
: (v: number) => (v < 254.5 ? (v > 0.5 ? v.toPrecision(3) : `0`) : `255`);
return has_alpha
? ([r, g, b]: RGB, a: number = 1) => `rgba(${f(r)},${f(g)},${f(b)},${a})`
: ([r, g, b]: RGB) => `rgb(${f(r)},${f(g)},${f(b)})`;
}
function fromHSL(str: string): RGBA {
const [, hh, ss, ll, a = 1.0] = /^hsla?\((-?[^,]*),([^,]*),([^,)]*),?([^)]*)?\)$/.exec(str);
const h = (+hh < 0 ? 360 - (-hh % 360) : +hh % 360) / 60;
const l = +ll.slice(0, ll.lastIndexOf("%")) / 100.0;
const v1 = l + (+ss.slice(0, ss.lastIndexOf("%")) / 100.0) * (l < 0.5 ? l : 1 - l);
const v2 = l + l - v1;
const r: RGBA = [255.0, 255.0, 255.0, +a];
const i = Math.floor(h);
r[((i + 1) >> 1) % 3] *= v1;
r[((i << 1) | 2) % 3] *= v2;
r[(7 - i) % 3] *= v2 + (v1 - v2) * (1 === i % 2 ? i + 1 - h : h - i);
return r;
}
function toHSL(has_alpha: boolean) {
const c = (l: number, d: number, h: number, x: number) =>
`${(h = 60 * (x + h / d)) < 0 ? h + 360 : h > 360 ? h - 360 : h},${100 * (d / (l < 255 ? l : 510 - l))}%,${l / 5.1}%`;
const f = ([r, g, b]: RGB) =>
b > g
? g > r
? c(b + r, b - r, b - g, 3)
: b > r
? c(b + g, b - g, r - g, 4)
: c(r + g, r - g, g - b, 0)
: b > r
? c(g + r, g - r, b - r, 2)
: g > r
? c(g + b, g - b, g - r, 1)
: r !== b
? c(r + b, r - b, g - b, 0)
: `0,0%,${r / 2.55}%`;
return has_alpha ? (r: RGB, a: number = 1) => `hsla(${f(r)},${a})` : (r: RGB) => `hsl(${f(r)})`;
}
type toColor = typeof toHSL | typeof toHEX | typeof toRGB;
function parseColor(str: string, from_alpha: void | number): RGBA | [RGBA, ReturnType<toColor>, boolean] {
const [p1, p2, , p4] = (str = str.trim());
const s = { "#": 0, r: 5, h: 6 }[p1];
if (null == s || (s & 4 && null == { g: 5, s: 6 }[p2])) {
const ctx = document.createElement("canvas").getContext("2d");
ctx.fillStyle = str;
const r = ctx.fillStyle;
if (!str || !r || str === r) throw new Error(`Could not parse color "${str}"`);
return parseColor(r, from_alpha);
}
const rgba = (s & 1 ? fromRGB : s & 2 ? fromHSL : fromHEX)(str);
if (null === from_alpha) return rgba;
const has_alpha = (rgba[3] !== from_alpha && from_alpha !== 1) || s & 4 ? "a" === p4 : 9 === str.length || 5 === str.length;
return [rgba, (s & 1 ? toRGB : s & 2 ? toHSL : toHEX)(has_alpha, s & 1 && -1 !== str.indexOf("%")), has_alpha];
}
function colors(from: string, to: string) {
const [r1, g1, b1, a1] = parseColor(from, null) as RGBA;
const [[r2, g2, b2, a2], toFormat, has_alpha] = parseColor(to, a1) as [RGBA, ReturnType<toColor>, boolean];
const [h1, c1, l1] = toHCL(r1, g1, b1);
const [h2, c2, l2] = toHCL(r2, g2, b2);
const dc = c2 - c1;
const dl = l2 - l1;
const da = a2 - a1;
let dh = h2 - h1;
if (Math.abs(dh) > 220) dh -= 360 * Math.round(dh / 360);
return has_alpha
? (t: number) => toFormat(fromHCL(h1 + dh * t, c1 + dc * t, l1 + dl * t), a1 + da * t)
: (t: number) => toFormat(fromHCL(h1 + dh * t, c1 + dc * t, l1 + dl * t));
}
/**
* MIT License github.com/pushkine/
* All constants sourced from chromium open source
*
* const u = (x) => x * 1.52587890625e-5;
* const ICC_sRGB_D50_Bradford = [
* [u(0x6fa2), u(0x6299), u(0x24a0)],
* [u(0x38f5), u(0xb785), u(0x0f84)],
* [u(0x0390), u(0x18da), u(0xb6cf)],
* ];
*
* sRGB + CIE constants are precomputed
*
* sRGB D50 Bradford + CIE XYZ D50
*
* [ 0.436065674, 0.385147095, 0.143066406] [ 0.452247074, 0.399439023, 0.148375274] / 0.96422
* [ 0.222488403, 0.716873169, 0.060607910] [ 0.222488403, 0.716873169, 0.060607910] / 1.00000
* [ 0.013916016, 0.097076416, 0.714096069] [ 0.016863605, 0.117638439, 0.865350722] / 0.82521
*
* [ 3.134112153,-1.617392459,-0.490633405] [ 3.021973625,-1.617392459,-0.404875592]
* [-0.978787296, 1.916279586, 0.033454714] [-0.943766287, 1.916279586, 0.027607165]
* [ 0.071983044,-0.228985850, 1.405385132] [ 0.069407491,-0.228985850, 1.159737864]
*
* * 0.96422 * 1.0 * 0.82521
*
* δ 6 / 29 0.2068965
* t0 δ ** 3 0.0088564
* ϵ 216 / 24389 0.0088564
* slope ? 0.003130805
* gamma corr 12.92 * 0.00313 0.0404499
* *255 10.314724
*
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment