Skip to content

Commit

Permalink
add color space xyz and xyz-d65 to color() function #27
Browse files Browse the repository at this point in the history
  • Loading branch information
tbela99 committed Feb 8, 2024
1 parent f4f6fa7 commit 9d35a24
Show file tree
Hide file tree
Showing 17 changed files with 395 additions and 52 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## V0.4.0

- [x] color-mix(srgb)
- [x] color(srgb, srgb-linear, display-p3, prophoto-rgb, a98-rgb, rec2020)
- [x] color(srgb, srgb-linear, display-p3, prophoto-rgb, a98-rgb, rec2020, xyz, xyz-d50)

## V0.3.0

Expand Down
75 changes: 74 additions & 1 deletion dist/index-umd-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -1375,9 +1375,44 @@
return expr;
}

// from https://www.w3.org/TR/css-color-4/multiply-matrices.js
/**
* Simple matrix (and vector) multiplication
* Warning: No error handling for incompatible dimensions!
* @author Lea Verou 2020 MIT License
*/
// A is m x n. B is n x p. product is m x p.
function multiplyMatrices(A, B) {
let m = A.length;
if (!Array.isArray(A[0])) {
// A is vector, convert to [[a, b, c, ...]]
A = [A];
}
if (!Array.isArray(B[0])) {
// B is vector, convert to [[a], [b], [c], ...]]
B = B.map((x) => [x]);
}
let p = B[0].length;
let B_cols = B[0].map((_, i) => B.map((x) => x[i])); // transpose B
let product = A.map((row) => B_cols.map((col) => {
if (!Array.isArray(row)) {
return col.reduce((a, c) => a + c * row, 0);
}
return row.reduce((a, c, i) => a + c * (col[i] || 0), 0);
}));
if (m === 1) {
product = product[0]; // Avoid [[a, b, c, ...]]
}
if (p === 1) {
return product.map((x) => x[0]); // Avoid [[a], [b], [c], ...]]
}
return product;
}

function roundWithPrecision(value, original) {
return +value.toFixed(original.toString().split('.')[1]?.length ?? 0);
}

// from https://www.w3.org/TR/css-color-4/#color-conversion-code
// srgb-linear -> srgb
// 0 <= r, g, b <= 1
Expand Down Expand Up @@ -1450,6 +1485,35 @@
});
}

function XYZ_to_sRGB(x, y, z) {
// @ts-ignore
return gam_sRGB(...XYZ_to_lin_sRGB(x, y, z));
}
function XYZ_to_lin_sRGB(x, y, z) {
// convert XYZ to linear-light sRGB
const M = [
[12831 / 3959, -329 / 214, -1974 / 3959],
[-851781 / 878810, 1648619 / 878810, 36519 / 878810],
[705 / 12673, -2585 / 12673, 705 / 667],
];
const XYZ = [x, y, z]; // convert to XYZ
return multiplyMatrices(M, XYZ).map((v, index) => roundWithPrecision(v, XYZ[index]));
}
function XYZ_D50_to_sRGB(x, y, z) {
// @ts-ignore
return gam_sRGB(...XYZ_to_lin_sRGB(...D50_to_D65(x, y, z)));
}
function D50_to_D65(x, y, z) {
// Bradford chromatic adaptation from D50 to D65
const M = [
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
];
const XYZ = [x, y, z];
return multiplyMatrices(M, XYZ).map((v, index) => roundWithPrecision(v, XYZ[index]));
}

