UNPKG

colorjs.io

Version:

Let’s get serious about color

166 lines (137 loc) 4.58 kB
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!!! };