@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
package/test/test.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import test from "tape";
|
|
2
|
+
import Color from "colorjs.io";
|
|
3
|
+
import arrayAlmostEqual from "./almost-equal.js";
|
|
4
|
+
import {
|
|
5
|
+
floatToByte,
|
|
6
|
+
hexToRGB,
|
|
7
|
+
isRGBInGamut,
|
|
8
|
+
RGBtoHex,
|
|
9
|
+
linear_sRGB_to_LMS_M,
|
|
10
|
+
LMS_to_linear_sRGB_M,
|
|
11
|
+
LMS_to_XYZ_M,
|
|
12
|
+
XYZ_to_linear_sRGB_M,
|
|
13
|
+
XYZ_to_LMS_M,
|
|
14
|
+
convert,
|
|
15
|
+
OKLab,
|
|
16
|
+
OKLCH,
|
|
17
|
+
sRGB,
|
|
18
|
+
sRGBLinear,
|
|
19
|
+
XYZ,
|
|
20
|
+
OKLab_from,
|
|
21
|
+
OKLab_to,
|
|
22
|
+
transform,
|
|
23
|
+
OKHSL,
|
|
24
|
+
sRGBGamut,
|
|
25
|
+
OKHSV,
|
|
26
|
+
serialize,
|
|
27
|
+
gamutMapOKLCH,
|
|
28
|
+
degToRad,
|
|
29
|
+
OKHSLToOKLab,
|
|
30
|
+
OKHSVToOKLab,
|
|
31
|
+
OKLabToOKHSL,
|
|
32
|
+
OKLabToOKHSV,
|
|
33
|
+
XYZD65ToD50,
|
|
34
|
+
XYZD50ToD65,
|
|
35
|
+
XYZD50,
|
|
36
|
+
ProPhotoRGB,
|
|
37
|
+
ProPhotoRGBLinear,
|
|
38
|
+
findCuspOKLCH,
|
|
39
|
+
LMS_to_OKLab_M,
|
|
40
|
+
DisplayP3,
|
|
41
|
+
A98RGB,
|
|
42
|
+
A98RGBLinear,
|
|
43
|
+
XYZ_to_linear_A98RGB_M,
|
|
44
|
+
DisplayP3Gamut,
|
|
45
|
+
} from "../src/index.js";
|
|
46
|
+
|
|
47
|
+
test("should convert XYZ in different whitepoints", async (t) => {
|
|
48
|
+
const oklab = [0.56, 0.03, -0.1];
|
|
49
|
+
const xyz_d65_input = new Color("oklab", oklab).to("xyz").coords;
|
|
50
|
+
const xyz_d50_output = new Color("xyz", xyz_d65_input).to("xyz-d50").coords;
|
|
51
|
+
|
|
52
|
+
let tmp = [0, 0, 0];
|
|
53
|
+
const out = XYZD65ToD50(xyz_d65_input, tmp);
|
|
54
|
+
t.equal(tmp, out);
|
|
55
|
+
t.deepEqual(out, xyz_d50_output);
|
|
56
|
+
|
|
57
|
+
tmp = [0, 0, 0];
|
|
58
|
+
const out2 = XYZD50ToD65(xyz_d50_output, tmp);
|
|
59
|
+
t.deepEqual(out2, xyz_d65_input);
|
|
60
|
+
t.equal(out2, tmp);
|
|
61
|
+
|
|
62
|
+
t.deepEqual(convert(xyz_d50_output, XYZD50, XYZ), xyz_d65_input);
|
|
63
|
+
t.deepEqual(convert(xyz_d65_input, XYZ, XYZD50), xyz_d50_output);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("should convert", async (t) => {
|
|
67
|
+
const oklab = [0.56, 0.03, -0.1];
|
|
68
|
+
const rgbExpected = new Color("oklab", oklab)
|
|
69
|
+
.to("srgb")
|
|
70
|
+
.coords.map((n) => floatToByte(n));
|
|
71
|
+
|
|
72
|
+
const lrgb = OKLab_to(oklab, LMS_to_linear_sRGB_M);
|
|
73
|
+
const rgb = convert(lrgb, sRGBLinear, sRGB).map((f) => floatToByte(f));
|
|
74
|
+
t.deepEqual(rgb, rgbExpected, "oklab to srgb");
|
|
75
|
+
|
|
76
|
+
const inArray = oklab.slice();
|
|
77
|
+
const outArray = OKLab_to(oklab, LMS_to_linear_sRGB_M, inArray);
|
|
78
|
+
t.equal(inArray, outArray);
|
|
79
|
+
|
|
80
|
+
const oklab2 = OKLab_from(lrgb, linear_sRGB_to_LMS_M);
|
|
81
|
+
t.equals(arrayAlmostEqual(oklab, oklab2), true, "linear srgb to oklab");
|
|
82
|
+
|
|
83
|
+
const xyzExpected = new Color("oklab", oklab).to("xyz").coords;
|
|
84
|
+
const xyz0 = OKLab_to(oklab, LMS_to_XYZ_M);
|
|
85
|
+
t.deepEqual(arrayAlmostEqual(xyzExpected, xyz0), true, "OKLab to XYZ");
|
|
86
|
+
|
|
87
|
+
const oklabResult = OKLab_from(xyzExpected, XYZ_to_LMS_M);
|
|
88
|
+
t.deepEqual(arrayAlmostEqual(oklabResult, oklab), true, "XYZ to OKLab");
|
|
89
|
+
|
|
90
|
+
const xyzToLSRGBExpected = new Color("xyz", xyzExpected).to(
|
|
91
|
+
"srgb-linear"
|
|
92
|
+
).coords;
|
|
93
|
+
const xyzToLSRGB = transform(xyzExpected, XYZ_to_linear_sRGB_M);
|
|
94
|
+
t.deepEqual(
|
|
95
|
+
arrayAlmostEqual(xyzToLSRGB, xyzToLSRGBExpected),
|
|
96
|
+
true,
|
|
97
|
+
"XYZ to sRGB-linear"
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// XYZ to sRGBLinear
|
|
101
|
+
const outTest = [0, 0, 0];
|
|
102
|
+
const outTest2 = convert(xyzExpected, XYZ, sRGBLinear, outTest);
|
|
103
|
+
t.equal(outTest, outTest2, "returns out vec3");
|
|
104
|
+
t.deepEqual(outTest, xyzToLSRGB);
|
|
105
|
+
|
|
106
|
+
t.deepEqual(
|
|
107
|
+
convert(xyzExpected, XYZ, sRGB),
|
|
108
|
+
sRGB.fromBase(xyzToLSRGB),
|
|
109
|
+
"XYZ to sRGB"
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const sRGBL_from_XYZ = new Color("xyz", xyzExpected).to("srgb-linear").coords;
|
|
113
|
+
|
|
114
|
+
t.ok(
|
|
115
|
+
arrayAlmostEqual(convert(xyzExpected, XYZ, sRGBLinear), sRGBL_from_XYZ),
|
|
116
|
+
true,
|
|
117
|
+
"XYZ to sRGBLinear"
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// from https://bottosson.github.io/posts/oklab/
|
|
121
|
+
const knownPairsXYZToOKLab = [
|
|
122
|
+
[
|
|
123
|
+
[0.95, 1, 1.089],
|
|
124
|
+
[1, 0, 0],
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
[1, 0, 0],
|
|
128
|
+
[0.45, 1.236, -0.019],
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
[0, 1, 0],
|
|
132
|
+
[0.922, -0.671, 0.263],
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
[0, 0, 1],
|
|
136
|
+
[0.153, -1.415, -0.449],
|
|
137
|
+
],
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (let [xyz, oklabExpected] of knownPairsXYZToOKLab) {
|
|
141
|
+
const oklabRet = convert(xyz, XYZ, OKLab).map((n) => {
|
|
142
|
+
n = roundToNDecimals(n, 3);
|
|
143
|
+
if (n === -0) n = 0;
|
|
144
|
+
return n;
|
|
145
|
+
});
|
|
146
|
+
t.deepEqual(oklabRet, oklabExpected);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const oklch = [0.5, -0.36, 90];
|
|
150
|
+
t.deepEqual(
|
|
151
|
+
convert(oklch, OKLCH, OKLab),
|
|
152
|
+
new Color("oklch", oklch).to("oklab").coords,
|
|
153
|
+
"handle negative chroma"
|
|
154
|
+
);
|
|
155
|
+
t.equal(
|
|
156
|
+
arrayAlmostEqual(
|
|
157
|
+
convert(oklch, OKLCH, sRGB),
|
|
158
|
+
new Color("oklch", oklch).to("srgb").coords
|
|
159
|
+
),
|
|
160
|
+
true
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("should convert to okhsl", async (t) => {
|
|
165
|
+
const okhsl = [30, 0.5, 0.5];
|
|
166
|
+
const oklab = OKHSLToOKLab(okhsl, sRGBGamut);
|
|
167
|
+
const expectedLABfromOKHSL = [
|
|
168
|
+
0.568838198942395, 0.08553885335853362, 0.049385880012721296,
|
|
169
|
+
];
|
|
170
|
+
t.deepEqual(oklab, expectedLABfromOKHSL);
|
|
171
|
+
const okhslOut = OKLabToOKHSL(expectedLABfromOKHSL, sRGBGamut);
|
|
172
|
+
t.deepEqual(okhslOut, okhsl);
|
|
173
|
+
|
|
174
|
+
const okhsv = okhsl.slice();
|
|
175
|
+
const expectedLABfromOKHSV = [
|
|
176
|
+
0.45178419415172344, 0.0658295198906634, 0.03800669102949832,
|
|
177
|
+
];
|
|
178
|
+
t.deepEqual(OKHSVToOKLab(okhsv, sRGBGamut), expectedLABfromOKHSV);
|
|
179
|
+
t.deepEqual(
|
|
180
|
+
arrayAlmostEqual(OKLabToOKHSV(expectedLABfromOKHSV, sRGBGamut), okhsv),
|
|
181
|
+
true
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
t.deepEqual(convert(okhsl, OKHSL, OKLab), expectedLABfromOKHSL);
|
|
185
|
+
t.deepEqual(convert(okhsv, OKHSV, OKLab), expectedLABfromOKHSV);
|
|
186
|
+
t.deepEqual(
|
|
187
|
+
arrayAlmostEqual(convert(expectedLABfromOKHSV, OKLab, OKHSV), okhsv),
|
|
188
|
+
true
|
|
189
|
+
);
|
|
190
|
+
t.deepEqual(
|
|
191
|
+
arrayAlmostEqual(convert(expectedLABfromOKHSL, OKLab, OKHSL), okhsl),
|
|
192
|
+
true
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("should find cusp", async (t) => {
|
|
197
|
+
const H = 30;
|
|
198
|
+
const hueAngle = degToRad(H);
|
|
199
|
+
|
|
200
|
+
const aNorm = Math.cos(hueAngle);
|
|
201
|
+
const bNorm = Math.sin(hueAngle);
|
|
202
|
+
const out2 = [0, 0];
|
|
203
|
+
const cusp = findCuspOKLCH(aNorm, bNorm, sRGBGamut, out2);
|
|
204
|
+
const hue30sRGBCusp = [0.6322837041534408, 0.2535829789121266];
|
|
205
|
+
|
|
206
|
+
t.equal(out2, cusp);
|
|
207
|
+
t.deepEqual(cusp, hue30sRGBCusp);
|
|
208
|
+
|
|
209
|
+
const cuspP3 = findCuspOKLCH(aNorm, bNorm, DisplayP3Gamut, out2);
|
|
210
|
+
const hue30P3Cusp = [0.6542359095783624, 0.2931937837912358];
|
|
211
|
+
t.equal(out2, cuspP3);
|
|
212
|
+
t.deepEqual(cuspP3, hue30P3Cusp);
|
|
213
|
+
|
|
214
|
+
const l2 = 0.7;
|
|
215
|
+
const c2 = 0.3;
|
|
216
|
+
const newLCH = [l2, c2, H];
|
|
217
|
+
const mapped = gamutMapOKLCH(newLCH, sRGBGamut, OKLCH);
|
|
218
|
+
t.deepEqual(mapped, [0.679529110489262, 0.2093088779230169, 30]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("should gamut map", async (t) => {
|
|
222
|
+
const oklch = [0.9, 0.4, 30];
|
|
223
|
+
const rgb = convert(oklch, OKLCH, sRGB);
|
|
224
|
+
t.equals(isRGBInGamut(rgb, 0), false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("should serialize", async (t) => {
|
|
228
|
+
t.deepEqual(serialize([0, 0.5, 1], sRGB), "rgb(0, 128, 255)");
|
|
229
|
+
t.deepEqual(serialize([0, 0.5, 1], sRGBLinear), "color(srgb-linear 0 0.5 1)");
|
|
230
|
+
t.deepEqual(serialize([1, 0, 0], OKLCH, sRGB), "rgb(255, 255, 255)");
|
|
231
|
+
t.deepEqual(serialize([1, 0, 0], OKLCH), "oklch(1 0 0)");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("utils", async (t) => {
|
|
235
|
+
t.deepEqual(RGBtoHex([0, 0.5, 1]), "#0080ff");
|
|
236
|
+
t.deepEqual(hexToRGB("#0080ff"), [0, 0.5019607843137255, 1]);
|
|
237
|
+
const tmp = [0, 0, 0];
|
|
238
|
+
hexToRGB("#0080ff", tmp);
|
|
239
|
+
t.deepEqual(tmp, [0, 0.5019607843137255, 1]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("should convert D65 based to D50 based color spaces", async (t) => {
|
|
243
|
+
const rgbin = [0.25, 0.5, 1];
|
|
244
|
+
const xyzD65Input = new Color("srgb", rgbin).to("xyz-d65").coords;
|
|
245
|
+
const xyzD50Input = XYZD65ToD50(xyzD65Input);
|
|
246
|
+
|
|
247
|
+
const prophotoFromXYZD65_expected = new Color("xyz-d65", xyzD65Input).to(
|
|
248
|
+
"prophoto-linear"
|
|
249
|
+
).coords;
|
|
250
|
+
|
|
251
|
+
const ret = convert(xyzD65Input, XYZ, ProPhotoRGBLinear);
|
|
252
|
+
t.deepEqual(ret, prophotoFromXYZD65_expected);
|
|
253
|
+
|
|
254
|
+
const xyzD65 = convert(prophotoFromXYZD65_expected, ProPhotoRGBLinear, XYZ);
|
|
255
|
+
t.deepEqual(arrayAlmostEqual(xyzD65, xyzD65Input), true);
|
|
256
|
+
|
|
257
|
+
const prophoto2 = convert(rgbin, sRGB, ProPhotoRGBLinear);
|
|
258
|
+
const prophotoExpected = new Color("srgb", rgbin).to(
|
|
259
|
+
"prophoto-linear"
|
|
260
|
+
).coords;
|
|
261
|
+
t.deepEqual(arrayAlmostEqual(prophoto2, prophotoExpected), true);
|
|
262
|
+
|
|
263
|
+
const oklabIn = new Color("srgb", rgbin).to("oklab").coords;
|
|
264
|
+
const oklabToProphoto = new Color("oklab", oklabIn).to(
|
|
265
|
+
"prophoto-linear"
|
|
266
|
+
).coords;
|
|
267
|
+
t.deepEqual(
|
|
268
|
+
arrayAlmostEqual(
|
|
269
|
+
convert(oklabIn, OKLab, ProPhotoRGBLinear),
|
|
270
|
+
oklabToProphoto
|
|
271
|
+
),
|
|
272
|
+
true
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const oklabIn2 = [1, 1, 1];
|
|
276
|
+
const oklabToProphoto2 = new Color("oklab", oklabIn2).to("prophoto").coords;
|
|
277
|
+
|
|
278
|
+
t.deepEqual(
|
|
279
|
+
arrayAlmostEqual(convert(oklabIn2, OKLab, ProPhotoRGB), oklabToProphoto2),
|
|
280
|
+
true
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("should handle problematic coords", async (t) => {
|
|
285
|
+
const in0 = [0.95, 1, 1.089];
|
|
286
|
+
const out0 = convert(in0, XYZ, OKLab);
|
|
287
|
+
const expected0lab = new Color("xyz", in0).to("oklab").coords;
|
|
288
|
+
t.deepEqual(arrayAlmostEqual(out0, expected0lab), true);
|
|
289
|
+
const inP3 = [0, 0, 1];
|
|
290
|
+
const outXYZ = convert(inP3, DisplayP3, XYZ);
|
|
291
|
+
t.deepEqual(
|
|
292
|
+
arrayAlmostEqual(outXYZ, new Color("p3", inP3).to("xyz").coords),
|
|
293
|
+
true
|
|
294
|
+
);
|
|
295
|
+
const outA98 = convert(outXYZ, XYZ, A98RGBLinear);
|
|
296
|
+
t.deepEqual(
|
|
297
|
+
arrayAlmostEqual(
|
|
298
|
+
outA98,
|
|
299
|
+
new Color("xyz", outXYZ).to("a98rgb-linear").coords
|
|
300
|
+
),
|
|
301
|
+
true
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Failing test here, but it appears Colorjs does not match the latest
|
|
305
|
+
// CSS spec (working draft with rational form). Please open a PR/issue if you
|
|
306
|
+
// think you could help, but unless I'm mistaken it seems to be an upstream issue
|
|
307
|
+
const tolerance = 0.0000001;
|
|
308
|
+
t.deepEqual(
|
|
309
|
+
arrayAlmostEqual(
|
|
310
|
+
convert(inP3, DisplayP3, A98RGB),
|
|
311
|
+
new Color("p3", inP3).to("a98rgb").coords,
|
|
312
|
+
tolerance
|
|
313
|
+
),
|
|
314
|
+
true
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
function roundToNDecimals(value, digits) {
|
|
319
|
+
var tenToN = 10 ** digits;
|
|
320
|
+
return Math.round(value * tenToN) / tenToN;
|
|
321
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calculate `oklab` matrices.
|
|
3
|
+
|
|
4
|
+
Björn Ottosson, in his original calculations, used a different white point than
|
|
5
|
+
what CSS and most other people use. In the CSS repository, he commented on
|
|
6
|
+
how to calculate the M1 matrix using the exact same white point as CSS. He
|
|
7
|
+
provided the initial matrix used in this calculation, which we will call M0.
|
|
8
|
+
https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-945714988.
|
|
9
|
+
This M0 matrix is used to create a precise matrix to convert XYZ to LMS using
|
|
10
|
+
the D65 white point as specified by CSS. Both ColorAide and CSS use the D65
|
|
11
|
+
chromaticity coordinates of `(0.31270, 0.32900)` which is documented and used
|
|
12
|
+
for sRGB as the standard. There are likely implementations unaware that the
|
|
13
|
+
they should, or even how to adapt the Oklab M1 matrix to their white point
|
|
14
|
+
as this is not documented in the author's Oklab blog post, but is buried in a
|
|
15
|
+
CSS repository discussion.
|
|
16
|
+
|
|
17
|
+
Additionally, the documented M2 matrix is specified as 32 bit values, and the
|
|
18
|
+
inverse is calculated directly from the this 32 bit matrix. The forward and
|
|
19
|
+
reverse transform is calculated to perfectly convert 32 bit values, but when
|
|
20
|
+
translating 64 bit values, the transform adds a lot of noise after about 7 - 8
|
|
21
|
+
digits (the precision of 32 bit floats). This is particularly problematic for
|
|
22
|
+
achromatic colors in Oklab and OkLCh and can cause chroma not to resolve to zero.
|
|
23
|
+
|
|
24
|
+
To provide an M2 matrix that works better for 64 bit, we take the inverse M2,
|
|
25
|
+
which provides a perfect transforms to white from Oklab `[1, 0, 0]` in 32 bit
|
|
26
|
+
floating point. We process the matrix as float 32 bit values and emit them as 64
|
|
27
|
+
bit double values, ~17 digit double accuracy. We then calculate the forward
|
|
28
|
+
matrix. This gives us a transform in 64 bit that drives chroma extremely close
|
|
29
|
+
to zero for 64 bit doubles and maintains the same 32 bit precision of up to about
|
|
30
|
+
7 digits, the 32 bit accuracy limit (~7.22).
|
|
31
|
+
|
|
32
|
+
To demonstrate that our 64 bit converted matrices work as we claim and does not
|
|
33
|
+
alter the intent of the values, we can observe by comparing the documented matrices
|
|
34
|
+
(adjusting for our white point).
|
|
35
|
+
|
|
36
|
+
Below we demonstrate by first using the documented 32 bit M2 matrix (adjusting the
|
|
37
|
+
M1 for our white point). This is what most implementations do, though some may not
|
|
38
|
+
properly correct the M1 matrix for their white point. Notice how the lightness for
|
|
39
|
+
white is only accurate up to about 7 digits making the expected value of 1 not very
|
|
40
|
+
accurate. Also notice that a and b do not resolve as close to 0. The a value is
|
|
41
|
+
pretty good, but the b value is substantially worse. Also notice the first 7 digits
|
|
42
|
+
(the 32 bit precision) for red, green, and blue as they will be used for comparison.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
>>> from coloraide.everything import ColorAll as Color
|
|
46
|
+
>>> import numpy as np
|
|
47
|
+
>>> Color('white').convert('oklab')[:]
|
|
48
|
+
[0.9999999935000001, -1.6653345369377348e-16, 3.729999997759137e-08, 1.0]
|
|
49
|
+
>>> [np.float32(c) for c in Color('red').convert('oklab', norm=False)[:]]
|
|
50
|
+
[0.6279554, 0.22486307, 0.1258463, 1.0]
|
|
51
|
+
>>> [np.float32(c) for c in Color('green').convert('oklab', norm=False)[:]]
|
|
52
|
+
[0.51975185, -0.14030233, 0.107675895, 1.0]
|
|
53
|
+
>>> [np.float32(c) for c in Color('blue').convert('oklab', norm=False)[:]]
|
|
54
|
+
[0.4520137, -0.03245697, -0.31152815, 1.0]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
When we use our 64 bit adjusted M2 matrix, we now get a precise 1 for lightness
|
|
58
|
+
when converting white and get zero or nearly zero for a and b. When comparing the
|
|
59
|
+
first 7 digits to the previous example we get the same values. Anything after
|
|
60
|
+
~7 digits is not guaranteed to be the same.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
>>> from coloraide.everything import ColorAll as Color
|
|
64
|
+
>>> import numpy as np
|
|
65
|
+
>>> Color('white').convert('oklab')[:]
|
|
66
|
+
[1.0, -5.551115123125783e-17, 0.0, 1.0]
|
|
67
|
+
>>> [np.float32(c) for c in Color('red').convert('oklab', norm=False)[:]]
|
|
68
|
+
[0.6279554, 0.22486307, 0.12584628, 1.0]
|
|
69
|
+
>>> [np.float32(c) for c in Color('green').convert('oklab', norm=False)[:]]
|
|
70
|
+
[0.51975185, -0.14030233, 0.10767588, 1.0]
|
|
71
|
+
>>> [np.float32(c) for c in Color('blue').convert('oklab', norm=False)[:]]
|
|
72
|
+
[0.45201373, -0.032456975, -0.31152818, 1.0]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Okhsl is completely calculated using 32 bit floats as that is how the author
|
|
76
|
+
provided the algorithm, but we can see that when we calculate the new coefficients,
|
|
77
|
+
using our M1 and 64 bit adjusted M2 matrices, that we preserve the 32 precision.
|
|
78
|
+
Anything after ~7 digits is just noise due to the differences in 32 bit and 64 bit.
|
|
79
|
+
|
|
80
|
+
Comparing to the actual values returned using the author's code in his Okhsl and Okhsv
|
|
81
|
+
color pickers:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
// Okhsl
|
|
85
|
+
> var value = srgb_to_okhsl(255, 255, 255); value[0] *= 360; value
|
|
86
|
+
[89.87556309590242, 0.5582831888483675, 0.9999999923961898]
|
|
87
|
+
> var value = srgb_to_okhsl(255, 0, 0); value[0] *= 360; value
|
|
88
|
+
[29.23388519234263, 1.0000000001433997, 0.5680846525040862]
|
|
89
|
+
> var value = srgb_to_okhsl(0, 255, 0); value[0] *= 360; value
|
|
90
|
+
[142.49533888780996, 0.9999999700728788, 0.8445289645307816]
|
|
91
|
+
> var value = srgb_to_okhsl(0, 0, 255); value[0] *= 360; value
|
|
92
|
+
[264.052020638055, 0.9999999948631134, 0.3665653394260194]
|
|
93
|
+
|
|
94
|
+
// Okhsv
|
|
95
|
+
> var value = srgb_to_okhsv(255, 255, 255); value[0] *= 360; value
|
|
96
|
+
[89.87556309590242, 1.0347523928230576e-7, 1.000000027003774]
|
|
97
|
+
> var value = srgb_to_okhsv(255, 0, 0); value[0] *= 360; value
|
|
98
|
+
[29.23388519234263, 0.9995219692256989, 1.0000000001685625]
|
|
99
|
+
> var value = srgb_to_okhsv(0, 255, 0); value[0] *= 360; value
|
|
100
|
+
[142.49533888780996, 0.9999997210415695, 0.9999999884428648]
|
|
101
|
+
> var value = srgb_to_okhsv(0, 0, 255); value[0] *= 360; value
|
|
102
|
+
[264.052020638055, 0.9999910912349018, 0.9999999646150918]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
And then ours. Ignoring the authors hue and our hue results for white
|
|
106
|
+
and the oddly high chroma for the author's achromatic white in Okhsl
|
|
107
|
+
(both of which are meaningless in an achromatic color), we can see that
|
|
108
|
+
that we match quite well up to ~7 digits.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
# Okhsl
|
|
112
|
+
>>> Color('white').convert('okhsl', norm=False)[:]
|
|
113
|
+
[180.0, 0.0, 1.0, 1.0]
|
|
114
|
+
>>> Color('#ff0000').convert('okhsl', norm=False)[:]
|
|
115
|
+
[29.233880279627876, 1.0000001765854427, 0.5680846563197033, 1.0]
|
|
116
|
+
>>> Color('#00ff00').convert('okhsl', norm=False)[:]
|
|
117
|
+
[142.4953450414438, 1.0000000000000009, 0.8445289714936317, 1.0]
|
|
118
|
+
>>> [264.05202261637004, 1.0000000005848086, 0.36656533918708145, 1.0]
|
|
119
|
+
[264.05202261637004, 1.0000000005848086, 0.36656533918708145, 1.0]
|
|
120
|
+
# Okhsv
|
|
121
|
+
>>> Color('white').convert('okhsv', norm=False)[:]
|
|
122
|
+
[180.0, 0.0, 1.0, 1.0]
|
|
123
|
+
>>> Color('#ff0000').convert('okhsv', norm=False)[:]
|
|
124
|
+
[29.233880279627876, 1.0000004019360378, 0.9999999999999994, 1.0]
|
|
125
|
+
>>> Color('#00ff00').convert('okhsv', norm=False)[:]
|
|
126
|
+
[142.4953450414438, 0.9999998662471965, 1.0000000000000004, 1.0]
|
|
127
|
+
>>> Color('#0000ff').convert('okhsv', norm=False)[:]
|
|
128
|
+
[264.05202261637004, 1.000000002300706, 0.9999999999999999, 1.0]
|
|
129
|
+
"""
|
|
130
|
+
import sys
|
|
131
|
+
import os
|
|
132
|
+
import struct
|
|
133
|
+
|
|
134
|
+
sys.path.insert(0, os.getcwd())
|
|
135
|
+
|
|
136
|
+
# import tools.calc_xyz_transform as xyzt # noqa: E402
|
|
137
|
+
from coloraide import util # noqa: E402
|
|
138
|
+
from coloraide import algebra as alg # noqa: E402
|
|
139
|
+
|
|
140
|
+
"""Calculate XYZ conversion matrices."""
|
|
141
|
+
import sys
|
|
142
|
+
import os
|
|
143
|
+
|
|
144
|
+
sys.path.insert(0, os.getcwd())
|
|
145
|
+
|
|
146
|
+
xyzt_white_d65 = util.xy_to_xyz((0.31270, 0.32900))
|
|
147
|
+
xyzt_white_d50 = util.xy_to_xyz((0.34570, 0.35850))
|
|
148
|
+
xyzt_white_aces = util.xy_to_xyz((0.32168, 0.33767))
|
|
149
|
+
|
|
150
|
+
def xyzt_get_matrix(wp, space):
|
|
151
|
+
"""Get the matrices for the specified space."""
|
|
152
|
+
|
|
153
|
+
if space == 'srgb':
|
|
154
|
+
x = [0.64, 0.30, 0.15]
|
|
155
|
+
y = [0.33, 0.60, 0.06]
|
|
156
|
+
elif space == 'display-p3':
|
|
157
|
+
x = [0.68, 0.265, 0.150]
|
|
158
|
+
y = [0.32, 0.69, 0.060]
|
|
159
|
+
elif space == 'rec2020':
|
|
160
|
+
x = [0.708, 0.17, 0.131]
|
|
161
|
+
y = [0.292, 0.797, 0.046]
|
|
162
|
+
elif space == 'a98-rgb':
|
|
163
|
+
x = [0.64, 0.21, 0.15]
|
|
164
|
+
y = [0.33, 0.71, 0.06]
|
|
165
|
+
elif space == 'prophoto-rgb':
|
|
166
|
+
x = [0.7347, 0.1596, 0.0366]
|
|
167
|
+
y = [0.2653, 0.8404, 0.0001]
|
|
168
|
+
elif space == 'aces-ap0':
|
|
169
|
+
x = [0.7347, 0.0, 0.0001]
|
|
170
|
+
y = [0.2653, 1.0, -0.0770]
|
|
171
|
+
elif space == 'aces-ap1':
|
|
172
|
+
x = [0.713, 0.165, 0.128]
|
|
173
|
+
y = [0.293, 0.830, 0.044]
|
|
174
|
+
else:
|
|
175
|
+
raise ValueError
|
|
176
|
+
|
|
177
|
+
m = alg.transpose([util.xy_to_xyz(xy) for xy in zip(x, y)])
|
|
178
|
+
rgb = alg.solve(m, wp)
|
|
179
|
+
rgb2xyz = alg.multiply(m, rgb)
|
|
180
|
+
xyz2rgb = alg.inv(rgb2xyz)
|
|
181
|
+
|
|
182
|
+
return rgb2xyz, xyz2rgb
|
|
183
|
+
|
|
184
|
+
float32 = alg.vectorize(lambda value: struct.unpack('f', struct.pack('f', value))[0])
|
|
185
|
+
|
|
186
|
+
# Calculated using our own `calc_xyz_transform.py`
|
|
187
|
+
RGB_TO_XYZ, XYZ_TO_RGB = xyzt_get_matrix(xyzt_white_d65, 'srgb')
|
|
188
|
+
|
|
189
|
+
# Matrix provided by the author of Oklab to allow for calculating a precise M1 matrix
|
|
190
|
+
# using any white point.
|
|
191
|
+
M0 = [
|
|
192
|
+
[0.77849780, 0.34399940, -0.12249720],
|
|
193
|
+
[0.03303601, 0.93076195, 0.03620204],
|
|
194
|
+
[0.05092917, 0.27933344, 0.66973739]
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
# Calculate XYZ to LMS and LMS to XYZ using our white point.
|
|
198
|
+
XYZ_TO_LMS = alg.divide(M0, alg.outer(alg.matmul(M0, xyzt_white_d65), alg.ones(3)))
|
|
199
|
+
XYZD50_TO_LMS = alg.divide(M0, alg.outer(alg.matmul(M0, xyzt_white_d50), alg.ones(3)))
|
|
200
|
+
|
|
201
|
+
# Calculate the inverse
|
|
202
|
+
LMS_TO_XYZ = alg.inv(XYZ_TO_LMS)
|
|
203
|
+
LMS_TO_XYZD50 = alg.inv(XYZD50_TO_LMS)
|
|
204
|
+
|
|
205
|
+
# Calculate linear sRGB to LMS (used for Okhsl and Okhsv)
|
|
206
|
+
SRGBL_TO_LMS = alg.matmul(XYZ_TO_LMS, RGB_TO_XYZ)
|
|
207
|
+
LMS_TO_SRGBL = alg.inv(SRGBL_TO_LMS)
|
|
208
|
+
|
|
209
|
+
# Oklab specifies the following matrix as M1 along with the inverse.
|
|
210
|
+
# ```
|
|
211
|
+
# LMS3_TO_OKLAB = [
|
|
212
|
+
# [0.2104542553, 0.7936177850, -0.0040720468],
|
|
213
|
+
# [1.9779984951, -2.4285922050, 0.4505937099],
|
|
214
|
+
# [0.0259040371, 0.7827717662, -0.8086757660]
|
|
215
|
+
# ]
|
|
216
|
+
# ```
|
|
217
|
+
# But since the matrix is provided in 32 bit, we are not able to get the
|
|
218
|
+
# proper inverse for `[1, 0, 0]` in 64 bit, even if we calculate the a
|
|
219
|
+
# new 64 bit inverse for the above forward transform. What we need is a
|
|
220
|
+
# proper 64 bit forward and reverse transform.
|
|
221
|
+
#
|
|
222
|
+
# In order to adjust for this, we take documented 32 bit inverse matrix which
|
|
223
|
+
# gives us a perfect translation from Oklab `[1, 0, 0]` to LMS of `[1, 1, 1]`
|
|
224
|
+
# and parse the matrix as float 32 and emit it as 64 bit and then take the inverse.
|
|
225
|
+
OKLAB_TO_LMS3 = float32([
|
|
226
|
+
[1.0, 0.3963377774, 0.2158037573],
|
|
227
|
+
[1.0, -0.1055613458, -0.0638541728],
|
|
228
|
+
[1.0, -0.0894841775, -1.2914855480]
|
|
229
|
+
])
|
|
230
|
+
|
|
231
|
+
# Calculate the inverse
|
|
232
|
+
LMS3_TO_OKLAB = alg.inv(OKLAB_TO_LMS3)
|
|
233
|
+
|