UNPKG

colorjs.io

Version:

Let’s get serious about color

158 lines (134 loc) 4.32 kB
import ColorSpace from "../space.js"; import {constrain} from "../angles.js"; import xyz_d65 from "./xyz-d65.js"; import {fromCam16, toCam16, environment} from "./cam16.js"; import {WHITES} from "../adapt.js"; const white = WHITES.D65; const ε = 216 / 24389; // 6^3/29^3 == (24/116)^3 const κ = 24389 / 27; // 29^3/3^3 function toLstar (y) { // Convert XYZ Y to L* const fy = (y > ε) ? Math.cbrt(y) : (κ * y + 16) / 116; return (116.0 * fy) - 16.0; } function fromLstar (lstar) { // Convert L* back to XYZ Y return (lstar > 8) ? Math.pow((lstar + 16) / 116, 3) : lstar / κ; } function fromHct (coords, env) { // Use Newton's method to try and converge as quick as possible or // converge as close as we can. While the requested precision is achieved // most of the time, it may not always be achievable. Especially past the // visible spectrum, the algorithm will likely struggle to get the same // precision. If, for whatever reason, we cannot achieve the accuracy we // seek in the allotted iterations, just return the closest we were able to // get. let [h, c, t] = coords; let xyz = []; let j = 0; // Shortcut out for black if (t === 0) { return [0.0, 0.0, 0.0]; } // Calculate the Y we need to target let y = fromLstar(t); // A better initial guess yields better results. Polynomials come from // curve fitting the T vs J response. if (t > 0) { j = 0.00379058511492914 * t ** 2 + 0.608983189401032 * t + 0.9155088574762233; } else { j = 9.514440756550361e-06 * t ** 2 + 0.08693057439788597 * t - 21.928975842194614; } // Threshold of how close is close enough, and max number of attempts. // More precision and more attempts means more time spent iterating. Higher // required precision gives more accuracy but also increases the chance of // not hitting the goal. 2e-12 allows us to convert round trip with // reasonable accuracy of six decimal places or more. const threshold = 2e-12; const max_attempts = 15; let attempt = 0; let last = Infinity; let best = j; // Try to find a J such that the returned y matches the returned y of the L* while (attempt <= max_attempts) { xyz = fromCam16({J: j, C: c, h: h}, env); // If we are within range, return XYZ // If we are closer than last time, save the values const delta = Math.abs(xyz[1] - y); if (delta < last) { if (delta <= threshold) { return xyz; } best = j; last = delta; } // f(j_root) = (j ** (1 / 2)) * 0.1 // f(j) = ((f(j_root) * 100) ** 2) / j - 1 = 0 // f(j_root) = Y = y / 100 // f(j) = (y ** 2) / j - 1 // f'(j) = (2 * y) / j j = j - (xyz[1] - y) * j / (2 * xyz[1]); attempt += 1; } // We could not acquire the precision we desired, // return our closest attempt. return fromCam16({J: j, C: c, h: h}, env); } function toHct (xyz, env) { // Calculate HCT by taking the L* of CIE LCh D65 and CAM16 chroma and hue. const t = toLstar(xyz[1]); if (t === 0.0) { return [0.0, 0.0, 0.0]; } const cam16 = toCam16(xyz, viewingConditions); return [constrain(cam16.h), cam16.C, t]; } // Pre-calculate everything we can with the viewing conditions export const viewingConditions = environment( white, 200 / Math.PI * fromLstar(50.0), fromLstar(50.0) * 100, "average", false, ); // https://material.io/blog/science-of-color-design // This is not a port of the material-color-utilities, // but instead implements the full color space as described, // combining CAM16 JCh and Lab D65. This does not clamp conversion // to HCT to specific chroma bands and provides support for wider // gamuts than Google currently supports and does so at a greater // precision (> 8 bits back to sRGB). // This implementation comes from https://github.com/facelessuser/coloraide // which is licensed under MIT. export default new ColorSpace({ id: "hct", name: "HCT", coords: { h: { refRange: [0, 360], type: "angle", name: "Hue", }, c: { refRange: [0, 145], name: "Colorfulness", }, t: { refRange: [0, 100], name: "Tone", }, }, base: xyz_d65, fromBase (xyz) { return toHct(xyz, viewingConditions); }, toBase (hct) { return fromHct(hct, viewingConditions); }, formats: { color: { id: "--hct", coords: ["<number> | <angle>", "<percentage> | <number>", "<percentage> | <number>"], }, }, });