UNPKG

@texel/color

Version:

a minimal and modern color library

386 lines (316 loc) 9.54 kB
import { vec3, constrainAngle as constrain } from "./util.js"; import { OKLab_to } from "./core.js"; import { sRGBGamut } from "./spaces.js"; import { findCuspOKLCH, findGamutIntersectionOKLCH, getGamutLMStoRGB, } from "./gamut.js"; const K1 = 0.206; const K2 = 0.03; const K3 = (1.0 + K1) / (1.0 + K2); const tmp2A = [0, 0]; const tmp2B = [0, 0]; const tmp3A = vec3(); const tmp2Cusp = [0, 0]; const tau = 2 * Math.PI; const copySign = (to, from) => (Math.sign(to) === Math.sign(from) ? to : -to); const spow = (base, exp) => copySign(Math.abs(base) ** exp, base); const toe = (x) => 0.5 * (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x)); const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2)); const computeSt = (cusp, out) => { // To ST. let l = cusp[0]; let c = cusp[1]; out[0] = c / l; out[1] = c / (1 - l); }; const toScaleL = (lv, cv, a_, b_, lmsToRgb) => { let lvt = toeInv(lv); let cvt = (cv * lvt) / lv; // RGB scale tmp3A[0] = lvt; tmp3A[1] = a_ * cvt; tmp3A[2] = b_ * cvt; let ret = OKLab_to(tmp3A, lmsToRgb, tmp3A); return spow( 1.0 / Math.max(Math.max(ret[0], ret[1]), Math.max(ret[2], 0.0)), 1 / 3 ); }; const computeStMid = (a, b, out) => { // Returns a smooth approximation of the location of the cusp. // // This polynomial was created by an optimization process. // It has been designed so that S_mid < S_max and T_mid < T_max. let s = 0.11516993 + 1.0 / (7.4477897 + 4.1590124 * b + a * (-2.19557347 + 1.75198401 * b + a * (-2.13704948 - 10.02301043 * b + a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a)))); let t = 0.11239642 + 1.0 / (1.6132032 - 0.68124379 * b + a * (0.40370612 + 0.90148123 * b + a * (-0.27087943 + 0.6122399 * b + a * (0.00299215 - 0.45399568 * b - 0.14661872 * a)))); out[0] = s; out[1] = t; }; const getCs = (l, a, b, cusp, gamut) => { // Get Cs let cMax = findGamutIntersectionOKLCH(a, b, l, 1, l, cusp, gamut); let stMax = tmp2A; computeSt(cusp, stMax); // Scale factor to compensate for the curved part of gamut shape: let k = cMax / Math.min(l * stMax[0], (1 - l) * stMax[1]); const stMid = tmp2B; computeStMid(a, b, stMid); // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. let ca = l * stMid[0]; let cb = (1.0 - l) * stMid[1]; let cMid = 0.9 * k * Math.sqrt(Math.sqrt(1.0 / (1.0 / ca ** 4 + 1.0 / cb ** 4))); // For `C_0`, the shape is independent of hue, so `ST` are constant. // Values picked to roughly be the average values of `ST`. ca = l * 0.4; cb = (1.0 - l) * 0.8; // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. let c0 = Math.sqrt(1.0 / (1.0 / ca ** 2 + 1.0 / cb ** 2)); return [c0, cMid, cMax]; }; /** * Converts OKHSL color to OKLab color. * * @method * @category oklab * @param {Vector} hsl - The OKHSL color as an array [h, s, l]. * @param {ColorGamut} [gamut=sRGBGamut] - The color gamut. * @param {Vector} [out=vec3()] - The output array to store the OKLab color. * @returns {Vector} The OKLab color as an array [L, a, b]. */ export const OKHSLToOKLab = (hsl, gamut = sRGBGamut, out = vec3()) => { // Convert Okhsl to Oklab. let h = hsl[0], s = hsl[1], l = hsl[2]; let L = toeInv(l); let a = 0; let b = 0; h = constrain(h) / 360.0; if (L !== 0.0 && L !== 1.0 && s !== 0) { let a_ = Math.cos(tau * h); let b_ = Math.sin(tau * h); const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp); let Cs = getCs(L, a_, b_, cusp, gamut); let c0 = Cs[0], cMid = Cs[1], cMax = Cs[2]; // Interpolate the three values for C so that: // ``` // At s=0: dC/ds = C_0, C=0 // At s=0.8: C=C_mid // At s=1.0: C=C_max // ``` let mid = 0.8; let midInv = 1.25; let t, k0, k1, k2; if (s < mid) { t = midInv * s; k0 = 0.0; k1 = mid * c0; k2 = 1.0 - k1 / cMid; } else { t = 5 * (s - 0.8); k0 = cMid; k1 = (0.2 * cMid ** 2 * 1.25 ** 2) / c0; k2 = 1.0 - k1 / (cMax - cMid); } let c = k0 + (t * k1) / (1.0 - k2 * t); a = c * a_; b = c * b_; } out[0] = L; out[1] = a; out[2] = b; return out; }; /** * Converts OKLab color to OKHSL color. * * @method * @category oklab * @param {Vector} lab - The OKLab color as an array [L, a, b]. * @param {ColorGamut} [gamut=sRGBGamut] - The color gamut. * @param {Vector} [out=vec3()] - The output array to store the OKHSL color. * @returns {Vector} The OKHSL color as an array [h, s, l]. */ export const OKLabToOKHSL = (lab, gamut = sRGBGamut, out = vec3()) => { // Oklab to Okhsl. // Epsilon for lightness should approach close to 32 bit lightness // Epsilon for saturation just needs to be sufficiently close when denoting achromatic let εL = 1e-7; let εS = 1e-4; let L = lab[0]; let s = 0.0; let l = toe(L); let c = Math.sqrt(lab[1] ** 2 + lab[2] ** 2); let h = 0.5 + Math.atan2(-lab[2], -lab[1]) / tau; if (l !== 0.0 && l !== 1.0 && c !== 0) { let a_ = lab[1] / c; let b_ = lab[2] / c; const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp); let Cs = getCs(L, a_, b_, cusp, gamut); let c0 = Cs[0], cMid = Cs[1], cMax = Cs[2]; let mid = 0.8; let midInv = 1.25; let k0, k1, k2, t; if (c < cMid) { k1 = mid * c0; k2 = 1.0 - k1 / cMid; t = c / (k1 + k2 * c); s = t * mid; } else { k0 = cMid; k1 = (0.2 * cMid ** 2 * midInv ** 2) / c0; k2 = 1.0 - k1 / (cMax - cMid); t = (c - k0) / (k1 + k2 * (c - k0)); s = mid + 0.2 * t; } } const achromatic = Math.abs(s) < εS; if (achromatic || l === 0.0 || Math.abs(1 - l) < εL) { // Due to floating point imprecision near lightness of 1, we can end up // with really high around white, this is to provide consistency as // saturation can be really high for white due this imprecision. if (!achromatic) { s = 0.0; } } h = constrain(h * 360); out[0] = h; out[1] = s; out[2] = l; return out; }; /** * Converts OKHSV color to OKLab color. * * @method * @category oklab * @param {Vector} hsv - The OKHSV color as an array [h, s, v]. * @param {ColorGamut} [gamut=sRGBGamut] - The color gamut. * @param {Vector} [out=vec3()] - The output array to store the OKLab color. * @returns {Vector} The OKLab color as an array [L, a, b]. */ export const OKHSVToOKLab = (hsv, gamut = sRGBGamut, out = vec3()) => { // Convert from Okhsv to Oklab.""" let h = hsv[0], s = hsv[1], v = hsv[2]; h = constrain(h) / 360.0; let l = toeInv(v); let a = 0; let b = 0; // Avoid processing gray or colors with undefined hues if (l !== 0.0 && s !== 0.0) { let a_ = Math.cos(tau * h); let b_ = Math.sin(tau * h); const lmsToRgb = getGamutLMStoRGB(gamut); const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp); computeSt(cusp, tmp2A); const sMax = tmp2A[0]; const tMax = tmp2A[1]; let s0 = 0.5; let k = 1 - s0 / sMax; // first we compute L and V as if the gamut is a perfect triangle: // L, C when v==1: let lv = 1 - (s * s0) / (s0 + tMax - tMax * k * s); let cv = (s * tMax * s0) / (s0 + tMax - tMax * k * s); l = v * lv; let c = v * cv; // then we compensate for both toe and the curved top part of the triangle: const scaleL = toScaleL(lv, cv, a_, b_, lmsToRgb); let lNew = toeInv(l); c = (c * lNew) / l; l = lNew; l = l * scaleL; c = c * scaleL; a = c * a_; b = c * b_; } out[0] = l; out[1] = a; out[2] = b; return out; }; /** * Converts OKLab color to OKHSV color. * * @method * @category oklab * @param {Vector} lab The OKLab color as an array [L, a, b]. * @param {ColorGamut} [gamut=sRGBGamut] The color gamut. * @param {Vector} [out=vec3()] The output array to store the OKHSV color. * @returns {Vector} The OKHSV color as an array [h, s, v]. */ export const OKLabToOKHSV = (lab, gamut = sRGBGamut, out = vec3()) => { // Oklab to Okhsv. const lmsToRgb = getGamutLMStoRGB(gamut); // Epsilon for saturation just needs to be sufficiently close when denoting achromatic let ε = 1e-4; let l = lab[0]; let s = 0.0; let v = toe(l); let c = Math.sqrt(lab[1] ** 2 + lab[2] ** 2); let h = 0.5 + Math.atan2(-lab[2], -lab[1]) / tau; if (l !== 0.0 && l !== 1 && c !== 0.0) { let a_ = lab[1] / c; let b_ = lab[2] / c; const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp); computeSt(cusp, tmp2A); const sMax = tmp2A[0]; const tMax = tmp2A[1]; let s0 = 0.5; let k = 1 - s0 / sMax; // first we find `L_v`, `C_v`, `L_vt` and `C_vt` let t = tMax / (c + l * tMax); let lv = t * l; let cv = t * c; const scaleL = toScaleL(lv, cv, a_, b_, lmsToRgb); l = l / scaleL; c = c / scaleL; const toeL = toe(l); c = (c * toeL) / l; l = toeL; // we can now compute v and s: v = l / lv; s = ((s0 + tMax) * cv) / (tMax * s0 + tMax * k * cv); } // unlike colorjs.io, we are not working with none-types // if (Math.abs(s) < ε || v === 0.0) { // h = null; // } h = constrain(h * 360); out[0] = h; out[1] = s; out[2] = v; return out; };