colorjs.io
Version:
Let’s get serious about color
166 lines (137 loc) • 4.58 kB
JavaScript
import lab from "../spaces/lab.js";
import lch from "../spaces/lch.js";
// deltaE2000 is a statistically significant improvement
// and is recommended by the CIE and Idealliance
// especially for color differences less than 10 deltaE76
// but is wicked complicated
// and many implementations have small errors!
// DeltaE2000 is also discontinuous; in case this
// matters to you, use deltaECMC instead.
const Gfactor = 25 ** 7;
const π = Math.PI;
const r2d = 180 / π;
const d2r = π / 180;
export default function (color, sample, {kL = 1, kC = 1, kH = 1} = {}) {
// Given this color as the reference
// and the function parameter as the sample,
// calculate deltaE 2000.
// This implementation assumes the parametric
// weighting factors kL, kC and kH
// for the influence of viewing conditions
// are all 1, as sadly seems typical.
// kL should be increased for lightness texture or noise
// and kC increased for chroma noise
let [L1, a1, b1] = lab.from(color);
let C1 = lch.from(lab, [L1, a1, b1])[1];
let [L2, a2, b2] = lab.from(sample);
let C2 = lch.from(lab, [L2, a2, b2])[1];
// Check for negative Chroma,
// which might happen through
// direct user input of LCH values
if (C1 < 0) {
C1 = 0;
}
if (C2 < 0) {
C2 = 0;
}
let Cbar = (C1 + C2)/2; // mean Chroma
// calculate a-axis asymmetry factor from mean Chroma
// this turns JND ellipses for near-neutral colors back into circles
let C7 = Cbar ** 7;
let G = 0.5 * (1 - Math.sqrt(C7/(C7 + Gfactor)));
// scale a axes by asymmetry factor
// this by the way is why there is no Lab2000 colorspace
let adash1 = (1 + G) * a1;
let adash2 = (1 + G) * a2;
// calculate new Chroma from scaled a and original b axes
let Cdash1 = Math.sqrt(adash1 ** 2 + b1 ** 2);
let Cdash2 = Math.sqrt(adash2 ** 2 + b2 ** 2);
// calculate new hues, with zero hue for true neutrals
// and in degrees, not radians
let h1 = (adash1 === 0 && b1 === 0)? 0: Math.atan2(b1, adash1);
let h2 = (adash2 === 0 && b2 === 0)? 0: Math.atan2(b2, adash2);
if (h1 < 0) {
h1 += 2 * π;
}
if (h2 < 0) {
h2 += 2 * π;
}
h1 *= r2d;
h2 *= r2d;
// Lightness and Chroma differences; sign matters
let ΔL = L2 - L1;
let ΔC = Cdash2 - Cdash1;
// Hue difference, getting the sign correct
let hdiff = h2 - h1;
let hsum = h1 + h2;
let habs = Math.abs(hdiff);
let Δh;
if (Cdash1 * Cdash2 === 0) {
Δh = 0;
}
else if (habs <= 180) {
Δh = hdiff;
}
else if (hdiff > 180) {
Δh = hdiff - 360;
}
else if (hdiff < -180) {
Δh = hdiff + 360;
}
else {
console.log("the unthinkable has happened");
}
// weighted Hue difference, more for larger Chroma
let ΔH = 2 * Math.sqrt(Cdash2 * Cdash1) * Math.sin(Δh * d2r / 2);
// calculate mean Lightness and Chroma
let Ldash = (L1 + L2)/2;
let Cdash = (Cdash1 + Cdash2)/2;
let Cdash7 = Math.pow(Cdash, 7);
// Compensate for non-linearity in the blue region of Lab.
// Four possibilities for hue weighting factor,
// depending on the angles, to get the correct sign
let hdash;
if (Cdash1 * Cdash2 === 0) {
hdash = hsum; // which should be zero
}
else if (habs <= 180) {
hdash = hsum / 2;
}
else if (hsum < 360) {
hdash = (hsum + 360) / 2;
}
else {
hdash = (hsum - 360) / 2;
}
// positional corrections to the lack of uniformity of CIELAB
// These are all trying to make JND ellipsoids more like spheres
// SL Lightness crispening factor
// a background with L=50 is assumed
let lsq = (Ldash - 50) ** 2;
let SL = 1 + ((0.015 * lsq) / Math.sqrt(20 + lsq));
// SC Chroma factor, similar to those in CMC and deltaE 94 formulae
let SC = 1 + 0.045 * Cdash;
// Cross term T for blue non-linearity
let T = 1;
T -= (0.17 * Math.cos(( hdash - 30) * d2r));
T += (0.24 * Math.cos( 2 * hdash * d2r));
T += (0.32 * Math.cos(((3 * hdash) + 6) * d2r));
T -= (0.20 * Math.cos(((4 * hdash) - 63) * d2r));
// SH Hue factor depends on Chroma,
// as well as adjusted hue angle like deltaE94.
let SH = 1 + 0.015 * Cdash * T;
// RT Hue rotation term compensates for rotation of JND ellipses
// and Munsell constant hue lines
// in the medium-high Chroma blue region
// (Hue 225 to 315)
let Δθ = 30 * Math.exp(-1 * (((hdash - 275)/25) ** 2));
let RC = 2 * Math.sqrt(Cdash7/(Cdash7 + Gfactor));
let RT = -1 * Math.sin(2 * Δθ * d2r) * RC;
// Finally calculate the deltaE, term by term as root sume of squares
let dE = (ΔL / (kL * SL)) ** 2;
dE += (ΔC / (kC * SC)) ** 2;
dE += (ΔH / (kH * SH)) ** 2;
dE += RT * (ΔC / (kC * SC)) * (ΔH / (kH * SH));
return Math.sqrt(dE);
// Yay!!!
};