|
// ------------------------- |
|
// rgb |
|
// ------------------------- |
|
function rgbToHex(r, g, b) { |
|
return `#${[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("")}`; |
|
} |
|
|
|
function hexToRgb(hex) { |
|
// Remove the '#' if present |
|
hex = hex.replace(/^#/, ""); |
|
|
|
// Parse short hex (e.g., #FFF → #FFFFFF) |
|
if (hex.length === 3) { |
|
hex = hex |
|
.split("") |
|
.map((char) => char + char) |
|
.join(""); |
|
} |
|
|
|
// Convert hex to RGB values |
|
const r = parseInt(hex.substring(0, 2), 16); |
|
const g = parseInt(hex.substring(2, 4), 16); |
|
const b = parseInt(hex.substring(4, 6), 16); |
|
|
|
return [r, g, b]; |
|
} |
|
|
|
// ------------------------- |
|
// oklch |
|
// ------------------------- |
|
|
|
// sRGB to Linear RGB |
|
function rgbToLinear([r, g, b]) { |
|
const gammaCorrection = (v) => { |
|
v /= 255; |
|
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); |
|
}; |
|
return [gammaCorrection(r), gammaCorrection(g), gammaCorrection(b)]; |
|
} |
|
|
|
function linearRgbToLms([r, g, b]) { |
|
return [ |
|
r * 0.412165612 + g * 0.536275208 + b * 0.0514575653, |
|
r * 0.211859107 + g * 0.6807189584 + b * 0.107406579, |
|
r * 0.0883097947 + g * 0.2818474174 + b * 0.6298384131 |
|
]; |
|
} |
|
|
|
function lmsToOklab(lms) { |
|
const lmsPrime = lms.map((v) => Math.cbrt(v)); |
|
|
|
return [ |
|
0.2104542553 * lmsPrime[0] + |
|
0.793617785 * lmsPrime[1] - |
|
0.0040720468 * lmsPrime[2], |
|
1.9779984951 * lmsPrime[0] - |
|
2.428592205 * lmsPrime[1] + |
|
0.4505937099 * lmsPrime[2], |
|
0.0259040371 * lmsPrime[0] + |
|
0.7827717662 * lmsPrime[1] - |
|
0.808675766 * lmsPrime[2] |
|
]; |
|
} |
|
|
|
function oklabToOklch([L, a, b]) { |
|
const C = Math.sqrt(a * a + b * b); |
|
const H = (Math.atan2(b, a) * (180 / Math.PI) + 360) % 360; |
|
|
|
return [ |
|
(L * 100).toFixed(2), // Lightness |
|
C.toFixed(4), // Chroma |
|
H.toFixed(2) // Hue |
|
]; |
|
} |
|
|
|
function rgbToOklch(r, g, b) { |
|
const linearRgb = rgbToLinear([r, g, b]); |
|
const lms = linearRgbToLms(linearRgb); |
|
const oklab = lmsToOklab(lms); |
|
return oklabToOklch(oklab); |
|
} |
|
|
|
function oklchToOklab([L, C, H]) { |
|
const hRad = (H * Math.PI) / 180; |
|
return [L, C * Math.cos(hRad), C * Math.sin(hRad)]; |
|
} |
|
|
|
function oklabToLms([L, a, b]) { |
|
const lmsPrime = [ |
|
L + 0.3963377774 * a + 0.2158037573 * b, |
|
L - 0.1055613458 * a - 0.0638541728 * b, |
|
L - 0.0894841775 * a - 1.291485548 * b |
|
]; |
|
|
|
// Convert back from cube root |
|
return lmsPrime.map((v) => v ** 3); |
|
} |
|
|
|
function lmsToLinearRgb([l, m, s]) { |
|
return [ |
|
l * 4.0767416621 - m * 3.3077115913 + s * 0.2309699292, |
|
l * -1.2684380046 + m * 2.6097574011 - s * 0.3413193965, |
|
l * -0.0041960863 - m * 0.7034186147 + s * 1.707614701 |
|
]; |
|
} |
|
|
|
function linearRgbToRgb(rgb) { |
|
return rgb.map((v) => { |
|
v = v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055; |
|
return Math.round(Math.max(0, Math.min(1, v)) * 255); |
|
}); |
|
} |
|
|
|
function oklchToRgb(L, C, H) { |
|
const oklab = oklchToOklab([L, C, H]); |
|
const lms = oklabToLms(oklab); |
|
const linearRgb = lmsToLinearRgb(lms); |
|
return linearRgbToRgb(linearRgb); |
|
} |
|
|
|
// ------------------------- |
|
// hsl |
|
// ------------------------- |
|
|
|
function computeHue(r, g, b, max, delta) { |
|
let h = 0; |
|
if (delta !== 0) { |
|
if (max === r) h = ((g - b) / delta) % 6; |
|
else if (max === g) h = (b - r) / delta + 2; |
|
else if (max === b) h = (r - g) / delta + 4; |
|
|
|
h *= 60; |
|
if (h < 0) h += 360; |
|
} |
|
return parseFloat(h.toFixed(2)); |
|
} |
|
|
|
function rgbToHsl(r, g, b) { |
|
(r /= 255), (g /= 255), (b /= 255); |
|
const max = Math.max(r, g, b); |
|
const min = Math.min(r, g, b); |
|
const delta = max - min; |
|
|
|
let h = computeHue(r, g, b, max, delta); |
|
let l = (max + min) / 2; |
|
let s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); |
|
|
|
return [ |
|
h, // Hue in degrees |
|
parseFloat((s * 100).toFixed(2)), // Saturation in % |
|
parseFloat((l * 100).toFixed(2)) // Lightness in % |
|
]; |
|
} |
|
|
|
function hslToRgb(h, s, l) { |
|
s = parseFloat(s) / 100; |
|
l = parseFloat(l) / 100; |
|
|
|
const C = (1 - Math.abs(2 * l - 1)) * s; |
|
const X = C * (1 - Math.abs(((h / 60) % 2) - 1)); |
|
const m = l - C / 2; |
|
|
|
let r = 0, |
|
g = 0, |
|
b = 0; |
|
|
|
if (h >= 0 && h < 60)[r, g, b] = [C, X, 0]; |
|
else if (h >= 60 && h < 120)[r, g, b] = [X, C, 0]; |
|
else if (h >= 120 && h < 180)[r, g, b] = [0, C, X]; |
|
else if (h >= 180 && h < 240)[r, g, b] = [0, X, C]; |
|
else if (h >= 240 && h < 300)[r, g, b] = [X, 0, C]; |
|
else if (h >= 300 && h < 360)[r, g, b] = [C, 0, X]; |
|
|
|
return [ |
|
Math.round((r + m) * 255), |
|
Math.round((g + m) * 255), |
|
Math.round((b + m) * 255) |
|
]; |
|
} |
|
|
|
// ------------------------- |
|
// lch |
|
// ------------------------- |
|
function linearRgbToXyz([r, g, b]) { |
|
// sRGB to XYZ (D50) matrix |
|
return [ |
|
r * 0.4360747 + g * 0.3850649 + b * 0.1430804, |
|
r * 0.2225045 + g * 0.7168786 + b * 0.0606169, |
|
r * 0.0139322 + g * 0.0971045 + b * 0.7141733 |
|
]; |
|
} |
|
|
|
function xyzToLab([x, y, z]) { |
|
// D50 reference white point |
|
const refX = 0.96422; |
|
const refY = 1.0; |
|
const refZ = 0.82521; |
|
|
|
x /= refX; |
|
y /= refY; |
|
z /= refZ; |
|
|
|
const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116); |
|
|
|
const fx = f(x); |
|
const fy = f(y); |
|
const fz = f(z); |
|
|
|
const L = 116 * fy - 16; |
|
const a = 500 * (fx - fy); |
|
const b = 200 * (fy - fz); |
|
|
|
return [L, a, b]; |
|
} |
|
|
|
function labToLch([L, a, b]) { |
|
const C = Math.sqrt(a * a + b * b); |
|
let H = Math.atan2(b, a) * (180 / Math.PI); |
|
if (H < 0) H += 360; |
|
return [L, C, H]; |
|
} |
|
|
|
function rgbToLch(r, g, b) { |
|
const linearRgb = rgbToLinear([r, g, b]); |
|
const xyz = linearRgbToXyz(linearRgb); |
|
const lab = xyzToLab(xyz); |
|
return labToLch(lab).map((v) => parseFloat(v.toFixed(2))); |
|
} |
|
|
|
function lchToLab([L, C, H]) { |
|
const hRad = (H * Math.PI) / 180; |
|
return [L, C * Math.cos(hRad), C * Math.sin(hRad)]; |
|
} |
|
|
|
function labToXyz([L, a, b]) { |
|
const refX = 0.96422, |
|
refY = 1.0, |
|
refZ = 0.82521; // D50 white |
|
|
|
const fy = (L + 16) / 116; |
|
const fx = fy + a / 500; |
|
const fz = fy - b / 200; |
|
|
|
const fInv = (t) => (t ** 3 > 0.008856 ? t ** 3 : (t - 16 / 116) / 7.787); |
|
|
|
const x = fInv(fx) * refX; |
|
const y = fInv(fy) * refY; |
|
const z = fInv(fz) * refZ; |
|
|
|
return [x, y, z]; |
|
} |
|
|
|
function xyzToLinearRgb([x, y, z]) { |
|
return [ |
|
x * 3.1338561 + y * -1.6168667 + z * -0.4906146, |
|
x * -0.9787684 + y * 1.9161415 + z * 0.033454, |
|
x * 0.0719453 + y * -0.2289914 + z * 1.4052427 |
|
]; |
|
} |
|
|
|
function lchToRgb(L, C, H) { |
|
const lab = lchToLab([L, C, H]); |
|
const xyz = labToXyz(lab); |
|
const linearRgb = xyzToLinearRgb(xyz); |
|
return linearRgbToRgb(linearRgb); |
|
} |
|
|
|
// ------------------------- |
|
// lab |
|
// ------------------------- |
|
function rgbToLab(r, g, b, usePercentage = false) { |
|
const linearRgb = rgbToLinear([r, g, b]); |
|
const xyz = linearRgbToXyz(linearRgb); |
|
let [L, a, b_] = xyzToLab(xyz); |
|
|
|
if (usePercentage) { |
|
L = `${parseFloat(L.toFixed(2))}%`; |
|
} else { |
|
L = parseFloat(L.toFixed(2)); |
|
} |
|
|
|
return [L, parseFloat(a.toFixed(2)), parseFloat(b_.toFixed(2))]; |
|
} |
|
|
|
function labToRgb(L, a, b) { |
|
const xyz = labToXyz([L, a, b]); |
|
const linearRgb = xyzToLinearRgb(xyz); |
|
return linearRgbToRgb(linearRgb); |
|
} |
|
|
|
// ------------------------- |
|
// hsv |
|
// ------------------------- |
|
function rgbToHsv(r, g, b) { |
|
(r /= 255), (g /= 255), (b /= 255); |
|
const max = Math.max(r, g, b); |
|
const min = Math.min(r, g, b); |
|
const delta = max - min; |
|
|
|
let h = computeHue(r, g, b, max, delta); |
|
let s = max === 0 ? 0 : delta / max; |
|
let v = max; |
|
|
|
return [ |
|
h, // Hue in degrees |
|
parseFloat((s * 100).toFixed(2)), // Saturation in % |
|
parseFloat((v * 100).toFixed(2)) // Value in % |
|
]; |
|
} |
|
|
|
function hsvToRgb(h, s, v) { |
|
s /= 100; |
|
v /= 100; |
|
|
|
const C = v * s; |
|
const X = C * (1 - Math.abs(((h / 60) % 2) - 1)); |
|
const m = v - C; |
|
|
|
let r = 0, |
|
g = 0, |
|
b = 0; |
|
|
|
if (h >= 0 && h < 60)[r, g, b] = [C, X, 0]; |
|
else if (h >= 60 && h < 120)[r, g, b] = [X, C, 0]; |
|
else if (h >= 120 && h < 180)[r, g, b] = [0, C, X]; |
|
else if (h >= 180 && h < 240)[r, g, b] = [0, X, C]; |
|
else if (h >= 240 && h < 300)[r, g, b] = [X, 0, C]; |
|
else if (h >= 300 && h < 360)[r, g, b] = [C, 0, X]; |
|
|
|
return [ |
|
Math.round((r + m) * 255), |
|
Math.round((g + m) * 255), |
|
Math.round((b + m) * 255) |
|
]; |
|
} |
|
|
|
// ------------------------- |
|
// display-p3 |
|
// ------------------------- |
|
|
|
// sRGB to Linear RGB |
|
function toLinear(v) { |
|
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); |
|
} |
|
|
|
// sRGB to XYZ (D65) |
|
function linearSrgbToXyz([r, g, b]) { |
|
return [ |
|
r * 0.4124564 + g * 0.3575761 + b * 0.1804375, |
|
r * 0.2126729 + g * 0.7151522 + b * 0.072175, |
|
r * 0.0193339 + g * 0.119192 + b * 0.9503041 |
|
]; |
|
} |
|
|
|
// Convert XYZ to Linear Display-P3 (Corrected Matrix) |
|
function xyzToLinearP3([x, y, z]) { |
|
return [ |
|
x * 2.493496911941425 + y * -0.9313836179191239 + z * -0.4027107844507168, // R |
|
x * -0.8294890169312207 + y * 1.7626640603183463 + z * 0.02362468584194357, // G |
|
x * 0.03584583024378447 + y * -0.07617238926804182 + z * 0.9568845240076872 // B |
|
]; |
|
} |
|
|
|
// Display-P3 Gamma Encoding |
|
function toGammaP3(v) { |
|
return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1.0 / 2.4) - 0.055; |
|
} |
|
|
|
// Apply Gamma After Clamping |
|
function applyDisplayP3Gamma([r, g, b]) { |
|
return [ |
|
toGammaP3(Math.max(0, Math.min(1, r))), |
|
toGammaP3(Math.max(0, Math.min(1, g))), |
|
toGammaP3(Math.max(0, Math.min(1, b))) |
|
]; |
|
} |
|
|
|
// Convert RGB to Display-P3 |
|
function rgbToDisplayP3(r, g, b) { |
|
(r /= 255), (g /= 255), (b /= 255); |
|
|
|
// sRGB to Linear RGB |
|
const linearR = toLinear(r); |
|
const linearG = toLinear(g); |
|
const linearB = toLinear(b); |
|
|
|
// Linear RGB → XYZ (D65) |
|
let [x, y, z] = linearSrgbToXyz([linearR, linearG, linearB]); |
|
let [p3R, p3G, p3B] = xyzToLinearP3([x, y, z]); |
|
let [finalR, finalG, finalB] = applyDisplayP3Gamma([p3R, p3G, p3B]); |
|
|
|
return [ |
|
parseFloat(finalR.toFixed(4)), |
|
parseFloat(finalG.toFixed(4)), |
|
parseFloat(finalB.toFixed(4)) |
|
]; |
|
} |
|
|
|
// Inverse Gamma Correction for Display-P3 |
|
function fromGammaP3(v) { |
|
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); |
|
} |
|
|
|
// Convert Display-P3 to XYZ (D65) - Inverse Matrix |
|
function linearP3ToXyz([r, g, b]) { |
|
return [ |
|
r * 0.515102 + g * 0.2919656 + b * 0.1571934, |
|
r * 0.2411823 + g * 0.6922363 + b * 0.0665814, |
|
r * -0.001049 + g * 0.0418819 + b * 0.7843782 |
|
]; |
|
} |
|
|
|
// Convert XYZ to Linear sRGB - Inverse Matrix |
|
function xyzToLinearSrgb([x, y, z]) { |
|
return [ |
|
x * 3.1338561 + y * -1.6168667 + z * -0.4906146, |
|
x * -0.9787684 + y * 1.9161415 + z * 0.033454, |
|
x * 0.0719453 + y * -0.2289914 + z * 1.4052427 |
|
]; |
|
} |
|
|
|
// Standard sRGB Gamma Correction |
|
function toGamma(v) { |
|
return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055; |
|
} |
|
|
|
function displayP3ToRgb(r, g, b) { |
|
// Inverse Gamma Correction for Display-P3 |
|
r = fromGammaP3(r); |
|
g = fromGammaP3(g); |
|
b = fromGammaP3(b); |
|
// Convert Linear Display-P3 to XYZ (D65) |
|
let [x, y, z] = linearP3ToXyz([r, g, b]); |
|
|
|
// Convert XYZ (D65) to Linear sRGB |
|
let [srgbR, srgbG, srgbB] = xyzToLinearSrgb([x, y, z]); |
|
|
|
// Apply Gamma Correction for sRGB |
|
srgbR = toGamma(srgbR); |
|
srgbG = toGamma(srgbG); |
|
srgbB = toGamma(srgbB); |
|
|
|
// Scale to 0-255 range and round |
|
return [ |
|
Math.round(srgbR * 255), |
|
Math.round(srgbG * 255), |
|
Math.round(srgbB * 255) |
|
]; |
|
} |
|
|
|
// Color Blindness Simulation Matrices |
|
const colorBlindMatrix = { |
|
protanopia: [ |
|
[0.567, 0.433, 0], |
|
[0.558, 0.442, 0], |
|
[0, 0.242, 0.758] |
|
], |
|
deuteranopia: [ |
|
[0.625, 0.375, 0], |
|
[0.7, 0.3, 0], |
|
[0, 0.3, 0.7] |
|
], |
|
tritanopia: [ |
|
[0.95, 0.05, 0], |
|
[0, 0.433, 0.567], |
|
[0, 0.475, 0.525] |
|
] |
|
}; |
|
|
|
function simulateColorBlindness(r, g, b, type = "protanopia") { |
|
const matrix = colorBlindMatrix[type]; |
|
return [ |
|
r * matrix[0][0] + g * matrix[0][1] + b * matrix[0][2], |
|
r * matrix[1][0] + g * matrix[1][1] + b * matrix[1][2], |
|
r * matrix[2][0] + g * matrix[2][1] + b * matrix[2][2] |
|
].map((v) => Math.round(v)); |
|
} |