@markgorzynski/color-utils
Version:
The only color science library with adaptive visual perception modeling and accessibility-driven optimization.
241 lines (204 loc) • 7.71 kB
JavaScript
/**
* @module cielab
* @description Functions for CIE L*a*b* and CIE L*C*h_ab conversions and pipelines to/from XYZ and sRGB.
*
* RANGE CONVENTIONS:
* ==================
* Input/Output Ranges:
* - XYZ: X,Y,Z in 0-1 scale (Y=1 for white point)
* - Lab: L in 0-100, a/b typically -128 to +127
* - LCh: L in 0-100, C in 0+, h in 0-360 degrees
* - sRGB: r,g,b in 0-1
*
* Internal Processing:
* - Reference white (D65): X=95.047, Y=100, Z=108.883 (0-100 scale)
* - XYZ inputs are scaled from 0-1 to 0-100 for CIELAB math
* - XYZ outputs are scaled from 0-100 back to 0-1
*
* IMPORTANT:
* - None of these functions perform any clamping
* - Out-of-gamut colors will produce values outside typical ranges
* - Clamping should be applied separately when needed
*/
import {
D65_WHITE_POINT_XYZ,
degreesToRadians,
radiansToDegrees,
normalizeHue,
} from './utils.js';
import {
srgbToXyz,
xyzToSrgb,
srgbToLinearSrgb,
linearSrgbToSrgb,
MATRIX_LINEAR_SRGB_TO_XYZ_D65,
MATRIX_XYZ_TO_LINEAR_SRGB_D65,
} from './srgb.js';
// --- CIELAB Constants ---
const DELTA = 6 / 29;
const DELTA_CUBED = Math.pow(DELTA, 3);
const EPSILON = DELTA_CUBED; // (6/29)^3
const KAPPA = Math.pow(29 / 3, 3); // (29/3)^3
// --- Internal CIELAB Transform Functions (Optimized) ---
/**
* Forward transform for CIELAB conversion (XYZ to Lab).
* @private
*/
function cielabForwardTransform(t) {
return t > DELTA_CUBED
? Math.cbrt(t)
: (t / (3 * DELTA * DELTA)) + (4 / 29);
}
/**
* Inverse transform for CIELAB conversion (Lab to XYZ).
* @private
*/
function cielabInverseTransform(t) {
return t > DELTA
? Math.pow(t, 3)
: (3 * DELTA * DELTA) * (t - (4 / 29));
}
// --- CIELAB ↔ XYZ ---
/**
* Converts CIE XYZ to CIELAB.
* @param {XyzColor} xyzColor - XYZ color with values in 0-1 range.
* @param {Object} [referenceWhite=D65_WHITE_POINT_XYZ] - Reference white point.
* @returns {LabColor} CIELAB color { L, a, b }.
*/
export function xyzToLab(xyzColor, referenceWhite = D65_WHITE_POINT_XYZ) {
// Scale XYZ from 0-1 to 0-100 for standard CIELAB calculations
const x = xyzColor.X * 100;
const y = xyzColor.Y * 100;
const z = xyzColor.Z * 100;
const fx = cielabForwardTransform(x / referenceWhite.X);
const fy = cielabForwardTransform(y / referenceWhite.Y);
const fz = cielabForwardTransform(z / referenceWhite.Z);
const L = 116 * fy - 16;
const a = 500 * (fx - fy);
const b = 200 * (fy - fz);
return { L, a, b };
}
/**
* Converts CIELAB to CIE XYZ.
* @param {LabColor} labColor - CIELAB color { L, a, b }.
* @param {Object} [referenceWhite=D65_WHITE_POINT_XYZ] - Reference white point.
* @returns {XyzColor} XYZ color with values in 0-1 range.
*/
export function labToXyz(labColor, referenceWhite = D65_WHITE_POINT_XYZ) {
const { L, a, b } = labColor;
const fy = (L + 16) / 116;
const fx = a / 500 + fy;
const fz = fy - b / 200;
const x = cielabInverseTransform(fx) * referenceWhite.X;
const y = cielabInverseTransform(fy) * referenceWhite.Y;
const z = cielabInverseTransform(fz) * referenceWhite.Z;
// Scale back to 0-1 range
return { X: x / 100, Y: y / 100, Z: z / 100 };
}
// --- CIELAB ↔ CIELCh ---
/**
* Converts CIELAB to CIELCh (cylindrical representation).
* @param {LabColor} labColor - CIELAB color { L, a, b }.
* @returns {LchColor} CIELCh color { L, C, h }.
*/
export function labToLch(labColor) {
const { L, a, b } = labColor;
const C = Math.sqrt(a * a + b * b);
let h = radiansToDegrees(Math.atan2(b, a));
// Normalize hue to [0, 360)
if (h < 0) h += 360;
return { L, C, h };
}
/**
* Converts CIELCh to CIELAB.
* @param {LchColor} lchColor - CIELCh color { L, C, h }.
* @returns {LabColor} CIELAB color { L, a, b }.
*/
export function lchToLab(lchColor) {
const { L, C, h } = lchColor;
const hRad = degreesToRadians(h);
const a = C * Math.cos(hRad);
const b = C * Math.sin(hRad);
return { L, a, b };
}
// --- Direct sRGB ↔ CIELAB Conversions (Optimized from abridged) ---
/**
* Converts sRGB directly to CIELAB.
* Optimized implementation that bypasses intermediate XYZ object creation.
* @param {SrgbColor} srgbColor - sRGB color { r, g, b } with values 0-1.
* @param {Object} [referenceWhite=D65_WHITE_POINT_XYZ] - Reference white point.
* @returns {LabColor} CIELAB color { L, a, b }.
*/
export function srgbToLab(srgbColor, referenceWhite = D65_WHITE_POINT_XYZ) {
// Convert to linear RGB
const lin = srgbToLinearSrgb(srgbColor);
// Convert to XYZ using matrix multiplication (scaled to 0-100)
const x = (MATRIX_LINEAR_SRGB_TO_XYZ_D65[0][0] * lin.r +
MATRIX_LINEAR_SRGB_TO_XYZ_D65[0][1] * lin.g +
MATRIX_LINEAR_SRGB_TO_XYZ_D65[0][2] * lin.b) * 100;
const y = (MATRIX_LINEAR_SRGB_TO_XYZ_D65[1][0] * lin.r +
MATRIX_LINEAR_SRGB_TO_XYZ_D65[1][1] * lin.g +
MATRIX_LINEAR_SRGB_TO_XYZ_D65[1][2] * lin.b) * 100;
const z = (MATRIX_LINEAR_SRGB_TO_XYZ_D65[2][0] * lin.r +
MATRIX_LINEAR_SRGB_TO_XYZ_D65[2][1] * lin.g +
MATRIX_LINEAR_SRGB_TO_XYZ_D65[2][2] * lin.b) * 100;
// Convert XYZ to Lab
const fx = cielabForwardTransform(x / referenceWhite.X);
const fy = cielabForwardTransform(y / referenceWhite.Y);
const fz = cielabForwardTransform(z / referenceWhite.Z);
return {
L: 116 * fy - 16,
a: 500 * (fx - fy),
b: 200 * (fy - fz)
};
}
/**
* Converts CIELAB directly to sRGB.
* Optimized implementation that bypasses intermediate XYZ object creation.
* @param {LabColor} labColor - CIELAB color { L, a, b }.
* @param {Object} [referenceWhite=D65_WHITE_POINT_XYZ] - Reference white point.
* @returns {SrgbColor} sRGB color { r, g, b } with values 0-1.
*/
export function labToSrgb(labColor, referenceWhite = D65_WHITE_POINT_XYZ) {
const { L, a, b } = labColor;
// Convert Lab to XYZ
const fy = (L + 16) / 116;
const fx = a / 500 + fy;
const fz = fy - b / 200;
const x = cielabInverseTransform(fx) * referenceWhite.X;
const y = cielabInverseTransform(fy) * referenceWhite.Y;
const z = cielabInverseTransform(fz) * referenceWhite.Z;
// Convert XYZ to linear RGB (scale from 100 to 1)
const linR = (MATRIX_XYZ_TO_LINEAR_SRGB_D65[0][0] * x +
MATRIX_XYZ_TO_LINEAR_SRGB_D65[0][1] * y +
MATRIX_XYZ_TO_LINEAR_SRGB_D65[0][2] * z) / 100;
const linG = (MATRIX_XYZ_TO_LINEAR_SRGB_D65[1][0] * x +
MATRIX_XYZ_TO_LINEAR_SRGB_D65[1][1] * y +
MATRIX_XYZ_TO_LINEAR_SRGB_D65[1][2] * z) / 100;
const linB = (MATRIX_XYZ_TO_LINEAR_SRGB_D65[2][0] * x +
MATRIX_XYZ_TO_LINEAR_SRGB_D65[2][1] * y +
MATRIX_XYZ_TO_LINEAR_SRGB_D65[2][2] * z) / 100;
// Apply gamma correction
return linearSrgbToSrgb({ r: linR, g: linG, b: linB });
}
// --- sRGB ↔ CIELCh Convenience Functions ---
/**
* Converts sRGB to CIELCh.
* @param {SrgbColor} srgbColor - sRGB color { r, g, b } with values 0-1.
* @param {Object} [referenceWhite=D65_WHITE_POINT_XYZ] - Reference white point.
* @returns {LchColor} CIELCh color { L, C, h }.
*/
export function srgbToLch(srgbColor, referenceWhite = D65_WHITE_POINT_XYZ) {
const lab = srgbToLab(srgbColor, referenceWhite);
return labToLch(lab);
}
/**
* Converts CIELCh to sRGB.
* @param {LchColor} lchColor - CIELCh color { L, C, h }.
* @param {Object} [referenceWhite=D65_WHITE_POINT_XYZ] - Reference white point.
* @returns {SrgbColor} sRGB color { r, g, b } with values 0-1.
*/
export function lchToSrgb(lchColor, referenceWhite = D65_WHITE_POINT_XYZ) {
const lab = lchToLab(lchColor);
return labToSrgb(lab, referenceWhite);
}