UNPKG

@markgorzynski/color-utils

Version:

The only color science library with adaptive visual perception modeling and accessibility-driven optimization.

261 lines (231 loc) 8.27 kB
/** * @module display-p3 * @description Display P3 color space conversions and utilities. * Display P3 is a wide-gamut RGB color space used by Apple devices and * increasingly supported in modern web browsers. * * Display P3 uses DCI-P3 primaries adapted to D65 white point. * This implementation follows the CSS Color Module Level 4 specification. * * @see {@link https://www.w3.org/TR/css-color-4/#predefined-display-p3} */ import { multiplyMatrixVector } from './utils.js'; import { srgbToLinearSrgb, linearSrgbToSrgb } from './srgb.js'; /** @typedef {{r: number, g: number, b: number}} DisplayP3Color */ /** @typedef {{r: number, g: number, b: number}} LinearDisplayP3Color */ // --- Display P3 Constants --- /** * Display P3 primaries and white point in CIE xy chromaticity coordinates * @private */ export const DISPLAY_P3_PRIMARIES = Object.freeze({ red: { x: 0.680, y: 0.320 }, green: { x: 0.265, y: 0.690 }, blue: { x: 0.150, y: 0.060 }, white: { x: 0.3127, y: 0.3290 } // D65 }); /** * Matrix to convert from linear Display P3 to XYZ (D65 adapted) * Derived from the Display P3 primaries * @private */ export const MATRIX_LINEAR_DISPLAY_P3_TO_XYZ_D65 = Object.freeze([ Object.freeze([0.4865709, 0.2656677, 0.1982173]), Object.freeze([0.2289746, 0.6917385, 0.0792869]), Object.freeze([0.0000000, 0.0451134, 1.0439444]) ]); /** * Matrix to convert from XYZ to linear Display P3 (D65 adapted) * Inverse of MATRIX_LINEAR_DISPLAY_P3_TO_XYZ_D65 * @private */ export const MATRIX_XYZ_TO_LINEAR_DISPLAY_P3_D65 = Object.freeze([ Object.freeze([2.4934969, -0.9313836, -0.4027108]), Object.freeze([-0.8294890, 1.7626641, 0.0236247]), Object.freeze([0.0358458, -0.0761724, 0.9568845]) ]); /** * Matrix for direct linear sRGB to linear Display P3 conversion * This is more efficient than going through XYZ * @private */ export const MATRIX_LINEAR_SRGB_TO_LINEAR_DISPLAY_P3 = Object.freeze([ Object.freeze([0.8224621, 0.1775380, 0.0000000]), Object.freeze([0.0331941, 0.9668058, 0.0000001]), Object.freeze([0.0170827, 0.0723974, 0.9105199]) ]); /** * Matrix for direct linear Display P3 to linear sRGB conversion * Inverse of MATRIX_LINEAR_SRGB_TO_LINEAR_DISPLAY_P3 * @private */ export const MATRIX_LINEAR_DISPLAY_P3_TO_LINEAR_SRGB = Object.freeze([ Object.freeze([1.2249401, -0.2249404, 0.0000003]), Object.freeze([-0.0420569, 1.0420571, -0.0000002]), Object.freeze([-0.0196376, -0.0786361, 1.0982737]) ]); // --- Gamma Correction --- /** * Apply Display P3 gamma correction (same as sRGB gamma) * @private */ function displayP3ChannelToLinear(channel) { return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4); } /** * Remove Display P3 gamma correction (same as sRGB gamma) * @private */ function linearChannelToDisplayP3(channel) { return channel <= 0.0031308 ? channel * 12.92 : 1.055 * Math.pow(channel, 1 / 2.4) - 0.055; } // --- Display P3 ↔ Linear Display P3 --- /** * Convert Display P3 to linear Display P3 * @param {DisplayP3Color} p3Color - Display P3 color with gamma correction * @returns {LinearDisplayP3Color} Linear Display P3 color */ export function displayP3ToLinearDisplayP3(p3Color) { return { r: displayP3ChannelToLinear(p3Color.r), g: displayP3ChannelToLinear(p3Color.g), b: displayP3ChannelToLinear(p3Color.b) }; } /** * Convert linear Display P3 to Display P3 with gamma correction * @param {LinearDisplayP3Color} linearP3Color - Linear Display P3 color * @returns {DisplayP3Color} Display P3 color with gamma correction */ export function linearDisplayP3ToDisplayP3(linearP3Color) { return { r: linearChannelToDisplayP3(linearP3Color.r), g: linearChannelToDisplayP3(linearP3Color.g), b: linearChannelToDisplayP3(linearP3Color.b) }; } // --- sRGB ↔ Display P3 Conversions --- /** * Convert sRGB to Display P3 * @param {SrgbColor} srgbColor - sRGB color * @returns {DisplayP3Color} Display P3 color * @example * // Convert pure red from sRGB to Display P3 * const p3Red = srgbToDisplayP3({ r: 1, g: 0, b: 0 }); * // Result: { r: 0.9175, g: 0.2003, b: 0.1386 } */ export function srgbToDisplayP3(srgbColor) { // Convert to linear sRGB const linearSrgb = srgbToLinearSrgb(srgbColor); // Convert to linear Display P3 using direct matrix const [r, g, b] = multiplyMatrixVector( MATRIX_LINEAR_SRGB_TO_LINEAR_DISPLAY_P3, [linearSrgb.r, linearSrgb.g, linearSrgb.b] ); // Apply Display P3 gamma return linearDisplayP3ToDisplayP3({ r, g, b }); } /** * Convert Display P3 to sRGB * Note: Display P3 has a wider gamut than sRGB, so colors may be clipped * @param {DisplayP3Color} p3Color - Display P3 color * @returns {SrgbColor} sRGB color (may be clipped if out of gamut) */ export function displayP3ToSrgb(p3Color) { // Convert to linear Display P3 const linearP3 = displayP3ToLinearDisplayP3(p3Color); // Convert to linear sRGB using direct matrix const [r, g, b] = multiplyMatrixVector( MATRIX_LINEAR_DISPLAY_P3_TO_LINEAR_SRGB, [linearP3.r, linearP3.g, linearP3.b] ); // Apply sRGB gamma return linearSrgbToSrgb({ r, g, b }); } // --- XYZ ↔ Display P3 Conversions --- /** * Convert linear Display P3 to XYZ (D65) * @param {LinearDisplayP3Color} linearP3Color - Linear Display P3 color * @returns {{X: number, Y: number, Z: number}} XYZ color */ export function linearDisplayP3ToXyz(linearP3Color) { const [X, Y, Z] = multiplyMatrixVector( MATRIX_LINEAR_DISPLAY_P3_TO_XYZ_D65, [linearP3Color.r, linearP3Color.g, linearP3Color.b] ); return { X, Y, Z }; } /** * Convert XYZ (D65) to linear Display P3 * @param {{X: number, Y: number, Z: number}} xyzColor - XYZ color * @returns {LinearDisplayP3Color} Linear Display P3 color */ export function xyzToLinearDisplayP3(xyzColor) { const [r, g, b] = multiplyMatrixVector( MATRIX_XYZ_TO_LINEAR_DISPLAY_P3_D65, [xyzColor.X, xyzColor.Y, xyzColor.Z] ); return { r, g, b }; } // --- Utility Functions --- /** * Check if a Display P3 color is within sRGB gamut * @param {DisplayP3Color} p3Color - Display P3 color to check * @returns {boolean} True if the color can be represented in sRGB */ export function isDisplayP3InSrgbGamut(p3Color) { const srgb = displayP3ToSrgb(p3Color); const epsilon = 0.00001; return ( srgb.r >= -epsilon && srgb.r <= 1 + epsilon && srgb.g >= -epsilon && srgb.g <= 1 + epsilon && srgb.b >= -epsilon && srgb.b <= 1 + epsilon ); } /** * Check if an sRGB color utilizes the extended Display P3 gamut * @param {SrgbColor} srgbColor - sRGB color to check * @returns {boolean} True if converting to P3 provides a wider gamut representation */ export function benefitsFromDisplayP3(srgbColor) { const p3 = srgbToDisplayP3(srgbColor); // If any P3 channel is significantly different from sRGB, it benefits from P3 const epsilon = 0.01; return ( Math.abs(p3.r - srgbColor.r) > epsilon || Math.abs(p3.g - srgbColor.g) > epsilon || Math.abs(p3.b - srgbColor.b) > epsilon ); } /** * Format a Display P3 color for CSS Color Module Level 4 * @param {DisplayP3Color} p3Color - Display P3 color * @param {number} [precision=4] - Decimal precision * @returns {string} CSS color() function string * @example * formatDisplayP3ForCSS({ r: 0.9175, g: 0.2003, b: 0.1386 }) * // Returns: "color(display-p3 0.9175 0.2003 0.1386)" */ export function formatDisplayP3ForCSS(p3Color, precision = 4) { const r = p3Color.r.toFixed(precision); const g = p3Color.g.toFixed(precision); const b = p3Color.b.toFixed(precision); return `color(display-p3 ${r} ${g} ${b})`; } /** * Parse a CSS Color Module Level 4 display-p3 color * @param {string} cssString - CSS color() function string * @returns {DisplayP3Color|null} Display P3 color or null if invalid * @example * parseDisplayP3FromCSS("color(display-p3 0.9175 0.2003 0.1386)") * // Returns: { r: 0.9175, g: 0.2003, b: 0.1386 } */ export function parseDisplayP3FromCSS(cssString) { const match = cssString.match(/color\(\s*display-p3\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\)/); if (!match) return null; return { r: parseFloat(match[1]), g: parseFloat(match[2]), b: parseFloat(match[3]) }; }