Skip to content

Commit

Permalink
feat: add XYZ D50 and split with D65 + split lab50/65
Browse files Browse the repository at this point in the history
- rename XYZ matrices for clarity and add sources
  • Loading branch information
dmnsgn committed Aug 9, 2024
1 parent bb52c54 commit 0cdc911
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 52 deletions.
89 changes: 66 additions & 23 deletions lab.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toXYZ, fromXYZ } from "./xyz.js";
import { toXYZD50, fromXYZD50, fromXYZD65, toXYZD65 } from "./xyz.js";
import { setAlpha } from "./utils.js";

/**
* @typedef {number[]} lab CIELAB with D65 standard illuminant as default.
* @typedef {number[]} lab CIELAB perceptual Lightness, a* red/green, b* blue/yellow.
*
* Components range (D65): 0 <= l <= 1; -0.86183 <= a <= 0.98234; -1.0786 <= b <= 0.94478;
*
Expand All @@ -18,6 +18,9 @@ import { setAlpha } from "./utils.js";
export const D65 = [0.3127 / 0.329, 1, (1 - 0.3127 - 0.329) / 0.329];
export const D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];

// ε = 6^3 / 29^3 = 0.008856
// κ = 29^3 / 3^3 = 903.2962963
// 903.2962963 / 116 = 7.787037
function fromLabValueToXYZValue(val, white) {
const pow = val ** 3;
return (pow > 0.008856 ? pow : (val - 16 / 116) / 7.787037) * white;
Expand All @@ -28,18 +31,14 @@ function fromXYZValueToLabValue(val, white) {
return val > 0.008856 ? Math.cbrt(val) : 7.787037 * val + 16 / 116;
}

/**
* Updates a color based on Lab values and alpha.
* @alias module:pex-color.fromLab
* @param {import("./color.js").color} color
* @param {number} l
* @param {number} a
* @param {number} b
* @param {number} α
* @param {Array} illuminant
* @returns {import("./color.js").color}
*/
export function fromLab(color, l, a, b, α, illuminant = D65) {
export function fromLab(
color,
l,
a,
b,
α,
{ illuminant = D50, fromXYZ = fromXYZD50 } = {},
) {
const y = (l + 0.16) / 1.16;

return fromXYZ(
Expand All @@ -51,15 +50,11 @@ export function fromLab(color, l, a, b, α, illuminant = D65) {
);
}

/**
* Returns a Lab representation of a given color.
* @alias module:pex-color.toLab
* @param {import("./color.js").color} color
* @param {Array} out
* @param {Array} illuminant
* @returns {lab}
*/
export function toLab(color, out = [], illuminant = D65) {
export function toLab(
color,
out = [],
{ illuminant = D50, toXYZ = toXYZD50 } = {},
) {
const xyz = toXYZ(color);

const x = fromXYZValueToLabValue(xyz[0], illuminant[0]);
Expand All @@ -72,3 +67,51 @@ export function toLab(color, out = [], illuminant = D65) {

return setAlpha(out, color[3]);
}

/**
* Updates a color based on Lab values and alpha using D50 standard illuminant.
* @alias module:pex-color.fromLabD50
* @param {import("./color.js").color} color
* @param {number} l
* @param {number} a
* @param {number} b
* @param {number} α
* @returns {import("./color.js").color}
*/
export function fromLabD50(color, l, a, b, α) {
return fromLab(color, l, a, b, α, { illuminant: D50, fromXYZ: fromXYZD50 });
}
/**
* Returns a Lab representation of a given color using D50 standard illuminant.
* @alias module:pex-color.toLabD50
* @param {import("./color.js").color} color
* @param {Array} out
* @returns {lab}
*/
export function toLabD50(color, out = []) {
return toLab(color, out, { illuminant: D50, toXYZ: toXYZD50 });
}

/**
* Updates a color based on Lab values and alpha using D65 standard illuminant.
* @alias module:pex-color.fromLabD65
* @param {import("./color.js").color} color
* @param {number} l
* @param {number} a
* @param {number} b
* @param {number} α
* @returns {import("./color.js").color}
*/
export function fromLabD65(color, l, a, b, α) {
return fromLab(color, l, a, b, α, { illuminant: D65, fromXYZ: fromXYZD65 });
}
/**
* Returns a Lab representation of a given color using D65 standard illuminant.
* @alias module:pex-color.toLabD65
* @param {import("./color.js").color} color
* @param {Array} out
* @returns {lab}
*/
export function toLabD65(color, out = []) {
return toLab(color, out, { illuminant: D65, toXYZ: toXYZD65 });
}
6 changes: 3 additions & 3 deletions lchuv.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toXYZ, fromXYZ } from "./xyz.js";
import { toXYZD65, fromXYZD65 } from "./xyz.js";
import { luvToXyz, lchToLuv, luvToLch, xyzToLuv, setAlpha } from "./utils.js";

/**
Expand All @@ -19,7 +19,7 @@ import { luvToXyz, lchToLuv, luvToLch, xyzToLuv, setAlpha } from "./utils.js";
* @returns {import("./color.js").color}
*/
export function fromLCHuv(color, l, c, h, a) {
return fromXYZ(color, ...luvToXyz(lchToLuv([l, c, h])), a);
return fromXYZD65(color, ...luvToXyz(lchToLuv([l, c, h])), a);
}

/**
Expand All @@ -30,6 +30,6 @@ export function fromLCHuv(color, l, c, h, a) {
* @returns {lchuv}
*/
export function toLCHuv([r, g, b, a], out = []) {
[out[0], out[1], out[2]] = luvToLch(xyzToLuv(toXYZ([r, g, b])));
[out[0], out[1], out[2]] = luvToLch(xyzToLuv(toXYZD65([r, g, b])));
return setAlpha(out, a);
}
37 changes: 25 additions & 12 deletions utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/**
* @module utils
*/
/** @module utils */

export const setAlpha = (color, a) => {
if (a !== undefined) color[3] = a;
Expand Down Expand Up @@ -33,22 +31,37 @@ export const floorArray = (color, precision = 5) => {

export const TMP = [0, 0, 0];

// HSLuv
// https://github.com/hsluv/hsluv/tree/master/haxe/src/hsluv
export const L_EPSILON = 1e-10;

export const m = [
// XYZ
// https://github.com/hsluv/hsluv-javascript/blob/14b49e6cf9a9137916096b8487a5372626b57ba4/src/hsluv.ts#L8-L16
export const mXYZToLinearsRGBD65 = [
[3.240969941904521, -1.537383177570093, -0.498610760293],
[-0.96924363628087, 1.87596750150772, 0.041555057407175],
[0.055630079696993, -0.20397695888897, 1.056971514242878],
];

export const minv = [
// https://github.com/hsluv/hsluv-javascript/blob/14b49e6cf9a9137916096b8487a5372626b57ba4/src/hsluv.ts#L152-L154
export const mLinearsRGBToXYZD65 = [
[0.41239079926595, 0.35758433938387, 0.18048078840183],
[0.21263900587151, 0.71516867876775, 0.072192315360733],
[0.019330818715591, 0.11919477979462, 0.95053215224966],
];

// https://github.com/Evercoder/culori/tree/main/src/xyz50
export const mXYZToLinearsRGBD50 = [
[3.1341359569958707, 1.6173863321612538, 0.4906619460083532],
[-0.978795502912089, 1.916254567259524, 0.03344273116131949],
[0.07195537988411677, 0.2289768264158322, 1.405386058324125],
];
export const mLinearsRGBToXYZD50 = [
[0.436065742824811, 0.3851514688337912, 0.14307845442264197],
[0.22249319175623702, 0.7168870538238823, 0.06061979053616537],
[0.013923904500943465, 0.09708128566574634, 0.7140993584005155],
];

// HSLuv
// https://github.com/hsluv/hsluv-javascript/blob/main/src/hsluv.ts
export const L_EPSILON = 1e-10;

const REF_U = 0.19783000664283;
const REF_V = 0.46831999493879;
const KAPPA = 9.032962962;
Expand Down Expand Up @@ -110,9 +123,9 @@ export const getBounds = (L) => {
let _g = 0;
while (_g < 3) {
const c = _g++;
const m1 = m[c][0];
const m2 = m[c][1];
const m3 = m[c][2];
const m1 = mXYZToLinearsRGBD65[c][0];
const m2 = mXYZToLinearsRGBD65[c][1];
const m3 = mXYZToLinearsRGBD65[c][2];
let _g1 = 0;
while (_g1 < 2) {
const t = _g1++;
Expand Down
112 changes: 98 additions & 14 deletions xyz.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
import { linearToSrgb, srgbToLinear, m, minv, setAlpha } from "./utils.js";
import {
linearToSrgb,
srgbToLinear,
setAlpha,
mXYZToLinearsRGBD50,
mLinearsRGBToXYZD50,
mXYZToLinearsRGBD65,
mLinearsRGBToXYZD65,
} from "./utils.js";

/**
* @typedef {number[]} xyz CIE XYZ using D65 standard illuminant.
* @typedef {number[]} xyz CIE XYZ.
*
* Components range: 0 <= x <= 0.95; 0 <= y <= 1; 0 <= z <= 1.08;
* @see {@link https://en.wikipedia.org/wiki/CIE_1931_color_space}
*/

/**
* Updates a color based on XYZ values and alpha.
* @alias module:pex-color.fromXYZ
* Updates a color based on XYZ values and alpha using D50 standard illuminant.
* @alias module:pex-color.fromXYZD50
* @param {import("./color.js").color} color
* @param {number} x
* @param {number} y
* @param {number} z
* @param {number} a
* @returns {import("./color.js").color}
*/
export function fromXYZ(color, x, y, z, a) {
const r = x * m[0][0] + y * m[0][1] + z * m[0][2];
const g = x * m[1][0] + y * m[1][1] + z * m[1][2];
const b = x * m[2][0] + y * m[2][1] + z * m[2][2];
export function fromXYZD50(color, x, y, z, a) {
const r =
x * mXYZToLinearsRGBD50[0][0] -
y * mXYZToLinearsRGBD50[0][1] -
z * mXYZToLinearsRGBD50[0][2];
const g =
x * mXYZToLinearsRGBD50[1][0] +
y * mXYZToLinearsRGBD50[1][1] +
z * mXYZToLinearsRGBD50[1][2];
const b =
x * mXYZToLinearsRGBD50[2][0] -
y * mXYZToLinearsRGBD50[2][1] +
z * mXYZToLinearsRGBD50[2][2];

color[0] = linearToSrgb(r);
color[1] = linearToSrgb(g);
Expand All @@ -30,19 +47,86 @@ export function fromXYZ(color, x, y, z, a) {
}

/**
* Returns a XYZ representation of a given color.
* @alias module:pex-color.toXYZ
* Returns a XYZ representation of a given color using D50 standard illuminant.
* @alias module:pex-color.toXYZD50
* @param {import("./color.js").color} color
* @param {Array} out
* @returns {xyz}
*/
export function toXYZ([r, g, b, a], out = []) {
export function toXYZD50([r, g, b, a], out = []) {
const lr = srgbToLinear(r);
const lg = srgbToLinear(g);
const lb = srgbToLinear(b);

out[0] = lr * minv[0][0] + lg * minv[0][1] + lb * minv[0][2];
out[1] = lr * minv[1][0] + lg * minv[1][1] + lb * minv[1][2];
out[2] = lr * minv[2][0] + lg * minv[2][1] + lb * minv[2][2];
out[0] =
lr * mLinearsRGBToXYZD50[0][0] +
lg * mLinearsRGBToXYZD50[0][1] +
lb * mLinearsRGBToXYZD50[0][2];
out[1] =
lr * mLinearsRGBToXYZD50[1][0] +
lg * mLinearsRGBToXYZD50[1][1] +
lb * mLinearsRGBToXYZD50[1][2];
out[2] =
lr * mLinearsRGBToXYZD50[2][0] +
lg * mLinearsRGBToXYZD50[2][1] +
lb * mLinearsRGBToXYZD50[2][2];
return setAlpha(out, a);
}

/**
* Updates a color based on XYZ values and alpha using D65 standard illuminant.
* @alias module:pex-color.fromXYZD65
* @param {import("./color.js").color} color
* @param {number} x
* @param {number} y
* @param {number} z
* @param {number} a
* @returns {import("./color.js").color}
*/
export function fromXYZD65(color, x, y, z, a) {
const r =
x * mXYZToLinearsRGBD65[0][0] +
y * mXYZToLinearsRGBD65[0][1] +
z * mXYZToLinearsRGBD65[0][2];
const g =
x * mXYZToLinearsRGBD65[1][0] +
y * mXYZToLinearsRGBD65[1][1] +
z * mXYZToLinearsRGBD65[1][2];
const b =
x * mXYZToLinearsRGBD65[2][0] +
y * mXYZToLinearsRGBD65[2][1] +
z * mXYZToLinearsRGBD65[2][2];

color[0] = linearToSrgb(r);
color[1] = linearToSrgb(g);
color[2] = linearToSrgb(b);

return setAlpha(color, a);
}

/**
* Returns a XYZ representation of a given color using D65 standard illuminant.
* @alias module:pex-color.toXYZD65
* @param {import("./color.js").color} color
* @param {Array} out
* @returns {xyz}
*/
export function toXYZD65([r, g, b, a], out = []) {
const lr = srgbToLinear(r);
const lg = srgbToLinear(g);
const lb = srgbToLinear(b);

out[0] =
lr * mLinearsRGBToXYZD65[0][0] +
lg * mLinearsRGBToXYZD65[0][1] +
lb * mLinearsRGBToXYZD65[0][2];
out[1] =
lr * mLinearsRGBToXYZD65[1][0] +
lg * mLinearsRGBToXYZD65[1][1] +
lb * mLinearsRGBToXYZD65[1][2];
out[2] =
lr * mLinearsRGBToXYZD65[2][0] +
lg * mLinearsRGBToXYZD65[2][1] +
lb * mLinearsRGBToXYZD65[2][2];
return setAlpha(out, a);
}

0 comments on commit 0cdc911

Please sign in to comment.