|
import { mat3, vec3 } from "gl-matrix"; |
|
import { |
|
clamp, |
|
gamma_to_linear, |
|
linear_to_gamma, |
|
multiplyMatrices, |
|
} from "./util.js"; |
|
|
|
// OKLab and OKLCH |
|
// https://bottosson.github.io/posts/oklab/ |
|
|
|
// XYZ <-> LMS matrices recalculated for consistent reference white |
|
// see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-943521484 |
|
// recalculated for 64bit precision |
|
// see https://github.com/color-js/color.js/pull/357 |
|
|
|
// Given XYZ relative to D65, convert to OKLab |
|
export const XYZ_to_LMS_M = [ |
|
[0.819022437996703, 0.3619062600528904, -0.1288737815209879], |
|
[0.0329836539323885, 0.9292868615863434, 0.0361446663506424], |
|
[0.0481771893596242, 0.2642395317527308, 0.6335478284694309], |
|
]; |
|
|
|
export const LMS_to_OKLab_M = [ |
|
[0.210454268309314, 0.7936177747023054, -0.0040720430116193], |
|
[1.9779985324311684, -2.4285922420485799, 0.450593709617411], |
|
[0.0259040424655478, 0.7827717124575296, -0.8086757549230774], |
|
]; |
|
|
|
// Given OKLab, convert to XYZ relative to D65 |
|
export const LMS_to_XYZ_M = [ |
|
[1.2268798758459243, -0.5578149944602171, 0.2813910456659647], |
|
[-0.0405757452148008, 1.112286803280317, -0.0717110580655164], |
|
[-0.0763729366746601, -0.4214933324022432, 1.5869240198367816], |
|
]; |
|
|
|
export const OKLab_to_LMS_M = [ |
|
[1.0, 0.3963377773761749, 0.2158037573099136], |
|
[1.0, -0.1055613458156586, -0.0638541728258133], |
|
[1.0, -0.0894841775298119, -1.2914855480194092], |
|
]; |
|
|
|
export const linear_sRGB_to_LMS_M = [ |
|
[0.4122214694707629, 0.5363325372617349, 0.051445993267502196], |
|
[0.2119034958178251, 0.6806995506452345, 0.10739695353694051], |
|
[0.08830245919005637, 0.2817188391361215, 0.6299787016738223], |
|
]; |
|
|
|
export const LMS_to_linear_sRGB_M = [ |
|
[4.076741636075959, -3.307711539258062, 0.2309699031821041], |
|
[-1.2684379732850313, 2.6097573492876878, -0.3413193760026569], |
|
[-0.004196076138675526, -0.703418617935936, 1.7076146940746113], |
|
]; |
|
|
|
export const LMS_to_linear_P3_M = [ |
|
[3.127768971361874, -2.2571357625916395, 0.12936679122976516], |
|
[-1.0910090184377979, 2.413331710306922, -0.32232269186912466], |
|
[-0.02601080193857028, -0.508041331704167, 1.5340521336427373], |
|
]; |
|
|
|
export const linear_P3_to_LMS_M = [ |
|
[0.4813798527499543, 0.4621183710113182, 0.05650177623872754], |
|
[0.2288319418112447, 0.6532168193835677, 0.11795123880518772], |
|
[0.08394575232299314, 0.22416527097756647, 0.6918889766994405], |
|
]; |
|
|
|
// https://github.com/w3c/csswg-drafts/issues/5922 |
|
export const linear_sRGB_to_XYZ_M = [ |
|
[0.41239079926595934, 0.357584339383878, 0.1804807884018343], |
|
[0.21263900587151027, 0.715168678767756, 0.07219231536073371], |
|
[0.01933081871559182, 0.11919477979462598, 0.9505321522496607], |
|
]; |
|
|
|
export const XYZ_to_linear_sRGB_M = [ |
|
[3.2409699419045226, -1.537383177570094, -0.4986107602930034], |
|
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559], |
|
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786], |
|
]; |
|
|
|
// Display-P3 to XYZ and back |
|
export const linear_P3_to_XYZ_M = [ |
|
[608311 / 1250200, 189793 / 714400, 198249 / 1000160], |
|
[35783 / 156275, 247089 / 357200, 198249 / 2500400], |
|
[0 / 1, 32229 / 714400, 5220557 / 5000800], |
|
]; |
|
|
|
export const XYZ_to_linear_P3_M = [ |
|
[446124 / 178915, -333277 / 357830, -72051 / 178915], |
|
[-14852 / 17905, 63121 / 35810, 423 / 17905], |
|
[11844 / 330415, -50337 / 660830, 316169 / 330415], |
|
]; |
|
|
|
const floatToByte = (n) => clamp(Math.round(255 * n), 0, 255); |
|
|
|
export function XYZ_to_OKLab(XYZ) { |
|
const LMS = multiplyMatrices(XYZ_to_LMS_M, XYZ); |
|
// JavaScript Math.cbrt returns a sign-matched cube root |
|
// beware if porting to other languages |
|
// especially if tempted to use a general power function |
|
return multiplyMatrices( |
|
LMS_to_OKLab_M, |
|
LMS.map((c) => Math.cbrt(c)) |
|
); |
|
// L in range [0,1]. For use in CSS, multiply by 100 and add a percent |
|
} |
|
|
|
export function OKLab_to_XYZ(OKLab) { |
|
const LMSnl = multiplyMatrices(OKLab_to_LMS_M, OKLab); |
|
return multiplyMatrices( |
|
LMS_to_XYZ_M, |
|
LMSnl.map((c) => c ** 3) |
|
); |
|
} |
|
|
|
// L = 0...1 |
|
// a, b = -0.4...0.4 |
|
export function OKLab_to_OKLCH(OKLab) { |
|
const hue = (Math.atan2(OKLab[2], OKLab[1]) * 180) / Math.PI; |
|
return [ |
|
OKLab[0], // L is still L |
|
Math.sqrt(OKLab[1] ** 2 + OKLab[2] ** 2), // Chroma |
|
hue >= 0 ? hue : hue + 360, // Hue, in degrees [0 to 360) |
|
]; |
|
} |
|
|
|
// L = 0...1 |
|
// C = -0.4..0.4 |
|
// H = 0...360 |
|
export function OKLCH_to_OKLab(OKLCH) { |
|
return [ |
|
OKLCH[0], // L is still L |
|
OKLCH[1] * Math.cos((OKLCH[2] * Math.PI) / 180), // a |
|
OKLCH[1] * Math.sin((OKLCH[2] * Math.PI) / 180), // b |
|
]; |
|
} |
|
|
|
export function linear_sRGB_to_XYZ(sRGB) { |
|
// convert an array of linear-light sRGB values to CIE XYZ |
|
// using sRGB's own white, D65 (no chromatic adaptation) |
|
return multiplyMatrices(linear_sRGB_to_XYZ_M, sRGB); |
|
} |
|
|
|
export function XYZ_to_linear_sRGB(XYZ) { |
|
// convert XYZ to linear-light sRGB |
|
return multiplyMatrices(XYZ_to_linear_sRGB_M, XYZ); |
|
} |
|
|
|
export const linear_sRGB_to_sRGB = (sRGB) => |
|
sRGB.map((n) => floatToByte(linear_to_gamma(n))); |
|
|
|
export const sRGB_to_linear_sRGB = (sRGB) => |
|
sRGB.map((n) => gamma_to_linear(n / 0xff)); |
|
|
|
export function linear_P3_to_XYZ(p3) { |
|
// convert an array of linear-light display-p3 values to CIE XYZ |
|
// using D65 (no chromatic adaptation) |
|
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html |
|
return multiplyMatrices(linear_P3_to_XYZ_M, p3); |
|
} |
|
|
|
export function XYZ_to_linear_P3(XYZ) { |
|
// convert XYZ to linear-light P3 |
|
return multiplyMatrices(XYZ_to_linear_P3_M, XYZ); |
|
} |
|
|
|
export const linear_P3_to_P3 = (p3) => p3.map((n) => linear_to_gamma(n)); |
|
export const P3_to_linear_P3 = (p3) => p3.map((n) => gamma_to_linear(n)); |
|
|
|
// const spaces = [ |
|
// 'srgb', |
|
// 'srgb-linear', |
|
// 'xyz', |
|
// 'xyy', |
|
// 'display-p3', |
|
// 'oklab', |
|
// 'oklch' |
|
// ]; |
|
|
|
// function convert (fromCoords, fromId, toId) { |
|
// } |
|
|
|
////// Utilities |
|
const epsilon = 0.000075; |
|
|
|
export const linear_sRGB_in_gamut = (lrgb) => |
|
lrgb.every((n) => { |
|
n = linear_to_gamma(n); |
|
return n >= -epsilon && n <= 1 + epsilon; |
|
}); |
|
|
|
export function linear_sRGB_to_OKLab(sRGB) { |
|
const R = sRGB[0]; |
|
const G = sRGB[1]; |
|
const B = sRGB[2]; |
|
const [l, m, s] = linear_sRGB_to_LMS_M.map((vec) => { |
|
return vec[0] * R + vec[1] * G + vec[2] * B; |
|
}); |
|
const l_ = Math.cbrt(l); |
|
const m_ = Math.cbrt(m); |
|
const s_ = Math.cbrt(s); |
|
return LMS_to_OKLab_M.map((vec) => { |
|
return vec[0] * l_ + vec[1] * m_ + vec[2] * s_; |
|
}); |
|
} |
|
|
|
export function OKLab_to_linear_sRGB(OKLab) { |
|
const L = OKLab[0]; |
|
const A = OKLab[1]; |
|
const B = OKLab[2]; |
|
const [l_, m_, s_] = OKLab_to_LMS_M.map((vec) => { |
|
return vec[0] * L + vec[1] * A + vec[2] * B; |
|
}); |
|
|
|
const l = l_ * l_ * l_; |
|
const m = m_ * m_ * m_; |
|
const s = s_ * s_ * s_; |
|
return LMS_to_linear_sRGB_M.map((vec) => { |
|
return vec[0] * l + vec[1] * m + vec[2] * s; |
|
}); |
|
} |
|
|
|
export function OKLab_to_linear_P3(OKLab) { |
|
const L = OKLab[0]; |
|
const A = OKLab[1]; |
|
const B = OKLab[2]; |
|
const [l_, m_, s_] = OKLab_to_LMS_M.map((vec) => { |
|
return vec[0] * L + vec[1] * A + vec[2] * B; |
|
}); |
|
|
|
const l = l_ * l_ * l_; |
|
const m = m_ * m_ * m_; |
|
const s = s_ * s_ * s_; |
|
return LMS_to_linear_P3_M.map((vec) => { |
|
return vec[0] * l + vec[1] * m + vec[2] * s; |
|
}); |
|
} |
|
|
|
export function linear_P3_to_OKLab(P3) { |
|
const R = P3[0]; |
|
const G = P3[1]; |
|
const B = P3[2]; |
|
const [l, m, s] = linear_P3_to_LMS_M.map((vec) => { |
|
return vec[0] * R + vec[1] * G + vec[2] * B; |
|
}); |
|
const l_ = Math.cbrt(l); |
|
const m_ = Math.cbrt(m); |
|
const s_ = Math.cbrt(s); |
|
return LMS_to_OKLab_M.map((vec) => { |
|
return vec[0] * l_ + vec[1] * m_ + vec[2] * s_; |
|
}); |
|
} |
|
|
|
export const OKLCH_to_linear_sRGB = (OKLCH) => |
|
OKLab_to_linear_sRGB(OKLCH_to_OKLab(OKLCH)); |
|
|
|
export const OKLab_to_sRGB = (OKLab) => |
|
linear_sRGB_to_sRGB(OKLab_to_linear_sRGB(OKLab)); |
|
|
|
export const OKLCH_to_sRGB = (OKLCH) => |
|
linear_sRGB_to_sRGB(OKLCH_to_linear_sRGB(OKLCH)); |
|
|
|
// export const linear_sRGB_to_OKLab = (sRGB) => |
|
// XYZ_to_OKLab(linear_sRGB_to_XYZ(sRGB)); |
|
|
|
export const linear_sRGB_to_OKLCH = (sRGB) => |
|
OKLab_to_OKLCH(linear_sRGB_to_OKLab(sRGB)); |
|
|
|
export const sRGB_to_OKLab = (sRGB) => |
|
linear_sRGB_to_OKLab(sRGB_to_linear_sRGB(sRGB)); |
|
|
|
export const sRGB_to_OKLCH = (sRGB) => |
|
linear_sRGB_to_OKLCH(sRGB_to_linear_sRGB(sRGB)); |
|
|
|
// p3 - oklab |
|
|
|
// direct versions now exist |
|
// export const OKLab_to_linear_P3 = (OKLab) => |
|
// XYZ_to_linear_P3(OKLab_to_XYZ(OKLab)); |
|
// export const linear_P3_to_OKLab = (p3) => XYZ_to_OKLab(linear_P3_to_XYZ(p3)); |
|
|
|
export const OKLab_to_P3 = (OKLab) => |
|
linear_P3_to_P3(OKLab_to_linear_P3(OKLab)); |
|
|
|
export const OKLCH_to_linear_P3 = (OKLCH) => |
|
XYZ_to_linear_P3(OKLab_to_XYZ(OKLCH_to_OKLab(OKLCH))); |
|
|
|
export const OKLCH_to_P3 = (OKLCH) => |
|
linear_P3_to_P3(OKLCH_to_linear_P3(OKLCH)); |
|
|
|
export const P3_to_OKLab = (p3) => linear_P3_to_OKLab(P3_to_linear_P3(p3)); |
|
|
|
export const XYZ_to_sRGB = (XYZ) => |
|
linear_sRGB_to_sRGB(XYZ_to_linear_sRGB(XYZ)); |