const colorsFunc = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'device-cmyk', 'color-mix', 'color'];
function reduceNumber(val) {
val = String(+val);
Expand Down Expand Up @@ -1706,7 +1770,7 @@
case exports.EnumToken.ColorTokenType:
if (options.convertColor) {
if (token.val == 'color') {
const supportedColorSpaces = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020'];
const supportedColorSpaces = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020', 'xyz', 'xyz-d65', 'xyz-d50'];
if (token.chi[0].typ == exports.EnumToken.IdenTokenType && supportedColorSpaces.includes(token.chi[0].val.toLowerCase())) {
let values = token.chi.slice(1, 4).map((t) => {
if (t.typ == exports.EnumToken.IdenTokenType && t.val == 'none') {
Expand All @@ -1732,6 +1796,15 @@
// @ts-ignore
values = gam_sRGB(...lin_2020(...values));
break;
case 'xyz':
case 'xyz-d65':
// @ts-ignore
values = XYZ_to_sRGB(...values);
break;
case 'xyz-d50':
// @ts-ignore
values = XYZ_D50_to_sRGB(...values);
break;
}
clampValues(values, colorSpace);
let value = `#${values.reduce((acc, curr) => {
Expand Down
75 changes: 74 additions & 1 deletion dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1373,9 +1373,44 @@ function computeComponentValue(expr, values) {
return expr;
}

// from https://www.w3.org/TR/css-color-4/multiply-matrices.js
/**
* Simple matrix (and vector) multiplication
* Warning: No error handling for incompatible dimensions!
* @author Lea Verou 2020 MIT License
*/
// A is m x n. B is n x p. product is m x p.
function multiplyMatrices(A, B) {
let m = A.length;
if (!Array.isArray(A[0])) {
// A is vector, convert to [[a, b, c, ...]]
A = [A];
}
if (!Array.isArray(B[0])) {
// B is vector, convert to [[a], [b], [c], ...]]
B = B.map((x) => [x]);
}
let p = B[0].length;
let B_cols = B[0].map((_, i) => B.map((x) => x[i])); // transpose B
let product = A.map((row) => B_cols.map((col) => {
if (!Array.isArray(row)) {
return col.reduce((a, c) => a + c * row, 0);
}
return row.reduce((a, c, i) => a + c * (col[i] || 0), 0);
}));
if (m === 1) {
product = product[0]; // Avoid [[a, b, c, ...]]
}
if (p === 1) {
return product.map((x) => x[0]); // Avoid [[a], [b], [c], ...]]
}
return product;
}

function roundWithPrecision(value, original) {
return +value.toFixed(original.toString().split('.')[1]?.length ?? 0);
}

// from https://www.w3.org/TR/css-color-4/#color-conversion-code
// srgb-linear -> srgb
// 0 <= r, g, b <= 1
Expand Down Expand Up @@ -1448,6 +1483,35 @@ function lin_2020(r, g, b) {
});
}

function XYZ_to_sRGB(x, y, z) {
// @ts-ignore
return gam_sRGB(...XYZ_to_lin_sRGB(x, y, z));
}
function XYZ_to_lin_sRGB(x, y, z) {
// convert XYZ to linear-light sRGB
const M = [
[12831 / 3959, -329 / 214, -1974 / 3959],
[-851781 / 878810, 1648619 / 878810, 36519 / 878810],
[705 / 12673, -2585 / 12673, 705 / 667],
];
const XYZ = [x, y, z]; // convert to XYZ
return multiplyMatrices(M, XYZ).map((v, index) => roundWithPrecision(v, XYZ[index]));
}
function XYZ_D50_to_sRGB(x, y, z) {
// @ts-ignore
return gam_sRGB(...XYZ_to_lin_sRGB(...D50_to_D65(x, y, z)));
}
function D50_to_D65(x, y, z) {
// Bradford chromatic adaptation from D50 to D65
const M = [
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
];
const XYZ = [x, y, z];
return multiplyMatrices(M, XYZ).map((v, index) => roundWithPrecision(v, XYZ[index]));
}

const colorsFunc = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'device-cmyk', 'color-mix', 'color'];
function reduceNumber(val) {
val = String(+val);
Expand Down Expand Up @@ -1704,7 +1768,7 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer,
case exports.EnumToken.ColorTokenType:
if (options.convertColor) {
if (token.val == 'color') {
const supportedColorSpaces = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020'];
const supportedColorSpaces = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020', 'xyz', 'xyz-d65', 'xyz-d50'];
if (token.chi[0].typ == exports.EnumToken.IdenTokenType && supportedColorSpaces.includes(token.chi[0].val.toLowerCase())) {
let values = token.chi.slice(1, 4).map((t) => {
if (t.typ == exports.EnumToken.IdenTokenType && t.val == 'none') {
Expand All @@ -1730,6 +1794,15 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer,
// @ts-ignore
values = gam_sRGB(...lin_2020(...values));
break;
case 'xyz':
case 'xyz-d65':
// @ts-ignore
values = XYZ_to_sRGB(...values);
break;
case 'xyz-d50':
// @ts-ignore
values = XYZ_D50_to_sRGB(...values);
break;
}
clampValues(values, colorSpace);
let value = `#${values.reduce((acc, curr) => {
Expand Down
12 changes: 11 additions & 1 deletion dist/lib/renderer/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import '../parser/parse.js';
import { isColor, isNewLine } from '../parser/utils/syntax.js';
import { parseRelativeColor } from './utils/relativecolor.js';
import { gam_sRGB, lin_2020, lin_a98rgb, lin_ProPhoto } from './utils/colorspace/rgb.js';
import { XYZ_D50_to_sRGB, XYZ_to_sRGB } from './utils/colorspace/xyz.js';

const colorsFunc = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'device-cmyk', 'color-mix', 'color'];
function reduceNumber(val) {
Expand Down Expand Up @@ -266,7 +267,7 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer,
case EnumToken.ColorTokenType:
if (options.convertColor) {
if (token.val == 'color') {
const supportedColorSpaces = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020'];
const supportedColorSpaces = ['srgb', 'srgb-linear', 'display-p3', 'prophoto-rgb', 'a98-rgb', 'rec2020', 'xyz', 'xyz-d65', 'xyz-d50'];
if (token.chi[0].typ == EnumToken.IdenTokenType && supportedColorSpaces.includes(token.chi[0].val.toLowerCase())) {
let values = token.chi.slice(1, 4).map((t) => {
if (t.typ == EnumToken.IdenTokenType && t.val == 'none') {
Expand All @@ -292,6 +293,15 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer,
// @ts-ignore
values = gam_sRGB(...lin_2020(...values));
break;
case 'xyz':
case 'xyz-d65':
// @ts-ignore
values = XYZ_to_sRGB(...values);
break;
case 'xyz-d50':
// @ts-ignore
values = XYZ_D50_to_sRGB(...values);
break;
}
clampValues(values, colorSpace);
let value = `#${values.reduce((acc, curr) => {
Expand Down
39 changes: 0 additions & 39 deletions dist/lib/renderer/utils/colorspace.js

This file was deleted.

5 changes: 2 additions & 3 deletions dist/lib/renderer/utils/colorspace/rgb.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
function roundWithPrecision(value, original) {
return +value.toFixed(original.toString().split('.')[1]?.length ?? 0);
}
import { roundWithPrecision } from './utils/round.js';

// from https://www.w3.org/TR/css-color-4/#color-conversion-code
// srgb-linear -> srgb
// 0 <= r, g, b <= 1
Expand Down
35 changes: 35 additions & 0 deletions dist/lib/renderer/utils/colorspace/utils/matrix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// from https://www.w3.org/TR/css-color-4/multiply-matrices.js
/**
* Simple matrix (and vector) multiplication
* Warning: No error handling for incompatible dimensions!
* @author Lea Verou 2020 MIT License
*/
// A is m x n. B is n x p. product is m x p.
function multiplyMatrices(A, B) {
let m = A.length;
if (!Array.isArray(A[0])) {
// A is vector, convert to [[a, b, c, ...]]
A = [A];
}
if (!Array.isArray(B[0])) {
// B is vector, convert to [[a], [b], [c], ...]]
B = B.map((x) => [x]);
}
let p = B[0].length;
let B_cols = B[0].map((_, i) => B.map((x) => x[i])); // transpose B
let product = A.map((row) => B_cols.map((col) => {
if (!Array.isArray(row)) {
return col.reduce((a, c) => a + c * row, 0);
}
return row.reduce((a, c, i) => a + c * (col[i] || 0), 0);
}));
if (m === 1) {
product = product[0]; // Avoid [[a, b, c, ...]]
}
if (p === 1) {
return product.map((x) => x[0]); // Avoid [[a], [b], [c], ...]]
}
return product;
}

export { multiplyMatrices };
5 changes: 5 additions & 0 deletions dist/lib/renderer/utils/colorspace/utils/round.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function roundWithPrecision(value, original) {
return +value.toFixed(original.toString().split('.')[1]?.length ?? 0);
}

export { roundWithPrecision };
34 changes: 34 additions & 0 deletions dist/lib/renderer/utils/colorspace/xyz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { multiplyMatrices } from './utils/matrix.js';
import { roundWithPrecision } from './utils/round.js';
import { gam_sRGB } from './rgb.js';

function XYZ_to_sRGB(x, y, z) {
// @ts-ignore
return gam_sRGB(...XYZ_to_lin_sRGB(x, y, z));
}
function XYZ_to_lin_sRGB(x, y, z) {
// convert XYZ to linear-light sRGB
const M = [
[12831 / 3959, -329 / 214, -1974 / 3959],
[-851781 / 878810, 1648619 / 878810, 36519 / 878810],
[705 / 12673, -2585 / 12673, 705 / 667],
];
const XYZ = [x, y, z]; // convert to XYZ
return multiplyMatrices(M, XYZ).map((v, index) => roundWithPrecision(v, XYZ[index]));
}
function XYZ_D50_to_sRGB(x, y, z) {
// @ts-ignore
return gam_sRGB(...XYZ_to_lin_sRGB(...D50_to_D65(x, y, z)));
}
function D50_to_D65(x, y, z) {
// Bradford chromatic adaptation from D50 to D65
const M = [
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
];
const XYZ = [x, y, z];
return multiplyMatrices(M, XYZ).map((v, index) => roundWithPrecision(v, XYZ[index]));
}

export { D50_to_D65, XYZ_D50_to_sRGB, XYZ_to_lin_sRGB, XYZ_to_sRGB };
2 changes: 2 additions & 0 deletions src/@types/token.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ export declare interface ImportantToken extends BaseToken {
}

export declare type ColorKind = 'lit' | 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla' | 'hwb' | 'device-cmyk';
// xyz-d65 is an alias for xyz
// display-p3 is an alias for srgb
export declare type ColorSpace =
'srgb' | "prophoto-rgb" | "a98-rgb" | 'rec2020'
| 'display-p3'
Expand Down
Loading

0 comments on commit 9d35a24

Please sign in to comment.