colorjs.io
Version:
Let’s get serious about color
111 lines (88 loc) • 3.21 kB
JavaScript
import lab from "../spaces/lab.js";
import lch from "../spaces/lch.js";
// More accurate color-difference formulae
// than the simple 1976 Euclidean distance in Lab
// CMC by the Color Measurement Committee of the
// Bradford Society of Dyeists and Colorsts, 1994.
// Uses LCH rather than Lab,
// with different weights for L, C and H differences
// A nice increase in accuracy for modest increase in complexity
const π = Math.PI;
const d2r = π / 180;
export default function (color, sample, {l = 2, c = 1} = {}) {
// Given this color as the reference
// and a sample,
// calculate deltaE CMC.
// This implementation assumes the parametric
// weighting factors l:c are 2:1
// which is typical for non-textile uses.
let [L1, a1, b1] = lab.from(color);
let [, C1, H1] = lch.from(lab, [L1, a1, b1]);
let [L2, a2, b2] = lab.from(sample);
let C2 = lch.from(lab, [L2, a2, b2])[1];
// let [L1, a1, b1] = color.getAll(lab);
// let C1 = color.get("lch.c");
// let H1 = color.get("lch.h");
// let [L2, a2, b2] = sample.getAll(lab);
// let C2 = sample.get("lch.c");
// Check for negative Chroma,
// which might happen through
// direct user input of LCH values
if (C1 < 0) {
C1 = 0;
}
if (C2 < 0) {
C2 = 0;
}
// we don't need H2 as ΔH is calculated from Δa, Δb and ΔC
// Lightness and Chroma differences
// These are (color - sample), unlike deltaE2000
let ΔL = L1 - L2;
let ΔC = C1 - C2;
let Δa = a1 - a2;
let Δb = b1 - b2;
// weighted Hue difference, less for larger Chroma difference
let H2 = (Δa ** 2) + (Δb ** 2) - (ΔC ** 2);
// due to roundoff error it is possible that, for zero a and b,
// ΔC > Δa + Δb is 0, resulting in attempting
// to take the square root of a negative number
// trying instead the equation from Industrial Color Physics
// By Georg A. Klein
// let ΔH = ((a1 * b2) - (a2 * b1)) / Math.sqrt(0.5 * ((C2 * C1) + (a2 * a1) + (b2 * b1)));
// console.log({ΔH});
// This gives the same result to 12 decimal places
// except it sometimes NaNs when trying to root a negative number
// let ΔH = Math.sqrt(H2); we never actually use the root, it gets squared again!!
// positional corrections to the lack of uniformity of CIELAB
// These are all trying to make JND ellipsoids more like spheres
// SL Lightness crispening factor, depends entirely on L1 not L2
let SL = 0.511; // linear portion of the Y to L transfer function
if (L1 >= 16) { // cubic portion
SL = (0.040975 * L1) / (1 + 0.01765 * L1);
}
// SC Chroma factor
let SC = ((0.0638 * C1) / (1 + 0.0131 * C1)) + 0.638;
// Cross term T for blue non-linearity
let T;
if ( Number.isNaN(H1)) {
H1 = 0;
}
if (H1 >= 164 && H1 <= 345) {
T = 0.56 + Math.abs(0.2 * Math.cos((H1 + 168) * d2r));
}
else {
T = 0.36 + Math.abs(0.4 * Math.cos((H1 + 35) * d2r));
}
// console.log({T});
// SH Hue factor also depends on C1,
let C4 = Math.pow(C1, 4);
let F = Math.sqrt(C4 / (C4 + 1900));
let SH = SC * ((F * T) + 1 - F);
// Finally calculate the deltaE, term by term as root sume of squares
let dE = (ΔL / (l * SL)) ** 2;
dE += (ΔC / (c * SC)) ** 2;
dE += (H2 / (SH ** 2));
// dE += (ΔH / SH) ** 2;
return Math.sqrt(dE);
// Yay!!!
};