@texel/color 1.0.0
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/LICENSE.md +21 -0
- package/README.md +376 -0
- package/package.json +65 -0
- package/src/conversion_matrices.js +268 -0
- package/src/core.js +186 -0
- package/src/gamut.js +336 -0
- package/src/index.js +6 -0
- package/src/okhsl.js +345 -0
- package/src/spaces/a98-rgb.js +50 -0
- package/src/spaces/display-p3.js +28 -0
- package/src/spaces/oklab.js +70 -0
- package/src/spaces/prophoto-rgb.js +72 -0
- package/src/spaces/rec2020.js +47 -0
- package/src/spaces/srgb.js +29 -0
- package/src/spaces/util.js +44 -0
- package/src/spaces/xyz.js +44 -0
- package/src/spaces.js +44 -0
- package/src/util.js +119 -0
- package/test/almost-equal.js +15 -0
- package/test/banner.png +0 -0
- package/test/bench-colorjs.js +138 -0
- package/test/bench-node.js +51 -0
- package/test/bench-size.js +3 -0
- package/test/canvas-graph.js +210 -0
- package/test/logo.js +112 -0
- package/test/logo.png +0 -0
- package/test/profiles/DisplayP3.icc +0 -0
- package/test/test-colorjs.js +87 -0
- package/test/test.js +321 -0
- package/tools/__pycache__/calc_oklab_matrices.cpython-311.pyc +0 -0
- package/tools/calc_oklab_matrices.py +233 -0
- package/tools/print_matrices.py +509 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import Color from "colorjs.io";
|
|
2
|
+
import {
|
|
3
|
+
convert,
|
|
4
|
+
OKLCH,
|
|
5
|
+
sRGB,
|
|
6
|
+
sRGBGamut,
|
|
7
|
+
listColorSpaces,
|
|
8
|
+
DisplayP3Gamut,
|
|
9
|
+
DisplayP3,
|
|
10
|
+
gamutMapOKLCH,
|
|
11
|
+
constrainAngle,
|
|
12
|
+
findCuspOKLCH,
|
|
13
|
+
degToRad,
|
|
14
|
+
MapToCuspL,
|
|
15
|
+
} from "../src/index.js";
|
|
16
|
+
|
|
17
|
+
const fixName = (name) => {
|
|
18
|
+
return name
|
|
19
|
+
.replace("display-", "")
|
|
20
|
+
.replace("a98-rgb", "a98rgb")
|
|
21
|
+
.replace("prophoto-rgb", "prophoto");
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// TODO: test okhsl with latest version of colorjs
|
|
25
|
+
const spaces = listColorSpaces().filter((f) => !/ok(hsv|hsl)/i.test(f.id));
|
|
26
|
+
const spacesForColorjs = spaces.map((s) => fixName(s.id));
|
|
27
|
+
|
|
28
|
+
const vecs = Array(128 * 128)
|
|
29
|
+
.fill()
|
|
30
|
+
.map((_, i, lst) => {
|
|
31
|
+
const t = i / (lst.length - 1);
|
|
32
|
+
return (
|
|
33
|
+
Array(3)
|
|
34
|
+
.fill()
|
|
35
|
+
// -0.5 .. 1.5
|
|
36
|
+
.map(() => t + (t * 2 - 1) * 0.5)
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const tmp = [0, 0, 0];
|
|
41
|
+
|
|
42
|
+
let now, elapsedColorjs, elapsedOurs;
|
|
43
|
+
|
|
44
|
+
//// OKLCH to sRGB with gamut mapping (direct path)
|
|
45
|
+
|
|
46
|
+
const hueCusps = Array(360).fill(null);
|
|
47
|
+
const oklchVecs = Array(512 * 256)
|
|
48
|
+
.fill()
|
|
49
|
+
.map((_, i, lst) => {
|
|
50
|
+
const t0 = i / (lst.length - 1);
|
|
51
|
+
const t1 = i / lst.length;
|
|
52
|
+
const H = constrainAngle(Math.round(t1 * 360));
|
|
53
|
+
if (!hueCusps[H]) {
|
|
54
|
+
const Hr = degToRad(H);
|
|
55
|
+
const a = Math.cos(Hr);
|
|
56
|
+
const b = Math.sin(Hr);
|
|
57
|
+
hueCusps[H] = findCuspOKLCH(a, b, sRGBGamut);
|
|
58
|
+
}
|
|
59
|
+
return [t0, t0, H];
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
now = performance.now();
|
|
63
|
+
for (let vec of oklchVecs) {
|
|
64
|
+
new Color("oklch", vec).to("srgb").toGamut({ space: "srgb", method: "css" });
|
|
65
|
+
}
|
|
66
|
+
elapsedColorjs = performance.now() - now;
|
|
67
|
+
|
|
68
|
+
now = performance.now();
|
|
69
|
+
for (let vec of oklchVecs) {
|
|
70
|
+
// you can omit the cusp and it will be found on the fly
|
|
71
|
+
// however the test will run slightly slower (e.g. ~100x faster rather than ~120x)
|
|
72
|
+
const cusp = hueCusps[vec[2]];
|
|
73
|
+
gamutMapOKLCH(vec, sRGBGamut, sRGB, tmp, MapToCuspL, cusp);
|
|
74
|
+
}
|
|
75
|
+
elapsedOurs = performance.now() - now;
|
|
76
|
+
|
|
77
|
+
console.log("OKLCH to sRGB with gamut mapping --");
|
|
78
|
+
console.log("Colorjs: %s ms", elapsedColorjs.toFixed(2));
|
|
79
|
+
console.log("Ours: %s ms", elapsedOurs.toFixed(2));
|
|
80
|
+
console.log("Speedup: %sx faster", (elapsedColorjs / elapsedOurs).toFixed(1));
|
|
81
|
+
|
|
82
|
+
//// conversions
|
|
83
|
+
|
|
84
|
+
now = performance.now();
|
|
85
|
+
for (let vec of vecs) {
|
|
86
|
+
for (let i = 0; i < spacesForColorjs.length; i++) {
|
|
87
|
+
for (let j = 0; j < spacesForColorjs.length; j++) {
|
|
88
|
+
const a = spacesForColorjs[i];
|
|
89
|
+
const b = spacesForColorjs[j];
|
|
90
|
+
new Color(a, vec).to(b);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
elapsedColorjs = performance.now() - now;
|
|
95
|
+
|
|
96
|
+
now = performance.now();
|
|
97
|
+
for (let vec of vecs) {
|
|
98
|
+
for (let i = 0; i < spaces.length; i++) {
|
|
99
|
+
for (let j = 0; j < spaces.length; j++) {
|
|
100
|
+
const a = spaces[i];
|
|
101
|
+
const b = spaces[j];
|
|
102
|
+
convert(vec, a, b, tmp);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
elapsedOurs = performance.now() - now;
|
|
107
|
+
console.log();
|
|
108
|
+
console.log("All Conversions --");
|
|
109
|
+
console.log("Colorjs: %s ms", elapsedColorjs.toFixed(2));
|
|
110
|
+
console.log("Ours: %s ms", elapsedOurs.toFixed(2));
|
|
111
|
+
console.log("Speedup: %sx faster", (elapsedColorjs / elapsedOurs).toFixed(1));
|
|
112
|
+
|
|
113
|
+
//// gamut mapping
|
|
114
|
+
|
|
115
|
+
now = performance.now();
|
|
116
|
+
for (let vec of vecs) {
|
|
117
|
+
for (let i = 0; i < spacesForColorjs.length; i++) {
|
|
118
|
+
const a = spacesForColorjs[i];
|
|
119
|
+
new Color(a, vec).to("p3").toGamut({ space: "p3", method: "css" });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
elapsedColorjs = performance.now() - now;
|
|
123
|
+
|
|
124
|
+
now = performance.now();
|
|
125
|
+
for (let vec of vecs) {
|
|
126
|
+
for (let i = 0; i < spaces.length; i++) {
|
|
127
|
+
const a = spaces[i];
|
|
128
|
+
convert(vec, a, OKLCH, tmp);
|
|
129
|
+
gamutMapOKLCH(tmp, DisplayP3Gamut, DisplayP3, tmp);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
elapsedOurs = performance.now() - now;
|
|
133
|
+
|
|
134
|
+
console.log();
|
|
135
|
+
console.log("Conversion + Gamut Mapping --");
|
|
136
|
+
console.log("Colorjs: %s ms", elapsedColorjs.toFixed(2));
|
|
137
|
+
console.log("Ours: %s ms", elapsedOurs.toFixed(2));
|
|
138
|
+
console.log("Speedup: %sx faster", (elapsedColorjs / elapsedOurs).toFixed(1));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
convert,
|
|
3
|
+
OKLCH,
|
|
4
|
+
sRGB,
|
|
5
|
+
sRGBGamut,
|
|
6
|
+
listColorSpaces,
|
|
7
|
+
gamutMapOKLCH,
|
|
8
|
+
} from "../src/index.js";
|
|
9
|
+
|
|
10
|
+
const spaces = listColorSpaces().filter((f) => !/ok(hsv|hsl)/i.test(f.id));
|
|
11
|
+
|
|
12
|
+
const vecs = Array(128 * 128)
|
|
13
|
+
.fill()
|
|
14
|
+
.map((_, i, lst) => {
|
|
15
|
+
const t = i / (lst.length - 1);
|
|
16
|
+
return (
|
|
17
|
+
Array(3)
|
|
18
|
+
.fill()
|
|
19
|
+
// -0.5 .. 1.5
|
|
20
|
+
.map(() => t + (t * 2 - 1) * 0.5)
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const tmp = [0, 0, 0];
|
|
25
|
+
|
|
26
|
+
// console.time("bench");
|
|
27
|
+
|
|
28
|
+
for (let vec of vecs) {
|
|
29
|
+
for (let i = 0; i < spaces.length; i++) {
|
|
30
|
+
for (let j = 0; j < spaces.length; j++) {
|
|
31
|
+
const a = spaces[i];
|
|
32
|
+
const b = spaces[j];
|
|
33
|
+
|
|
34
|
+
// convert A to B
|
|
35
|
+
convert(vec, a, b, tmp);
|
|
36
|
+
// convert B to OKLCH
|
|
37
|
+
convert(tmp, b, OKLCH, tmp);
|
|
38
|
+
// gamut map OKLCH
|
|
39
|
+
gamutMapOKLCH(tmp, sRGBGamut, sRGB, tmp);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// benchmark for EOK
|
|
45
|
+
// for (let i = 0; i < 1000; i++) {
|
|
46
|
+
// for (let vec of vecs) {
|
|
47
|
+
// deltaEOK(vec, [0, 0.25, 1]);
|
|
48
|
+
// }
|
|
49
|
+
// }
|
|
50
|
+
|
|
51
|
+
// console.timeEnd("bench");
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import canvasSketch from "canvas-sketch";
|
|
2
|
+
import {
|
|
3
|
+
findCuspOKLCH,
|
|
4
|
+
gamutMapOKLCH,
|
|
5
|
+
MapToAdaptiveCuspL,
|
|
6
|
+
A98RGBGamut,
|
|
7
|
+
convert,
|
|
8
|
+
DisplayP3Gamut,
|
|
9
|
+
OKLCH,
|
|
10
|
+
Rec2020Gamut,
|
|
11
|
+
sRGBGamut,
|
|
12
|
+
degToRad,
|
|
13
|
+
constrainAngle,
|
|
14
|
+
floatToByte,
|
|
15
|
+
isRGBInGamut,
|
|
16
|
+
} from "../src/index.js";
|
|
17
|
+
import arrayAlmostEqual from "./almost-equal.js";
|
|
18
|
+
|
|
19
|
+
const settings = {
|
|
20
|
+
dimensions: [768, 768],
|
|
21
|
+
animate: true,
|
|
22
|
+
playbackRate: "throttle",
|
|
23
|
+
fps: 2,
|
|
24
|
+
attributes: {
|
|
25
|
+
colorSpace: "display-p3",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const sketch = ({ width, height }) => {
|
|
30
|
+
return ({ context, width, height, frame, playhead }) => {
|
|
31
|
+
const { colorSpace = "srgb" } = context.getContextAttributes();
|
|
32
|
+
const gamut = colorSpace === "srgb" ? sRGBGamut : DisplayP3Gamut;
|
|
33
|
+
const mapping = MapToAdaptiveCuspL;
|
|
34
|
+
|
|
35
|
+
context.fillStyle = "gray";
|
|
36
|
+
context.fillRect(0, 0, width, height);
|
|
37
|
+
const H = constrainAngle((frame * 45) / 2);
|
|
38
|
+
|
|
39
|
+
// console.time("map");
|
|
40
|
+
// console.profile("map");
|
|
41
|
+
const tmp = [0, 0, 0];
|
|
42
|
+
const pixels = new Uint8ClampedArray(width * height * 4).fill(0xff);
|
|
43
|
+
for (let y = 0; y < height; y++) {
|
|
44
|
+
for (let x = 0; x < width; x++) {
|
|
45
|
+
const u = x / width;
|
|
46
|
+
const v = y / height;
|
|
47
|
+
|
|
48
|
+
const [L, C] = UVtoLC([u, v]);
|
|
49
|
+
|
|
50
|
+
let oklch = [L, C, H];
|
|
51
|
+
let rgb = convert(oklch, OKLCH, gamut.space, tmp);
|
|
52
|
+
if (!isRGBInGamut(rgb, 0)) {
|
|
53
|
+
rgb[0] = 0.25;
|
|
54
|
+
rgb[1] = 0.25;
|
|
55
|
+
rgb[2] = 0.25;
|
|
56
|
+
// if we wanted to fill the whole space with mapped colors
|
|
57
|
+
// rgb = gamutMapOKLCH(oklch, gamut, sRGB, tmp, mapping);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const idx = x + y * width;
|
|
61
|
+
pixels[idx * 4 + 0] = floatToByte(rgb[0]);
|
|
62
|
+
pixels[idx * 4 + 1] = floatToByte(rgb[1]);
|
|
63
|
+
pixels[idx * 4 + 2] = floatToByte(rgb[2]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
context.putImageData(
|
|
67
|
+
new ImageData(pixels, width, height, { colorSpace }),
|
|
68
|
+
0,
|
|
69
|
+
0
|
|
70
|
+
);
|
|
71
|
+
// console.profileEnd("map");
|
|
72
|
+
// console.timeEnd("map");
|
|
73
|
+
|
|
74
|
+
const A = [1, 0];
|
|
75
|
+
const B = [0, 0];
|
|
76
|
+
const lineWidth = width * 0.003;
|
|
77
|
+
context.lineWidth = lineWidth;
|
|
78
|
+
|
|
79
|
+
const gamuts = [
|
|
80
|
+
{ defaultColor: "yellow", gamut: sRGBGamut },
|
|
81
|
+
{ defaultColor: "palegreen", gamut: DisplayP3Gamut },
|
|
82
|
+
{ defaultColor: "red", gamut: Rec2020Gamut },
|
|
83
|
+
{ defaultColor: "pink", gamut: A98RGBGamut },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const hueAngle = degToRad(H);
|
|
87
|
+
const a = Math.cos(hueAngle);
|
|
88
|
+
const b = Math.sin(hueAngle);
|
|
89
|
+
|
|
90
|
+
for (let { gamut: dispGamut, defaultColor } of gamuts) {
|
|
91
|
+
const gamutCusp = findCuspOKLCH(a, b, dispGamut);
|
|
92
|
+
const gamutTri = [A, gamutCusp, B];
|
|
93
|
+
drawLCTriangle(
|
|
94
|
+
context,
|
|
95
|
+
gamutTri,
|
|
96
|
+
gamut === dispGamut ? "white" : defaultColor
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
context.strokeStyle = "white";
|
|
101
|
+
|
|
102
|
+
const steps = 64;
|
|
103
|
+
for (let i = 0; i < steps; i++) {
|
|
104
|
+
// get some LC point that is very likely to be out of gamut
|
|
105
|
+
const ox = 0.5;
|
|
106
|
+
const oy = 0.5;
|
|
107
|
+
const r = 1;
|
|
108
|
+
const t = (i / steps) * degToRad(360) + degToRad(-180);
|
|
109
|
+
const xy = [Math.cos(t) * r + ox, Math.sin(t) * r + oy];
|
|
110
|
+
const [L, C] = UVtoLC(xy);
|
|
111
|
+
const oklch = [L, C, H];
|
|
112
|
+
const lc = oklch.slice(0, 2);
|
|
113
|
+
|
|
114
|
+
const mapped = gamutMapOKLCH(oklch, gamut, OKLCH, tmp, mapping);
|
|
115
|
+
|
|
116
|
+
const radius = width * 0.01;
|
|
117
|
+
const didChange = !arrayAlmostEqual(mapped, oklch);
|
|
118
|
+
if (didChange) {
|
|
119
|
+
context.globalAlpha = 0.5;
|
|
120
|
+
drawLCPoint(context, lc, radius / 2, "white");
|
|
121
|
+
context.beginPath();
|
|
122
|
+
context.lineTo(...LCtoXY(lc));
|
|
123
|
+
context.lineTo(...LCtoXY(mapped));
|
|
124
|
+
context.stroke();
|
|
125
|
+
context.globalAlpha = 1;
|
|
126
|
+
drawLCPoint(context, mapped.slice(0, 2), radius, "white");
|
|
127
|
+
} else {
|
|
128
|
+
drawLCPoint(context, lc, radius);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fontSize = width * 0.03;
|
|
133
|
+
const boxHeight = fontSize * gamuts.length;
|
|
134
|
+
const pad = width * 0.05;
|
|
135
|
+
const padleft = width * 0.1;
|
|
136
|
+
context.fillStyle = "black";
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < gamuts.length; i++) {
|
|
139
|
+
const { gamut: dispGamut, defaultColor } = gamuts[i];
|
|
140
|
+
const curColor = dispGamut === gamut ? "white" : defaultColor;
|
|
141
|
+
|
|
142
|
+
context.font = `${fontSize}px monospace`;
|
|
143
|
+
context.textAlign = "right";
|
|
144
|
+
context.textBaseline = "top";
|
|
145
|
+
context.fillStyle = curColor;
|
|
146
|
+
const x = width - pad - padleft;
|
|
147
|
+
const y = height - boxHeight + i * fontSize - pad;
|
|
148
|
+
context.fillText(dispGamut.space.id, x, y);
|
|
149
|
+
context.beginPath();
|
|
150
|
+
context.lineTo(x + fontSize / 2, y + fontSize * 0.4);
|
|
151
|
+
context.lineTo(x + padleft, y + fontSize * 0.4);
|
|
152
|
+
context.strokeStyle = curColor;
|
|
153
|
+
context.stroke();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
context.fillStyle = "white";
|
|
157
|
+
context.fillText(
|
|
158
|
+
`Hue: ${H.toFixed(0)}º`,
|
|
159
|
+
width - pad,
|
|
160
|
+
height - pad - boxHeight - fontSize * 2
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function LCtoXY(okLC) {
|
|
165
|
+
const x = (okLC[1] / 1) * width;
|
|
166
|
+
const y = (1 - okLC[0]) * height;
|
|
167
|
+
return [x, y];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function XYtoLC(xy) {
|
|
171
|
+
return UVtoLC([xy[0] / width, xy[1] / height]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function UVtoLC(xy) {
|
|
175
|
+
const L = 1 - xy[1];
|
|
176
|
+
const C = xy[0] * 1;
|
|
177
|
+
return [L, C];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function drawLCTriangle(context, triangle, color = "white") {
|
|
181
|
+
context.beginPath();
|
|
182
|
+
triangle.forEach((oklch) => {
|
|
183
|
+
const [x, y] = LCtoXY(oklch);
|
|
184
|
+
context.lineTo(x, y);
|
|
185
|
+
});
|
|
186
|
+
context.closePath();
|
|
187
|
+
context.strokeStyle = color;
|
|
188
|
+
context.stroke();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function drawLCPoint(
|
|
192
|
+
context,
|
|
193
|
+
okLC,
|
|
194
|
+
radius = width * 0.01,
|
|
195
|
+
color = "white",
|
|
196
|
+
fill = true
|
|
197
|
+
) {
|
|
198
|
+
context.beginPath();
|
|
199
|
+
context.arc(...LCtoXY(okLC), radius, 0, Math.PI * 2);
|
|
200
|
+
if (fill) {
|
|
201
|
+
context.fillStyle = color;
|
|
202
|
+
context.fill();
|
|
203
|
+
} else {
|
|
204
|
+
context.strokeStyle = color;
|
|
205
|
+
context.stroke();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
canvasSketch(sketch, settings);
|
package/test/logo.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import test from "tape";
|
|
2
|
+
import Color from "colorjs.io";
|
|
3
|
+
import arrayAlmostEqual from "./almost-equal.js";
|
|
4
|
+
import { listColorSpaces, convert } from "../src/index.js";
|
|
5
|
+
|
|
6
|
+
test("should approximately match colorjs.io conversions", async (t) => {
|
|
7
|
+
// note: we skip okhsv/hsl as colorjs.io doesn't support in the current npm version
|
|
8
|
+
const spaces = listColorSpaces().filter((f) => !/ok(hsl|hsv)/i.test(f.id));
|
|
9
|
+
const vecs = [
|
|
10
|
+
[0.12341, 0.12001, 0.05212],
|
|
11
|
+
[1, 1, 1],
|
|
12
|
+
[1, 0, 0],
|
|
13
|
+
[0, 0, 0],
|
|
14
|
+
// some other inputs
|
|
15
|
+
[0.95, 1, 1.089],
|
|
16
|
+
[0.45, 1.236, -0.019],
|
|
17
|
+
[0, 1, 0],
|
|
18
|
+
[0.922, -0.671, 0.263],
|
|
19
|
+
[0, 0, 1],
|
|
20
|
+
[0.153, -1.415, -0.449],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// just a further sanity check, uncomment to go wild
|
|
24
|
+
// for (let i = 0; i < 100; i++)
|
|
25
|
+
// vecs.push([
|
|
26
|
+
// Math.random() * 2 - 1,
|
|
27
|
+
// Math.random() * 2 - 1,
|
|
28
|
+
// Math.random() * 2 - 1,
|
|
29
|
+
// ]);
|
|
30
|
+
|
|
31
|
+
const fixName = (name) => {
|
|
32
|
+
return name
|
|
33
|
+
.replace("display-", "")
|
|
34
|
+
.replace("a98-rgb", "a98rgb")
|
|
35
|
+
.replace("prophoto-rgb", "prophoto");
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (let vec of vecs) {
|
|
39
|
+
for (let i = 0; i < spaces.length; i++) {
|
|
40
|
+
for (let j = 0; j < spaces.length; j++) {
|
|
41
|
+
const a = spaces[i];
|
|
42
|
+
const b = spaces[j];
|
|
43
|
+
const suffix = `${a.id}-to-${b.id}`;
|
|
44
|
+
|
|
45
|
+
console.log(suffix);
|
|
46
|
+
const expected0 = convert(vec, a, b);
|
|
47
|
+
const tmp = vec.slice();
|
|
48
|
+
const expected1 = convert(vec, a, b, tmp);
|
|
49
|
+
|
|
50
|
+
const colorjsid_a = fixName(a.id);
|
|
51
|
+
const colorjsid_b = fixName(b.id);
|
|
52
|
+
t.deepEqual(expected0, tmp, `${suffix} copies into`);
|
|
53
|
+
t.deepEqual(expected0, expected1, `${suffix} copies into`);
|
|
54
|
+
t.equal(expected1, tmp, `${suffix} copies into and returns`);
|
|
55
|
+
|
|
56
|
+
// ColorJS returns NaN for display-p3 0 0 0 --> OKLCH
|
|
57
|
+
const outCoords = new Color(colorjsid_a, vec)
|
|
58
|
+
.to(colorjsid_b)
|
|
59
|
+
.coords.map((n) => n || 0);
|
|
60
|
+
|
|
61
|
+
// Colorjs does not appear to have as high precision as the latest
|
|
62
|
+
// CSS working draft spec which uses rational numbers
|
|
63
|
+
// so I have lowered tolerance for A98RGB, and consider it an upstream bug.
|
|
64
|
+
// please open a PR/issue if you feel otherwise!
|
|
65
|
+
const tolerance =
|
|
66
|
+
colorjsid_a.includes("a98") || colorjsid_b.includes("a98")
|
|
67
|
+
? 0.0000001
|
|
68
|
+
: undefined;
|
|
69
|
+
|
|
70
|
+
if (!arrayAlmostEqual(expected0, outCoords, tolerance)) {
|
|
71
|
+
console.error(
|
|
72
|
+
`\nError: %s - In (%s) Out (%s) Expected (%s)`,
|
|
73
|
+
suffix,
|
|
74
|
+
vec,
|
|
75
|
+
expected0,
|
|
76
|
+
outCoords
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
t.equal(
|
|
80
|
+
arrayAlmostEqual(expected0, outCoords, tolerance),
|
|
81
|
+
true,
|
|
82
|
+
suffix
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|