← back to Dw Pairs Well
lib/color.js
105 lines
// hex <-> CIE Lab + ΔE2000
function hexToRgb(hex) {
if (!hex) return null;
var h = String(hex).trim().replace(/^#/, '');
if (h.length === 3) h = h.split('').map(function (c) { return c + c; }).join('');
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16)
};
}
function rgbToXyz(rgb) {
function f(c) {
c = c / 255;
return c > 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92;
}
var r = f(rgb.r), g = f(rgb.g), b = f(rgb.b);
return {
x: (r * 0.4124564 + g * 0.3575761 + b * 0.1804375) * 100,
y: (r * 0.2126729 + g * 0.7151522 + b * 0.0721750) * 100,
z: (r * 0.0193339 + g * 0.1191920 + b * 0.9503041) * 100
};
}
function xyzToLab(xyz) {
// D65 reference white
var Xn = 95.0489, Yn = 100.0, Zn = 108.8840;
function f(t) { return t > 0.008856 ? Math.cbrt(t) : (7.787 * t) + (16 / 116); }
var fx = f(xyz.x / Xn), fy = f(xyz.y / Yn), fz = f(xyz.z / Zn);
return { L: (116 * fy) - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
}
function hexToLab(hex) {
var rgb = hexToRgb(hex);
if (!rgb) return null;
return xyzToLab(rgbToXyz(rgb));
}
// ΔE2000 — perceptual color distance. Lower = more similar.
function deltaE2000(lab1, lab2) {
if (!lab1 || !lab2) return Infinity;
var L1 = lab1.L, a1 = lab1.a, b1 = lab1.b;
var L2 = lab2.L, a2 = lab2.a, b2 = lab2.b;
var avgL = (L1 + L2) / 2;
var C1 = Math.sqrt(a1 * a1 + b1 * b1);
var C2 = Math.sqrt(a2 * a2 + b2 * b2);
var avgC = (C1 + C2) / 2;
var G = 0.5 * (1 - Math.sqrt(Math.pow(avgC, 7) / (Math.pow(avgC, 7) + Math.pow(25, 7))));
var a1p = (1 + G) * a1, a2p = (1 + G) * a2;
var C1p = Math.sqrt(a1p * a1p + b1 * b1), C2p = Math.sqrt(a2p * a2p + b2 * b2);
var avgCp = (C1p + C2p) / 2;
function hp(ap, bp) {
if (ap === 0 && bp === 0) return 0;
var h = Math.atan2(bp, ap) * 180 / Math.PI;
return h >= 0 ? h : h + 360;
}
var h1p = hp(a1p, b1), h2p = hp(a2p, b2);
var dhp;
if (C1p * C2p === 0) dhp = 0;
else {
dhp = h2p - h1p;
if (dhp > 180) dhp -= 360;
if (dhp < -180) dhp += 360;
}
var dLp = L2 - L1, dCp = C2p - C1p;
var dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin((dhp / 2) * Math.PI / 180);
var avgHp;
if (C1p * C2p === 0) avgHp = h1p + h2p;
else if (Math.abs(h1p - h2p) <= 180) avgHp = (h1p + h2p) / 2;
else if (h1p + h2p < 360) avgHp = (h1p + h2p + 360) / 2;
else avgHp = (h1p + h2p - 360) / 2;
var T = 1
- 0.17 * Math.cos((avgHp - 30) * Math.PI / 180)
+ 0.24 * Math.cos((2 * avgHp) * Math.PI / 180)
+ 0.32 * Math.cos((3 * avgHp + 6) * Math.PI / 180)
- 0.20 * Math.cos((4 * avgHp - 63) * Math.PI / 180);
var dTheta = 30 * Math.exp(-Math.pow((avgHp - 275) / 25, 2));
var Rc = 2 * Math.sqrt(Math.pow(avgCp, 7) / (Math.pow(avgCp, 7) + Math.pow(25, 7)));
var Sl = 1 + ((0.015 * Math.pow(avgL - 50, 2)) / Math.sqrt(20 + Math.pow(avgL - 50, 2)));
var Sc = 1 + 0.045 * avgCp;
var Sh = 1 + 0.015 * avgCp * T;
var Rt = -Math.sin(2 * dTheta * Math.PI / 180) * Rc;
return Math.sqrt(
Math.pow(dLp / Sl, 2) +
Math.pow(dCp / Sc, 2) +
Math.pow(dHp / Sh, 2) +
Rt * (dCp / Sc) * (dHp / Sh)
);
}
function safeParseHexArray(value) {
if (!value) return [];
if (Array.isArray(value)) return value.filter(Boolean);
try {
var parsed = JSON.parse(value);
if (Array.isArray(parsed)) return parsed.filter(Boolean);
} catch (_) { /* not JSON */ }
return [];
}
module.exports = { hexToRgb, hexToLab, deltaE2000, safeParseHexArray };