UNPKG

spectral.js

Version:

Spectral.js is a powerful and versatile JavaScript library that allows you to achieve realistic color mixing in your web-based projects. It is based on the Kubelka-Munk theory, a proven scientific model that simulates how light interacts with paint to pro

821 lines (728 loc) 31.8 kB
// MIT License // // Copyright (c) 2025 Ronald van Wijnen // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : ((global = global || self), factory((global.spectral = {}))); })(this, function (exports) { ('use strict'); const SIZE = 38; const GAMMA = 2.4; /** * Class representing a color and its conversions between various color spaces. * * @class */ class Color { /** * Create a Color instance. * * The constructor accepts either: * - A single string value (interpreted as a CSS color: hex or rgb). * - A single array: * - If its length equals SIZE, it is assumed to be the R values. * - Otherwise, it is assumed to be an sRGB array. * * @constructor * @param {...(string|number[])} args - A single color string or an array of numbers. */ constructor(...args) { if (args.length === 1) { if (typeof args[0] === 'string') { this.sRGB = parse(args[0]).slice(0, 3); this.lRGB = sRGB_to_lRGB(this.sRGB); this.R = lRGB_to_R(this.lRGB); this.XYZ = R_to_XYZ(this.R); } if (Array.isArray(args[0])) { if (args[0].length === SIZE) { this.R = args[0]; this.XYZ = R_to_XYZ(this.R); this.lRGB = XYZ_to_lRGB(this.XYZ); this.sRGB = lRGB_to_sRGB(this.lRGB); } else { this.sRGB = args[0]; this.lRGB = sRGB_to_lRGB(this.sRGB); this.R = lRGB_to_R(this.lRGB); this.XYZ = R_to_XYZ(this.R); } } } } /** * Gets the OKLab color space representation. * * @type {number[]} * @readonly */ get OKLab() { return (this._OKLab ??= XYZ_to_OKLab(this.XYZ)); } /** * Gets the OKLCh color space representation. * * @type {number[]} * @readonly */ get OKLCh() { return (this._OKLCh ??= OKLab_to_OKLCh(this.OKLab)); } /** * Gets the array of KS values computed from R. * * @type {number[]} * @readonly */ get KS() { return (this._KS ??= this.R.map((r) => KS(r))); } /** * Gets the luminance value. * * The value is at least Number.EPSILON. * * @type {number} * @readonly */ get luminance() { return (this._luminance ??= Math.max(Number.EPSILON, this.XYZ[1])); } /** * Gets the tinting strength. * * Default value is 1. * * @type {number} */ get tintingStrength() { return (this._tintingStrength ??= 1); } /** * Sets the tinting strength. * * @param {number} ts - The new tinting strength. */ set tintingStrength(ts) { this._tintingStrength = ts; } /** * Determines whether the color is in gamut based on its linear RGB values. * * @param {Object} [options={}] - Options for gamut checking. * @param {number} [options.epsilon=0] - The tolerance for checking. * @return {boolean} True if in gamut; otherwise, false. */ inGamut = ({ epsilon = 0 } = {}) => { return inGamut(this.lRGB, epsilon); }; /** * Maps the color to a valid gamut using a specified method. * * @param {Object} [options={}] - Options for gamut mapping. * @param {string} [options.method='map'] - Method to use ('clip' or 'map'). * @return {Color} A new Color instance that is in gamut. * @throws {TypeError} If the specified method is unknown. */ toGamut = ({ method = 'map' } = {}) => { switch (method.toLowerCase()) { case 'clip': return new Color(this.sRGB.map((x) => utils.clamp(x, 0, 255))); case 'map': return gamutMap(this); default: throw new TypeError(`Unknown method: '${method}'`); } }; /** * Converts the color to a string representation. * * The color is first mapped into the gamut before converting. * * @param {Object} [options={}] - Options for conversion. * @param {string} [options.format='hex'] - Output format. Currently supports 'hex'. * @param {string} [options.method='map'] - Gamut mapping method ('clip' or 'map'). * @return {string} The color as a string. * @throws {TypeError} If the specified method is unknown. * @throws {TypeError} If the specified format is unknown. */ toString = ({ format = 'hex', method = 'map' } = {}) => { let sRGB; if (!this.inGamut()) { switch (method.toLowerCase()) { case 'clip': sRGB = this.sRGB.map((x) => utils.clamp(x, 0, 255)); break; case 'map': sRGB = gamutMap(this).sRGB; break; default: throw new TypeError(`Unknown method: '${method}'`); } } else { sRGB = this.sRGB; } switch (format.toLowerCase()) { case 'hex': return `#${sRGB .map((x) => x.toString(16).padStart(2, '0')) .join('') .toUpperCase()}`; case 'rgb': return `rgb(${sRGB.join(', ')})`; default: throw new TypeError(`Unknown format: '${format}'`); } }; } /** * Checks if all values in the provided linear RGB array are within gamut. * * @param {number[]} lRGB - Array of linear RGB values. * @param {Object} [options={}] - Options for the check. * @param {number} [options.epsilon=0] - Tolerance value. * @return {boolean} True if all values are within the range [-epsilon, 1+epsilon]. */ const inGamut = (lRGB, { epsilon = 0 } = {}) => { return lRGB.every((x) => x >= -epsilon && x <= 1 + epsilon); }; /** * Computes the Delta E (Euclidean distance) between two OKLab colors. * * @param {number[]} OKLab1 - First OKLab color representation. * @param {number[]} OKLab2 - Second OKLab color representation. * @return {number} The distance between the two colors. */ const deltaEOK = (OKLab1, OKLab2) => { let [L1, a1, b1] = OKLab1; let [L2, a2, b2] = OKLab2; return ((L1 - L2) ** 2 + (a1 - a2) ** 2 + (b1 - b2) ** 2) ** 0.5; }; /** * Maps a color into a valid gamut using a binary search over chroma values. * * @param {Color} color - The Color instance to be gamut-mapped. * @param {Object} [options={}] - Options for gamut mapping. * @param {number} [options.jnd=0.03] - Just-noticeable difference threshold. * @param {number} [options.e=0.0001] - Epsilon for binary search termination. * @return {Color} A new Color instance within gamut. */ const gamutMap = (color, { jnd = 0.03, e = 0.0001 } = {}) => { let L = color.OKLCh[0]; if (L >= 1) { return new Color([255, 255, 255]); } if (L <= 0) { return new Color([0, 0, 0]); } if (inGamut(color.lRGB)) return color; let h = color.OKLCh[2]; let min = 0; let max = color.OKLCh[1]; let min_inGamut = true; let current = color.lRGB; let clipped = lRGB_to_OKLab(current.map((x) => utils.clamp(x))); let E = deltaEOK(clipped, lRGB_to_OKLab(current)); if (E < jnd) { return new Color(lRGB_to_sRGB(XYZ_to_lRGB(OKLab_to_XYZ(clipped)))); } while (max - min > e) { const chroma = (min + max) / 2; let OKLab = OKLCh_to_OKLab([L, chroma, h]); let XYZ = OKLab_to_XYZ(OKLab); current = XYZ_to_lRGB(XYZ); if (min_inGamut && inGamut(current)) { min = chroma; } else { clipped = lRGB_to_OKLab(current.map((x) => utils.clamp(x))); E = deltaEOK(clipped, OKLab); if (E < jnd) { if (jnd - E < e) { break; } else { min_inGamut = false; min = chroma; } } else { max = chroma; } } } return new Color(lRGB_to_sRGB(XYZ_to_lRGB(OKLab_to_XYZ(clipped)))); }; /** * Computes the Kubelka–Munk absorption/scattering parameter KS for a given spectral reflectance R. * * In Kubelka–Munk theory, the KS function reflects the ratio that controls the conversion from spectral * reflectance to an equivalent absorption/scattering coefficient. The formulation * <code>(1 - R)² / (2 * R)</code> is a common approximation that assumes a diffusely scattering medium. * * @param {number} R - The spectral reflectance value. * @return {number} The computed KS value. */ const KS = (R) => { return (1 - R) ** 2 / (2 * R); }; /** * Computes the Kubelka–Munk mixing coefficient KM from a given KS value. * * The KM function transforms the KS parameter into a measure that can be linearly mixed. * This conversion is essential because the Kubelka–Munk model assumes that when pigments are * mixed, the resulting reflectance is a function of the weighted combination of the pigment * absorption and scattering properties. The formula used here: * * <pre> * KM(KS) = 1 + KS - √(KS² + 2KS) * </pre> * * provides the appropriate transformation for blending multiple pigment spectra. * * @param {number} KS - The KS value (absorption/scattering parameter). * @return {number} The computed KM mixing coefficient. */ const KM = (KS) => { return 1 + KS - (KS ** 2 + 2 * KS) ** 0.5; }; /** * Mixes multiple colors using a model based on the Kubelka–Munk theory. * * This function implements a mixing algorithm that is inspired by the Kubelka–Munk theory, * which models how light interacts with diffusely scattering and absorbing layers (such as pigments or paints). * The approach is as follows: * * - For each wavelength band (with SIZE samples), compute a weighted average of the KS values. * - Weights are determined by the square of a factor that considers both the square-root of the color's * luminance and its tinting strength multiplied by a user-specified factor. * - The resulting weighted KS average is then converted back using the KM function to obtain the * mixed spectral reflectance. * * In effect, this method blends pigments based on their optical absorption and scattering properties, * providing a physically motivated approximation for pigment mixing as described by Kubelka–Munk. * * Each argument should be provided as an array of two elements: [Color, factor]. The factor determines * the influence of that particular color in the overall mix. * * @param {...[Color, number]} colors - Colors and their associated mixing factors. * @return {Color} The resulting mixed Color. * * @example * // Mix two Color instances, with a heavier weight given to the first color: * const mixedColor = mix([color1, 2], [color2, 1]); */ const mix = (...colors) => { let R = new Array(SIZE); for (let i = 0; i < SIZE; i++) { let ksMix = 0, totalConcentration = 0; for (let [color, factor] of colors) { let concentration = factor ** 2 * color.tintingStrength ** 2 * color.luminance; totalConcentration += concentration; ksMix += color.KS[i] * concentration; } R[i] = KM(ksMix / totalConcentration); } return new Color(R); }; /** * Generates a palette of colors transitioning between two colors. * * @param {Color} a - The starting Color. * @param {Color} b - The ending Color. * @param {number} size - The number of colors in the palette. * @return {Color[]} An array of Color objects forming the palette. */ const palette = (a, b, size) => { let p = new Array(size); for (let i = 0; i < size; i++) { p[i] = mix([a, size - 1 - i], [b, i]); } return p; }; /** * Interpolates between multiple colors based on a parameter t. * * Each additional argument should be an array with two elements: [Color, position]. * * @param {number} t - Interpolation parameter between 0 and 1. * @param {...[Color, number]} colors - Colors with their positions in the gradient. * @return {Color} The interpolated Color. */ const gradient = (t, ...colors) => { let a = null, b = null; for (const [color, pos] of colors) { if (pos <= t && (!a || pos > a[1])) a = [color, pos]; if (pos >= t && (!b || pos < b[1])) b = [color, pos]; } if (!a) return b[0]; if (!b) return a[0]; if (a[1] === b[1]) return a[0]; const factor = (t - a[1]) / (b[1] - a[1]); return mix([a[0], 1 - factor], [b[0], factor]); }; /** * Applies the inverse companding (linearization) to a value. * * @param {number} x - The sRGB value in the range [0, 1]. * @return {number} The uncompanded (linear) value. */ const uncompand = (x) => { return x > 0.04045 ? ((x + 0.055) / 1.055) ** GAMMA : x / 12.92; }; /** * Applies the companding function to a value. * * @param {number} x - The linear value. * @return {number} The companded sRGB value in the range [0, 1]. */ const compand = (x) => { return x > 0.0031308 ? 1.055 * x ** (1.0 / GAMMA) - 0.055 : x * 12.92; }; /** * Converts sRGB values (in [0,255]) to linear RGB values. * * @param {number[]} sRGB - Array of sRGB component values. * @return {number[]} The corresponding linear RGB values. */ const sRGB_to_lRGB = (sRGB) => { return sRGB.map((x) => uncompand(x / 255)); }; /** * Converts linear RGB values to sRGB values (in [0,255]). * * @param {number[]} lRGB - Array of linear RGB values. * @return {number[]} The corresponding sRGB values rounded to the nearest integer. */ const lRGB_to_sRGB = (lRGB) => { return lRGB.map((x) => Math.round(compand(x) * 255)); }; /** * Converts XYZ color space values to linear RGB. * * @param {number[]} XYZ - XYZ representation. * @return {number[]} The corresponding linear RGB values. */ const XYZ_to_lRGB = (XYZ) => { return utils.mulMatVec(CONVERSION.XYZ_RGB, XYZ); }; /** * Converts linear RGB values to XYZ color space. * * @param {number[]} lRGB - Linear RGB values. * @return {number[]} The corresponding XYZ values. */ const lRGB_to_XYZ = (lRGB) => { return utils.mulMatVec(CONVERSION.RGB_XYZ, lRGB); }; /** * Converts linear RGB values directly to OKLab by first converting to XYZ. * * @param {number[]} lRGB - Linear RGB values. * @return {number[]} The corresponding OKLab values. */ const lRGB_to_OKLab = (lRGB) => { return XYZ_to_OKLab(lRGB_to_XYZ(lRGB)); }; /** * Converts XYZ values to OKLab color space. * * @param {number[]} XYZ - XYZ representation. * @return {number[]} The resulting OKLab values. */ const XYZ_to_OKLab = (XYZ) => { let lms = utils.mulMatVec(CONVERSION.XYZ_LMS, XYZ).map((x) => Math.cbrt(x)); return utils.mulMatVec(CONVERSION.LMS_LAB, lms); }; /** * Converts OKLab values to XYZ color space. * * @param {number[]} OKLab - OKLab representation. * @return {number[]} The resulting XYZ values. */ const OKLab_to_XYZ = (OKLab) => { let lms = utils.mulMatVec(CONVERSION.LAB_LMS, OKLab).map((x) => x ** 3); return utils.mulMatVec(CONVERSION.LMS_XYZ, lms); }; /** * Converts OKLab values to OKLCh color space. * * @param {number[]} OKLab - OKLab representation. * @return {number[]} An array [L, C, H] representing the OKLCh color. */ const OKLab_to_OKLCh = (OKLab) => { let [L, a, b] = OKLab; const C = (a * a + b * b) ** 0.5; const h = (Math.atan2(b, a) * 180) / Math.PI; return [L, C, h >= 0 ? h : h + 360]; }; /** * Converts OKLCh values to OKLab color space. * * @param {number[]} OKLCh - An array [L, C, H] representing the OKLCh color. * @return {number[]} The resulting OKLab values. */ const OKLCh_to_OKLab = (OKLCh) => { let [L, C, h] = OKLCh; let a = C * Math.cos((h * Math.PI) / 180); let b = C * Math.sin((h * Math.PI) / 180); return [L, a, b]; }; /** * Converts spectral reflectance values to XYZ using the CIE color matching functions. * * @param {number[]} R - Array of spectral reflectance values. * @return {number[]} The resulting XYZ values. */ const R_to_XYZ = (R) => { return utils.mulMatVec(CIE.CMF, R); }; /** * Converts linear RGB values to spectral reflectance values. * * This function uses pre-calculated reflectances. * * @param {number[]} lRGB - Linear RGB values. * @return {number[]} The resulting spectral reflectance values. */ const lRGB_to_R = (lRGB) => { let w = Math.min(...lRGB); lRGB = [lRGB[0] - w, lRGB[1] - w, lRGB[2] - w]; let c = Math.min(lRGB[1], lRGB[2]); let m = Math.min(lRGB[0], lRGB[2]); let y = Math.min(lRGB[0], lRGB[1]); let r = Math.max(0, Math.min(lRGB[0] - lRGB[2], lRGB[0] - lRGB[1])); let g = Math.max(0, Math.min(lRGB[1] - lRGB[2], lRGB[1] - lRGB[0])); let b = Math.max(0, Math.min(lRGB[2] - lRGB[1], lRGB[2] - lRGB[0])); const R = new Array(SIZE); for (let i = 0; i < SIZE; i++) { R[i] = Math.max( Number.EPSILON, w * BASE_SPECTRA.W[i] + c * BASE_SPECTRA.C[i] + m * BASE_SPECTRA.M[i] + y * BASE_SPECTRA.Y[i] + r * BASE_SPECTRA.R[i] + g * BASE_SPECTRA.G[i] + b * BASE_SPECTRA.B[i] ); } return R; }; /** * A collection of utility functions for mathematical operations. * * @namespace */ const utils = { /** * Linear interpolation between two values. * * @param {number} a - Start value. * @param {number} b - End value. * @param {number} t - Interpolation factor in [0,1]. * @return {number} The interpolated value. */ lerp: (a, b, t) => a + (b - a) * t, /** * Clamps a value between a minimum and maximum. * * @param {number} x - The value to clamp. * @param {number} [min=0] - Minimum allowed value. * @param {number} [max=1] - Maximum allowed value. * @return {number} The clamped value. */ clamp: (x, min = 0, max = 1) => Math.min(Math.max(x, min), max), /** * Calculates the dot product between two arrays of numbers. * * @param {number[]} a - First vector. * @param {number[]} b - Second vector. * @return {number} The dot product. */ dot: (a, b) => a.reduce((acc, val, i) => acc + val * b[i], 0), /** * Multiplies a matrix with a vector. * * @param {number[][]} m - The matrix. * @param {number[]} v - The vector. * @return {number[]} The resulting vector. */ mulMatVec: (m, v) => m.map((row) => utils.dot(row, v)), }; /** * Parses a CSS color string (hex or rgb) and returns an array of components. * * For hex strings, returns an array in the format: [R, G, B, A] (with A defaulting to 1 if omitted). * For rgb strings, converts percentages to values in [0, 255] if necessary. * * @param {string} str - The CSS color string to parse. * @return {(number[]|number)} An array of color components or NaN if unrecognized. */ const parse = (str) => { if (str[0] === '#') { str = str.length === 4 ? str.replace(/./g, (m) => m + m).slice(1) : str.slice(1); return [ parseInt(str.substring(0, 2), 16), parseInt(str.substring(2, 4), 16), parseInt(str.substring(4, 6), 16), str.length === 8 ? parseInt(str.substring(6, 8), 16) / 255 : 1, ]; } else if (str.startsWith('rgb')) { return str .slice(str.indexOf('(') + 1, -1) .split(',') .map((v, i) => (i < 3 && v.includes('%') ? Math.round(parseFloat(v) * 2.55) : parseFloat(v))); } return NaN; }; /** * Spectra data. * * Contains arrays for various spectra (White, Cyan, Magenta, Yellow, Red, Green, Blue). * * @constant {object} * @readonly */ const BASE_SPECTRA = Object.freeze({ W: [ 1.00116072718764, 1.00116065159728, 1.00116031922747, 1.00115867270789, 1.00115259844552, 1.00113252528998, 1.00108500663327, 1.00099687889453, 1.00086525152274, 1.0006962900094, 1.00050496114888, 1.00030808187992, 1.00011966602013, 0.999952765968407, 0.999821836899297, 0.999738609557593, 0.999709551639612, 0.999731930210627, 0.999799436346195, 0.999900330316671, 1.00002040652611, 1.00014478793658, 1.00025997903412, 1.00035579697089, 1.00042753780269, 1.00047623344888, 1.00050720967508, 1.00052519156373, 1.00053509606896, 1.00054022097482, 1.00054272816784, 1.00054389569087, 1.00054448212151, 1.00054476959992, 1.00054489887762, 1.00054496254689, 1.00054498927058, 1.000544996993, ], C: [ 0.970585001322962, 0.970592498143425, 0.970625348729891, 0.970786806119017, 0.971368673228248, 0.973163230621252, 0.976740223158765, 0.981587605491377, 0.986280265652949, 0.989949147689134, 0.99249270153842, 0.994145680405256, 0.995183975033212, 0.995756750110818, 0.99591281828671, 0.995606157834528, 0.994597600961854, 0.99221571549237, 0.986236452783249, 0.967943337264541, 0.891285004244943, 0.536202477862053, 0.154108119001878, 0.0574575093228929, 0.0315349873107007, 0.0222633920086335, 0.0182022841492439, 0.016299055973264, 0.0153656239334613, 0.0149111568733976, 0.0146954339898235, 0.0145964146717719, 0.0145470156699655, 0.0145228771899495, 0.0145120341118965, 0.0145066940939832, 0.0145044507314479, 0.0145038009464639, ], M: [ 0.990673557319988, 0.990671524961979, 0.990662582353421, 0.990618107644795, 0.99045148087871, 0.989871081400204, 0.98828660875964, 0.984290692797504, 0.973934905625306, 0.941817838460145, 0.817390326195156, 0.432472805065729, 0.13845397825887, 0.0537347216940033, 0.0292174996673231, 0.021313651750859, 0.0201349530181136, 0.0241323096280662, 0.0372236145223627, 0.0760506552706601, 0.205375471942399, 0.541268903460439, 0.815841685086486, 0.912817704123976, 0.946339830166962, 0.959927696331991, 0.966260595230312, 0.969325970058424, 0.970854536721399, 0.971605066528128, 0.971962769757392, 0.972127272274509, 0.972209417745812, 0.972249577678424, 0.972267621998742, 0.97227650946215, 0.972280243306874, 0.97228132482656, ], Y: [ 0.0210523371789306, 0.0210564627517414, 0.0210746178695038, 0.0211649058448753, 0.0215027957272504, 0.0226738799041561, 0.0258235649693629, 0.0334879385639851, 0.0519069663740307, 0.100749014833473, 0.239129899706847, 0.534804312272748, 0.79780757864303, 0.911449894067384, 0.953797963004507, 0.971241615465429, 0.979303123807588, 0.983380119507575, 0.985461246567755, 0.986435046976605, 0.986738250670141, 0.986617882445032, 0.986277776758643, 0.985860592444056, 0.98547492767621, 0.985176934765558, 0.984971574014181, 0.984846303415712, 0.984775351811199, 0.984738066625265, 0.984719648311765, 0.984711023391939, 0.984706683300676, 0.984704554393091, 0.98470359630937, 0.984703124077552, 0.98470292561509, 0.984702868122795, ], R: [ 0.0315605737777207, 0.0315520718330149, 0.0315148215513658, 0.0313318044982702, 0.0306729857725527, 0.0286480476989607, 0.0246450407045709, 0.0192960753663651, 0.0142066612220556, 0.0102942608878609, 0.0076191460521811, 0.005898041083542, 0.0048233247781713, 0.0042298748350633, 0.0040599171299341, 0.0043533695594676, 0.0053434425970201, 0.0076917201010463, 0.0135969795736536, 0.0316975442661115, 0.107861196355249, 0.463812603168704, 0.847055405272011, 0.943185409393918, 0.968862150696558, 0.978030667473603, 0.982043643854306, 0.983923623718707, 0.984845484154382, 0.985294275814596, 0.985507295219825, 0.985605071539837, 0.985653849933578, 0.985677685033883, 0.985688391806122, 0.985693664690031, 0.985695879848205, 0.985696521463762, ], G: [ 0.0095560747554212, 0.0095581580120851, 0.0095673245444588, 0.0096129126297349, 0.0097837090401843, 0.010378622705871, 0.0120026452378567, 0.0160977721473922, 0.026706190223168, 0.0595555440185881, 0.186039826532826, 0.570579820116159, 0.861467768400292, 0.945879089767658, 0.970465486474305, 0.97841363028445, 0.979589031411224, 0.975533536908632, 0.962288755397813, 0.92312157451312, 0.793434018943111, 0.459270135902429, 0.185574103666303, 0.0881774959955372, 0.05436302287667, 0.0406288447060719, 0.034221520431697, 0.0311185790956966, 0.0295708898336134, 0.0288108739348928, 0.0284486271324597, 0.0282820301724731, 0.0281988376490237, 0.0281581655342037, 0.0281398910216386, 0.0281308901665811, 0.0281271086805816, 0.0281260133612096, ], B: [ 0.979404752502014, 0.97940070684313, 0.979382903470261, 0.979294364945594, 0.97896301460857, 0.977814466694043, 0.974724321133836, 0.967198482343973, 0.949079657530575, 0.900850128940977, 0.76315044546224, 0.465922171649319, 0.201263280451005, 0.0877524413419623, 0.0457176793291679, 0.0284706050521843, 0.020527176756985, 0.0165302792310211, 0.0145135107212858, 0.0136003508637687, 0.0133604258769571, 0.013548894314568, 0.0139594356366992, 0.014443425575357, 0.0148854440621406, 0.0152254296999746, 0.0154592848180209, 0.0156018026485961, 0.0156824871281936, 0.0157248764360615, 0.0157458108784121, 0.0157556123350225, 0.0157605443964911, 0.0157629637515278, 0.0157640525629106, 0.015764589232951, 0.0157648147772649, 0.0157648801149616, ], }); /** * CIE Color Matching Functions weighted by D65 Standard Illuminant * * @constant {object} * @readonly */ const CIE = Object.freeze({ CMF: [ [ 0.0000646919989576, 0.0002194098998132, 0.0011205743509343, 0.0037666134117111, 0.011880553603799, 0.0232864424191771, 0.0345594181969747, 0.0372237901162006, 0.0324183761091486, 0.021233205609381, 0.0104909907685421, 0.0032958375797931, 0.0005070351633801, 0.0009486742057141, 0.0062737180998318, 0.0168646241897775, 0.028689649025981, 0.0426748124691731, 0.0562547481311377, 0.0694703972677158, 0.0830531516998291, 0.0861260963002257, 0.0904661376847769, 0.0850038650591277, 0.0709066691074488, 0.0506288916373645, 0.035473961885264, 0.0214682102597065, 0.0125164567619117, 0.0068045816390165, 0.0034645657946526, 0.0014976097506959, 0.000769700480928, 0.0004073680581315, 0.0001690104031614, 0.0000952245150365, 0.0000490309872958, 0.0000199961492222, ], [ 0.000001844289444, 0.0000062053235865, 0.0000310096046799, 0.0001047483849269, 0.0003536405299538, 0.0009514714056444, 0.0022822631748318, 0.004207329043473, 0.0066887983719014, 0.0098883960193565, 0.0152494514496311, 0.0214183109449723, 0.0334229301575068, 0.0513100134918512, 0.070402083939949, 0.0878387072603517, 0.0942490536184085, 0.0979566702718931, 0.0941521856862608, 0.0867810237486753, 0.0788565338632013, 0.0635267026203555, 0.05374141675682, 0.042646064357412, 0.0316173492792708, 0.020885205921391, 0.0138601101360152, 0.0081026402038399, 0.004630102258803, 0.0024913800051319, 0.0012593033677378, 0.000541646522168, 0.0002779528920067, 0.0001471080673854, 0.0000610327472927, 0.0000343873229523, 0.0000177059860053, 0.000007220974913, ], [ 0.000305017147638, 0.0010368066663574, 0.0053131363323992, 0.0179543925899536, 0.0570775815345485, 0.113651618936287, 0.17335872618355, 0.196206575558657, 0.186082370706296, 0.139950475383207, 0.0891745294268649, 0.0478962113517075, 0.0281456253957952, 0.0161376622950514, 0.0077591019215214, 0.0042961483736618, 0.0020055092122156, 0.0008614711098802, 0.0003690387177652, 0.0001914287288574, 0.0001495555858975, 0.0000923109285104, 0.0000681349182337, 0.0000288263655696, 0.0000157671820553, 0.0000039406041027, 0.000001584012587, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], ], }); /** * Conversion matrices and constants for various color space transformations. * * @constant {object} * @readonly * @see {@link https://github.com/w3c/csswg-drafts/issues/5922} * @see {@link https://github.com/color-js/color.js/blob/main/src/spaces/srgb-linear.js} * @see {@link https://github.com/color-js/color.js/blob/main/src/spaces/oklab.js} */ const CONVERSION = Object.freeze({ //sRGB <-> XYZ conversion matrices RGB_XYZ: [ [0.41239079926595934, 0.357584339383878, 0.1804807884018343], [0.21263900587151027, 0.715168678767756, 0.07219231536073371], [0.01933081871559182, 0.11919477979462598, 0.9505321522496607], ], XYZ_RGB: [ [3.2409699419045226, -1.537383177570094, -0.4986107602930034], [-0.9692436362808796, 1.8759675015077202, 0.04155505740717559], [0.05563007969699366, -0.20397695888897652, 1.0569715142428786], ], // OKLab conversion matrices XYZ_LMS: [ [0.819022437996703, 0.3619062600528904, -0.1288737815209879], [0.0329836539323885, 0.9292868615863434, 0.0361446663506424], [0.0481771893596242, 0.2642395317527308, 0.6335478284694309], ], LMS_XYZ: [ [1.2268798758459243, -0.5578149944602171, 0.2813910456659647], [-0.0405757452148008, 1.112286803280317, -0.0717110580655164], [-0.0763729366746601, -0.4214933324022432, 1.5869240198367816], ], LMS_LAB: [ [0.210454268309314, 0.7936177747023054, -0.0040720430116193], [1.9779985324311684, -2.4285922420485799, 0.450593709617411], [0.0259040424655478, 0.7827717124575296, -0.8086757549230774], ], LAB_LMS: [ [1.0, 0.3963377773761749, 0.2158037573099136], [1.0, -0.1055613458156586, -0.0638541728258133], [1.0, -0.0894841775298119, -1.2914855480194092], ], }); exports.Color = Color; exports.mix = mix; exports.palette = palette; exports.gradient = gradient; });