@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
JavaScript
/**
* @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])
};
}