@texel/color 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/gamut.js +4 -2
- package/test/almost-equal.js +0 -15
- package/test/banner.png +0 -0
- package/test/bench-colorjs.js +0 -227
- package/test/bench-culori.js +0 -148
- package/test/bench-node.js +0 -51
- package/test/bench-size.js +0 -12
- package/test/canvas-graph.js +0 -212
- package/test/check-gamut-epsilon.js +0 -136
- package/test/colorjs-fn.js +0 -34
- package/test/example-interpolation.js +0 -107
- package/test/logo.js +0 -112
- package/test/logo.png +0 -0
- package/test/profiles/DisplayP3.icc +0 -0
- package/test/spaces/hsl.js +0 -87
- package/test/spaces/lab.js +0 -60
- package/test/test-colorjs.js +0 -86
- package/test/test-other-spaces.js +0 -115
- package/test/test.js +0 -451
- package/tools/__pycache__/calc_oklab_matrices.cpython-311.pyc +0 -0
- package/tools/calc_oklab_matrices.py +0 -233
- package/tools/print_matrices.py +0 -509
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// When gamut mapping with OKLCH approximation,
|
|
2
|
-
// the resulting points do not always lie exactly in gamut.
|
|
3
|
-
// The same may be true of OKHSL to RGB spaces.
|
|
4
|
-
// Let's figure out how far away they are:
|
|
5
|
-
// if a given point is under this threshold, gamut mapping
|
|
6
|
-
// will be redundant as it will just produce the same epsilon.
|
|
7
|
-
// This value is used in gamut.js as the RGB_CLIP_EPSILON
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
clampedRGB,
|
|
11
|
-
convert,
|
|
12
|
-
degToRad,
|
|
13
|
-
findCuspOKLCH,
|
|
14
|
-
findGamutIntersectionOKLCH,
|
|
15
|
-
gamutMapOKLCH,
|
|
16
|
-
isRGBInGamut,
|
|
17
|
-
lerp,
|
|
18
|
-
MapToCuspL,
|
|
19
|
-
OKLCH,
|
|
20
|
-
sRGB,
|
|
21
|
-
sRGBGamut,
|
|
22
|
-
sRGBLinear,
|
|
23
|
-
} from "../src/index.js";
|
|
24
|
-
|
|
25
|
-
const gamut = sRGBGamut;
|
|
26
|
-
const target = gamut.space.base; // linear form
|
|
27
|
-
|
|
28
|
-
// slice the plane into a square
|
|
29
|
-
const slices = 100;
|
|
30
|
-
let avgDelta = 0;
|
|
31
|
-
let avgCount = 0;
|
|
32
|
-
let minDelta = Infinity;
|
|
33
|
-
let maxDelta = -Infinity;
|
|
34
|
-
|
|
35
|
-
// a very small number which still catches many gamut-mapped points
|
|
36
|
-
// but produces very little difference in practical and visual results
|
|
37
|
-
const RGB_CLIP_EPSILON = 0.0000001;
|
|
38
|
-
let totalPointsOutOfGamut = 0;
|
|
39
|
-
let totalPointsUnderEpsilon = 0;
|
|
40
|
-
let totalPointsUnderEpsilonBeforeMapping = 0;
|
|
41
|
-
|
|
42
|
-
// this particular hue is a little funky
|
|
43
|
-
// https://github.com/color-js/color.js/issues/81
|
|
44
|
-
// it produces out of gamut sRGB, however, the oklab gamut approximation seems to handle it fine
|
|
45
|
-
const hue = 264.1;
|
|
46
|
-
const lightness = 0.4;
|
|
47
|
-
for (let chroma = 0.22; chroma < 0.285; chroma += 0.001) {
|
|
48
|
-
const oklch = [lightness, chroma, hue];
|
|
49
|
-
const rgb = convert(oklch, OKLCH, sRGBLinear);
|
|
50
|
-
if (!isRGBInGamut(rgb, 0)) {
|
|
51
|
-
const mappedOKLCH = gamutMapWithoutClipOKLCH(oklch);
|
|
52
|
-
const mappedRGB = convert(mappedOKLCH, OKLCH, sRGBLinear);
|
|
53
|
-
const delta = clipDelta(mappedRGB);
|
|
54
|
-
if (!delta.every((n) => n == 0)) console.log("hue", hue, "delta", delta);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// test all hue planes Nº difference apart
|
|
59
|
-
// we will see some gamut mapped points still do not lie exactly in gamut
|
|
60
|
-
for (let H = 0; H < 360; H += 0.5) {
|
|
61
|
-
for (let y = 0; y < slices; y++) {
|
|
62
|
-
for (let x = 0; x < slices; x++) {
|
|
63
|
-
const u = x / (slices - 1);
|
|
64
|
-
const v = y / (slices - 1);
|
|
65
|
-
const L = 1 - v;
|
|
66
|
-
const C = u * 0.4;
|
|
67
|
-
|
|
68
|
-
// try conversion
|
|
69
|
-
let rgbl = convert([L, C, H], OKLCH, target);
|
|
70
|
-
|
|
71
|
-
// not exactly in space
|
|
72
|
-
if (!isRGBInGamut(rgbl, 0)) {
|
|
73
|
-
// check epsilons
|
|
74
|
-
if (isRGBInGamut(rgbl, RGB_CLIP_EPSILON)) {
|
|
75
|
-
totalPointsUnderEpsilonBeforeMapping++;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const oklch = gamutMapWithoutClipOKLCH([L, C, H]);
|
|
79
|
-
rgbl = convert(oklch, OKLCH, target);
|
|
80
|
-
|
|
81
|
-
const [dr, dg, db] = clipDelta(rgbl);
|
|
82
|
-
const avg = (dr + dg + db) / 3;
|
|
83
|
-
const min = Math.min(dr, dg, db);
|
|
84
|
-
const max = Math.max(dr, dg, db);
|
|
85
|
-
avgDelta += avg;
|
|
86
|
-
avgCount++;
|
|
87
|
-
minDelta = Math.min(min, minDelta);
|
|
88
|
-
maxDelta = Math.max(max, maxDelta);
|
|
89
|
-
|
|
90
|
-
totalPointsOutOfGamut++;
|
|
91
|
-
if (isRGBInGamut(rgbl, RGB_CLIP_EPSILON)) {
|
|
92
|
-
totalPointsUnderEpsilon++;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function clipDelta(rgb) {
|
|
100
|
-
const clipped = clampedRGB(rgb);
|
|
101
|
-
const dr = Math.abs(rgb[0] - clipped[0]);
|
|
102
|
-
const dg = Math.abs(rgb[1] - clipped[1]);
|
|
103
|
-
const db = Math.abs(rgb[2] - clipped[2]);
|
|
104
|
-
return [dr, dg, db];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function gamutMapWithoutClipOKLCH(oklch) {
|
|
108
|
-
const [L, C, H] = oklch;
|
|
109
|
-
// we aren't in gamut, so let's map toward it
|
|
110
|
-
const hueAngle = degToRad(H);
|
|
111
|
-
const aNorm = Math.cos(hueAngle);
|
|
112
|
-
const bNorm = Math.sin(hueAngle);
|
|
113
|
-
|
|
114
|
-
const out = [L, C, H];
|
|
115
|
-
// choose our strategy
|
|
116
|
-
const cusp = findCuspOKLCH(aNorm, bNorm, gamut);
|
|
117
|
-
const LTarget = MapToCuspL(out, cusp);
|
|
118
|
-
|
|
119
|
-
let t = findGamutIntersectionOKLCH(aNorm, bNorm, L, C, LTarget, cusp, gamut);
|
|
120
|
-
out[0] = lerp(LTarget, L, t);
|
|
121
|
-
out[1] *= t;
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
avgDelta /= avgCount;
|
|
126
|
-
|
|
127
|
-
console.log("Min Epsilon:", minDelta);
|
|
128
|
-
console.log("Max Epsilon:", maxDelta);
|
|
129
|
-
console.log("Average Epsilon:", avgDelta);
|
|
130
|
-
console.log("Compare against epsilon:", RGB_CLIP_EPSILON);
|
|
131
|
-
console.log("Total points out of gamut:", totalPointsOutOfGamut);
|
|
132
|
-
console.log(
|
|
133
|
-
"Total points under epsilon (before map):",
|
|
134
|
-
totalPointsUnderEpsilonBeforeMapping
|
|
135
|
-
);
|
|
136
|
-
console.log("Total points under epsilon (after map):", totalPointsUnderEpsilon);
|
package/test/colorjs-fn.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import * as ColorJSFn from "colorjs.io/fn";
|
|
2
|
-
import { listColorSpaces } from "../src/index.js";
|
|
3
|
-
|
|
4
|
-
// Colorjs.io uses some different naming conventions than @texel/color
|
|
5
|
-
const getColorJSID = (name) => {
|
|
6
|
-
return name
|
|
7
|
-
.replace("display-", "")
|
|
8
|
-
.replace(/^xyz$/, "xyz-d65")
|
|
9
|
-
.replace("a98-rgb", "a98rgb")
|
|
10
|
-
.replace("prophoto-rgb", "prophoto");
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// returns a list of ColorJS.io space IDs that are supported by @texel/color
|
|
14
|
-
// okhsl/okhsv is skipped due to it not being included in this npm version of colorjs.io
|
|
15
|
-
export function getSupportedColorJSSpaces() {
|
|
16
|
-
const spaceFns = Object.values(ColorJSFn);
|
|
17
|
-
const spaces = listColorSpaces().filter((s) => !/ok(hsv|hsl)/i.test(s.id));
|
|
18
|
-
return spaces.map((space) => {
|
|
19
|
-
const cjsID = getColorJSID(space.id);
|
|
20
|
-
const colorJSSpace = spaceFns.find((f) => f.id === cjsID);
|
|
21
|
-
if (!colorJSSpace)
|
|
22
|
-
throw new Error(`expected ${cjsID} to exist in colorjs.io/fn`);
|
|
23
|
-
return {
|
|
24
|
-
space,
|
|
25
|
-
colorJSSpace,
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Register all spaces
|
|
31
|
-
const spaces = getSupportedColorJSSpaces().map((s) => s.colorJSSpace);
|
|
32
|
-
for (let space of spaces) {
|
|
33
|
-
ColorJSFn.ColorSpace.register(space);
|
|
34
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import canvasSketch from "canvas-sketch";
|
|
2
|
-
import {
|
|
3
|
-
convert,
|
|
4
|
-
DisplayP3Gamut,
|
|
5
|
-
gamutMapOKLCH,
|
|
6
|
-
lerp,
|
|
7
|
-
lerpAngle,
|
|
8
|
-
OKLab,
|
|
9
|
-
OKLCH,
|
|
10
|
-
serialize,
|
|
11
|
-
sRGB,
|
|
12
|
-
sRGBGamut,
|
|
13
|
-
} from "../src/index.js";
|
|
14
|
-
|
|
15
|
-
const settings = {
|
|
16
|
-
dimensions: [2048, 512],
|
|
17
|
-
attributes: {
|
|
18
|
-
// comment this out if you want sRGB output
|
|
19
|
-
colorSpace: "display-p3",
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const mix = (() => {
|
|
24
|
-
const tmpA = [0, 0, 0];
|
|
25
|
-
const tmpB = [0, 0, 0];
|
|
26
|
-
|
|
27
|
-
// you can decide whether you'd like to interpolate in
|
|
28
|
-
// OKLab, OKLCH or another space (you may need to adjust interpolation
|
|
29
|
-
// if you use a custom space)
|
|
30
|
-
const interpolationSpace = OKLCH;
|
|
31
|
-
|
|
32
|
-
// e.g. mix({ space, coords }, { space, coords }, 0.5, sRGB)
|
|
33
|
-
return (a, b, t, outputSpace = sRGB, out = [0, 0, 0]) => {
|
|
34
|
-
// bring both spaces into the shared interpolation space
|
|
35
|
-
convert(a.coords, a.space, interpolationSpace, tmpA);
|
|
36
|
-
convert(b.coords, b.space, interpolationSpace, tmpB);
|
|
37
|
-
|
|
38
|
-
// now do interpolation
|
|
39
|
-
out[0] = lerp(tmpA[0], tmpB[0], t);
|
|
40
|
-
out[1] = lerp(tmpA[1], tmpB[1], t);
|
|
41
|
-
if (interpolationSpace.id === "oklch") {
|
|
42
|
-
// for cylindrical spaces, use a circular interpolation for Hue parameter
|
|
43
|
-
// note if you decide to use a custom space like HSL as your interpolation space,
|
|
44
|
-
// you'll have to use the first parameter instead...
|
|
45
|
-
out[2] = lerpAngle(tmpA[2], tmpB[2], t);
|
|
46
|
-
} else {
|
|
47
|
-
// otherwise can use a regular linear interpolation
|
|
48
|
-
out[2] = lerp(tmpA[2], tmpB[2], t);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// make sure we convert from interpolation space to the target output space
|
|
52
|
-
convert(out, interpolationSpace, outputSpace, out);
|
|
53
|
-
return out;
|
|
54
|
-
};
|
|
55
|
-
})();
|
|
56
|
-
|
|
57
|
-
// utility to create a ramp between two 'colors' as { space, coords }
|
|
58
|
-
function ramp(a, b, steps = 4, outputSpace = sRGB) {
|
|
59
|
-
return Array(steps)
|
|
60
|
-
.fill()
|
|
61
|
-
.map((_, i, lst) => {
|
|
62
|
-
const t = lst.length <= 1 ? 0 : i / (lst.length - 1);
|
|
63
|
-
return mix(a, b, t, outputSpace);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const sketch = ({ context }) => {
|
|
68
|
-
const { colorSpace = "srgb" } = context.getContextAttributes();
|
|
69
|
-
const gamut = colorSpace === "srgb" ? sRGBGamut : DisplayP3Gamut;
|
|
70
|
-
|
|
71
|
-
return ({ context, width, height }) => {
|
|
72
|
-
context.fillStyle = "white";
|
|
73
|
-
context.fillRect(0, 0, width, height);
|
|
74
|
-
|
|
75
|
-
const A = {
|
|
76
|
-
space: sRGB,
|
|
77
|
-
coords: [0, 0, 1],
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const B = {
|
|
81
|
-
space: OKLCH,
|
|
82
|
-
coords: [0.55, 0.4, 30],
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const slices = 16;
|
|
86
|
-
const sliceWidth = width / slices;
|
|
87
|
-
|
|
88
|
-
// the output space is whatever the canvas expects (sRGB or DisplayP3)
|
|
89
|
-
const outputSpace = gamut.space;
|
|
90
|
-
|
|
91
|
-
// create a ramp of colors in OKLCH
|
|
92
|
-
// then gamut map them to the outputSpace
|
|
93
|
-
const colors = ramp(A, B, slices, OKLCH).map((oklch) =>
|
|
94
|
-
gamutMapOKLCH(oklch, gamut, outputSpace)
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
for (let i = 0; i < slices; i++) {
|
|
98
|
-
const color = colors[i];
|
|
99
|
-
|
|
100
|
-
// turn the color (now in outputSpace) into a context string
|
|
101
|
-
context.fillStyle = serialize(color, outputSpace);
|
|
102
|
-
context.fillRect(i * sliceWidth, 0, sliceWidth, height);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
canvasSketch(sketch, settings);
|
package/test/logo.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import {
|
|
3
|
-
ChunkType,
|
|
4
|
-
ColorType,
|
|
5
|
-
colorTypeToChannels,
|
|
6
|
-
encode,
|
|
7
|
-
encode_iCCP,
|
|
8
|
-
} from "png-tools";
|
|
9
|
-
import { deflate } from "pako";
|
|
10
|
-
import {
|
|
11
|
-
convert,
|
|
12
|
-
DisplayP3Gamut,
|
|
13
|
-
floatToByte,
|
|
14
|
-
hexToRGB,
|
|
15
|
-
OKHSLToOKLab,
|
|
16
|
-
OKLab,
|
|
17
|
-
radToDeg,
|
|
18
|
-
} from "../src/index.js";
|
|
19
|
-
|
|
20
|
-
const gamut = DisplayP3Gamut;
|
|
21
|
-
|
|
22
|
-
// or regular sRGB...
|
|
23
|
-
// const gamut = sRGBGamut;
|
|
24
|
-
|
|
25
|
-
const isLogo = process.argv.includes("--logo");
|
|
26
|
-
|
|
27
|
-
const colorType = ColorType.RGB;
|
|
28
|
-
const channels = colorTypeToChannels(colorType);
|
|
29
|
-
const depth = 8;
|
|
30
|
-
const width = isLogo ? 512 : 1024;
|
|
31
|
-
const height = isLogo ? 512 : 512;
|
|
32
|
-
const uint8 = new Uint8ClampedArray(width * height * channels);
|
|
33
|
-
|
|
34
|
-
let rgb = [0, 0, 0];
|
|
35
|
-
for (let y = 0; y < height; y++) {
|
|
36
|
-
for (let x = 0; x < width; x++) {
|
|
37
|
-
const u = x / width;
|
|
38
|
-
const v = y / height;
|
|
39
|
-
|
|
40
|
-
if (isLogo) {
|
|
41
|
-
const u2 = u * 2 - 1;
|
|
42
|
-
const v2 = v * 2 - 1;
|
|
43
|
-
const hueAngle = Math.atan2(v2, u2);
|
|
44
|
-
let H = radToDeg(hueAngle);
|
|
45
|
-
const hueSteps = 45 / 2;
|
|
46
|
-
H = Math.round(H / hueSteps) * hueSteps;
|
|
47
|
-
|
|
48
|
-
const S = 1;
|
|
49
|
-
const L = 0.7;
|
|
50
|
-
|
|
51
|
-
const oklab = OKHSLToOKLab([H, S, L], gamut);
|
|
52
|
-
convert(oklab, OKLab, gamut.space, rgb);
|
|
53
|
-
} else {
|
|
54
|
-
const margin = 0.1 * Math.min(width, height);
|
|
55
|
-
if (
|
|
56
|
-
x >= margin &&
|
|
57
|
-
y >= margin &&
|
|
58
|
-
x < width - margin &&
|
|
59
|
-
y < height - margin
|
|
60
|
-
) {
|
|
61
|
-
const u2 = inverseLerp(margin, width - margin, x);
|
|
62
|
-
const v2 = inverseLerp(margin, height - margin, y);
|
|
63
|
-
const hueSteps = 8;
|
|
64
|
-
const H = (Math.floor(u2 * hueSteps) / hueSteps) * 360;
|
|
65
|
-
const satSteps = 4;
|
|
66
|
-
const S = 1 - Math.floor(v2 * satSteps) / satSteps;
|
|
67
|
-
const L = 0.75;
|
|
68
|
-
const oklab = OKHSLToOKLab([H, S, L], gamut);
|
|
69
|
-
convert(oklab, OKLab, gamut.space, rgb);
|
|
70
|
-
} else {
|
|
71
|
-
hexToRGB("#e9e3d5", rgb);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const idx = x + y * width;
|
|
76
|
-
const [r, g, b] = rgb.map((f) => floatToByte(f));
|
|
77
|
-
uint8[idx * channels + 0] = r;
|
|
78
|
-
uint8[idx * channels + 1] = g;
|
|
79
|
-
uint8[idx * channels + 2] = b;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// optional color profile chunk
|
|
84
|
-
let iCCP = null;
|
|
85
|
-
if (gamut == DisplayP3Gamut) {
|
|
86
|
-
const iccFile = await fs.readFile("test/profiles/DisplayP3.icc");
|
|
87
|
-
const name = "Display P3";
|
|
88
|
-
const data = deflate(iccFile);
|
|
89
|
-
iCCP = {
|
|
90
|
-
type: ChunkType.iCCP,
|
|
91
|
-
data: encode_iCCP({ name, data }),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const png = encode(
|
|
96
|
-
{
|
|
97
|
-
width,
|
|
98
|
-
height,
|
|
99
|
-
data: uint8,
|
|
100
|
-
colorType,
|
|
101
|
-
depth,
|
|
102
|
-
ancillary: [iCCP].filter(Boolean),
|
|
103
|
-
},
|
|
104
|
-
deflate
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
fs.writeFile(`test/${isLogo ? "logo" : "banner"}.png`, png);
|
|
108
|
-
|
|
109
|
-
function inverseLerp(min, max, t) {
|
|
110
|
-
if (Math.abs(min - max) < Number.EPSILON) return 0;
|
|
111
|
-
else return (t - min) / (max - min);
|
|
112
|
-
}
|
package/test/logo.png
DELETED
|
Binary file
|
|
Binary file
|
package/test/spaces/hsl.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// HSL space (hue, saturation, lightness within sRGB gamut)
|
|
2
|
-
|
|
3
|
-
// Reference:
|
|
4
|
-
// https://github.com/color-js/color.js/blob/cfe55d358adb6c2e23c8a897282adf42904fd32d/src/spaces/hsl.js
|
|
5
|
-
|
|
6
|
-
import { sRGB, sRGBLinear } from "../../src/index.js";
|
|
7
|
-
|
|
8
|
-
export const HSL = {
|
|
9
|
-
id: "hsl",
|
|
10
|
-
// Note: @texel/color currently only supports 1-level depth for color spaces
|
|
11
|
-
// (for performance & memory reasons) - so our base must be one without another base space
|
|
12
|
-
base: sRGBLinear,
|
|
13
|
-
// Adapted from https://drafts.csswg.org/css-color-4/better-rgbToHsl.js
|
|
14
|
-
fromBase: (rgb, out = [0, 0, 0]) => {
|
|
15
|
-
// from sRGBLinear (this space's base) to sRGB (for HSL conversion)
|
|
16
|
-
sRGB.fromBase(rgb, out);
|
|
17
|
-
const r = out[0];
|
|
18
|
-
const g = out[1];
|
|
19
|
-
const b = out[2];
|
|
20
|
-
let max = Math.max(r, g, b);
|
|
21
|
-
let min = Math.min(r, g, b);
|
|
22
|
-
let h = 0,
|
|
23
|
-
s = 0,
|
|
24
|
-
l = (min + max) / 2;
|
|
25
|
-
let d = max - min;
|
|
26
|
-
|
|
27
|
-
if (d !== 0) {
|
|
28
|
-
s = l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l);
|
|
29
|
-
|
|
30
|
-
switch (max) {
|
|
31
|
-
case r:
|
|
32
|
-
h = (g - b) / d + (g < b ? 6 : 0);
|
|
33
|
-
break;
|
|
34
|
-
case g:
|
|
35
|
-
h = (b - r) / d + 2;
|
|
36
|
-
break;
|
|
37
|
-
case b:
|
|
38
|
-
h = (r - g) / d + 4;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
h = h * 60;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Very out of gamut colors can produce negative saturation
|
|
45
|
-
// If so, just rotate the hue by 180 and use a positive saturation
|
|
46
|
-
// see https://github.com/w3c/csswg-drafts/issues/9222
|
|
47
|
-
if (s < 0) {
|
|
48
|
-
h += 180;
|
|
49
|
-
s = Math.abs(s);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (h >= 360) {
|
|
53
|
-
h -= 360;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
out[0] = h;
|
|
57
|
-
out[1] = s * 100;
|
|
58
|
-
out[2] = l * 100;
|
|
59
|
-
return out;
|
|
60
|
-
},
|
|
61
|
-
// Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
|
|
62
|
-
toBase: (hsl, out = [0, 0, 0]) => {
|
|
63
|
-
let h = hsl[0];
|
|
64
|
-
let s = hsl[1];
|
|
65
|
-
let l = hsl[2];
|
|
66
|
-
h = h % 360;
|
|
67
|
-
if (h < 0) {
|
|
68
|
-
h += 360;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
s /= 100;
|
|
72
|
-
l /= 100;
|
|
73
|
-
|
|
74
|
-
const a = s * Math.min(l, 1 - l);
|
|
75
|
-
out[0] = f(h, l, a, 0);
|
|
76
|
-
out[1] = f(h, l, a, 8);
|
|
77
|
-
out[2] = f(h, l, a, 4);
|
|
78
|
-
// from sRGB to sRGBLinear (this space's base)
|
|
79
|
-
sRGB.toBase(out, out);
|
|
80
|
-
return out;
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
function f(h, l, a, n) {
|
|
85
|
-
let k = (n + h / 30) % 12;
|
|
86
|
-
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
87
|
-
}
|
package/test/spaces/lab.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Lab aka CIELAB aka L*a*b* (uses a D50 WHITE_D50 point and has to be adapted)
|
|
2
|
-
// refer to CSS Color Module Level 4 Spec for more details
|
|
3
|
-
|
|
4
|
-
// Reference:
|
|
5
|
-
// https://github.com/color-js/color.js/blob/cfe55d358adb6c2e23c8a897282adf42904fd32d/src/spaces/lab.js
|
|
6
|
-
import { D50_to_D65_M, D65_to_D50_M } from "../../src/index.js";
|
|
7
|
-
|
|
8
|
-
// K * e = 2^3 = 8
|
|
9
|
-
const e = 216 / 24389; // 6^3/29^3 == (24/116)^3
|
|
10
|
-
const e3 = 24 / 116;
|
|
11
|
-
const K = 24389 / 27; // 29^3/3^3
|
|
12
|
-
const WHITE_D50 = [0.3457 / 0.3585, 1.0, (1.0 - 0.3457 - 0.3585) / 0.3585];
|
|
13
|
-
|
|
14
|
-
const fterm = (value) =>
|
|
15
|
-
value > e ? Math.cbrt(value) : (K * value + 16) / 116;
|
|
16
|
-
|
|
17
|
-
const inv = (value) =>
|
|
18
|
-
value > e3 ? Math.pow(value, 3) : (116 * value - 16) / K;
|
|
19
|
-
|
|
20
|
-
export const Lab = {
|
|
21
|
-
id: "lab",
|
|
22
|
-
adapt: {
|
|
23
|
-
// chromatic adaptation to and from D65
|
|
24
|
-
to: D50_to_D65_M,
|
|
25
|
-
from: D65_to_D50_M,
|
|
26
|
-
},
|
|
27
|
-
// Convert D50-adapted XYX to Lab
|
|
28
|
-
// CIE 15.3:2004 section 8.2.1.1
|
|
29
|
-
fromXYZ(xyz, out = [0, 0, 0]) {
|
|
30
|
-
// XYZ scaled relative to reference WHITE_D50, then modified
|
|
31
|
-
out[0] = fterm(xyz[0] / WHITE_D50[0]);
|
|
32
|
-
out[1] = fterm(xyz[1] / WHITE_D50[1]);
|
|
33
|
-
out[2] = fterm(xyz[2] / WHITE_D50[2]);
|
|
34
|
-
let L = 116 * out[1] - 16;
|
|
35
|
-
let a = 500 * (out[0] - out[1]);
|
|
36
|
-
let b = 200 * (out[1] - out[2]);
|
|
37
|
-
out[0] = L;
|
|
38
|
-
out[1] = a;
|
|
39
|
-
out[2] = b;
|
|
40
|
-
return out;
|
|
41
|
-
},
|
|
42
|
-
// Convert Lab to D50-adapted XYZ
|
|
43
|
-
// Same result as CIE 15.3:2004 Appendix D although the derivation is different
|
|
44
|
-
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
|
|
45
|
-
toXYZ(Lab, out = [0, 0, 0]) {
|
|
46
|
-
// compute f, starting with the luminance-related term
|
|
47
|
-
const L = Lab[0];
|
|
48
|
-
const a = Lab[1];
|
|
49
|
-
const b = Lab[2];
|
|
50
|
-
out[1] = (L + 16) / 116;
|
|
51
|
-
out[0] = a / 500 + out[1];
|
|
52
|
-
out[2] = out[1] - b / 200;
|
|
53
|
-
|
|
54
|
-
// compute xyz and scale by WHITE_D50
|
|
55
|
-
out[0] = inv(out[0]) * WHITE_D50[0];
|
|
56
|
-
out[1] = (L > 8 ? Math.pow((L + 16) / 116, 3) : L / K) * WHITE_D50[1];
|
|
57
|
-
out[2] = inv(out[2]) * WHITE_D50[2];
|
|
58
|
-
return out;
|
|
59
|
-
},
|
|
60
|
-
};
|
package/test/test-colorjs.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import test from "tape";
|
|
2
|
-
import Color from "colorjs.io";
|
|
3
|
-
import arrayAlmostEqual from "./almost-equal.js";
|
|
4
|
-
import { convert } from "../src/index.js";
|
|
5
|
-
import { getSupportedColorJSSpaces } from "./colorjs-fn.js";
|
|
6
|
-
|
|
7
|
-
test("should approximately match colorjs.io conversions", async (t) => {
|
|
8
|
-
// note: we skip okhsv/hsl as colorjs.io doesn't support in the current npm version
|
|
9
|
-
const spaces = getSupportedColorJSSpaces();
|
|
10
|
-
const vecs = [
|
|
11
|
-
[0.12341, 0.12001, 0.05212],
|
|
12
|
-
[1, 1, 1],
|
|
13
|
-
[1, 0, 0],
|
|
14
|
-
[0, 0, 0],
|
|
15
|
-
[-0.5, -0.5, -0.5],
|
|
16
|
-
// some other inputs
|
|
17
|
-
[0.95, 1, 1.089],
|
|
18
|
-
[0.45, 1.236, -0.019],
|
|
19
|
-
[0, 1, 0],
|
|
20
|
-
[0.922, -0.671, 0.263],
|
|
21
|
-
[0, 0, 1],
|
|
22
|
-
[0.153, -1.415, -0.449],
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
// just a further sanity check, uncomment to go wild
|
|
26
|
-
// for (let i = 0; i < 100; i++)
|
|
27
|
-
// vecs.push([
|
|
28
|
-
// Math.random() * 2 - 1,
|
|
29
|
-
// Math.random() * 2 - 1,
|
|
30
|
-
// Math.random() * 2 - 1,
|
|
31
|
-
// ]);
|
|
32
|
-
|
|
33
|
-
for (let vec of vecs) {
|
|
34
|
-
for (let i = 0; i < spaces.length; i++) {
|
|
35
|
-
for (let j = 0; j < spaces.length; j++) {
|
|
36
|
-
const A = spaces[i];
|
|
37
|
-
const B = spaces[j];
|
|
38
|
-
|
|
39
|
-
// @texel/color spaces
|
|
40
|
-
const a = A.space;
|
|
41
|
-
const b = B.space;
|
|
42
|
-
const suffix = `${a.id}-to-${b.id}`;
|
|
43
|
-
|
|
44
|
-
console.log(suffix);
|
|
45
|
-
const expected0 = convert(vec, a, b);
|
|
46
|
-
const tmp = vec.slice();
|
|
47
|
-
const expected1 = convert(vec, a, b, tmp);
|
|
48
|
-
|
|
49
|
-
const colorjsid_a = A.colorJSSpace.id;
|
|
50
|
-
const colorjsid_b = B.colorJSSpace.id;
|
|
51
|
-
t.deepEqual(expected0, tmp, `${suffix} copies into`);
|
|
52
|
-
t.deepEqual(expected0, expected1, `${suffix} copies into`);
|
|
53
|
-
t.equal(expected1, tmp, `${suffix} copies into and returns`);
|
|
54
|
-
|
|
55
|
-
// ColorJS returns NaN for display-p3 0 0 0 --> OKLCH
|
|
56
|
-
const outCoords = new Color(colorjsid_a, vec)
|
|
57
|
-
.to(colorjsid_b)
|
|
58
|
-
.coords.map((n) => n || 0);
|
|
59
|
-
|
|
60
|
-
// Colorjs does not appear to have as high precision as the latest
|
|
61
|
-
// CSS working draft spec which uses rational numbers
|
|
62
|
-
// so I have lowered tolerance for A98RGB, and consider it an upstream bug.
|
|
63
|
-
// please open a PR/issue if you feel otherwise!
|
|
64
|
-
const tolerance =
|
|
65
|
-
colorjsid_a.includes("a98") || colorjsid_b.includes("a98")
|
|
66
|
-
? 0.0000001
|
|
67
|
-
: undefined;
|
|
68
|
-
|
|
69
|
-
if (!arrayAlmostEqual(expected0, outCoords, tolerance)) {
|
|
70
|
-
console.error(
|
|
71
|
-
`\nError: %s - In (%s) Out (%s) Expected (%s)`,
|
|
72
|
-
suffix,
|
|
73
|
-
vec,
|
|
74
|
-
expected0,
|
|
75
|
-
outCoords
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
t.equal(
|
|
79
|
-
arrayAlmostEqual(expected0, outCoords, tolerance),
|
|
80
|
-
true,
|
|
81
|
-
suffix
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
});
|