@texel/color 1.1.1 → 1.1.3
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/core.js +30 -18
- package/src/gamut.js +4 -2
- package/test/spaces/hsl.js +5 -0
- package/test/spaces/lab.js +3 -1
- package/test/test.js +108 -36
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ The return value is the new coordinates in the destination space; such as `[r,g,
|
|
|
85
85
|
|
|
86
86
|
#### `output = gamutMapOKLCH(oklch, gamut = sRGBGamut, targetSpace = gamut.space, out = [0, 0, 0], mapping = MapToCuspL, [cusp])`
|
|
87
87
|
|
|
88
|
-
Performs fast gamut mapping in OKLCH as [described by Björn
|
|
88
|
+
Performs fast gamut mapping in OKLCH as [described by Björn Ottosson](https://bottosson.github.io/posts/gamutclipping/) (2021). This takes an input `[l,c,h]` coords in OKLCH space, and ensures the final result will lie within the specified color `gamut` (default `sRGBGamut`). You can further specify a different target space (which default's to the gamut's space), for example to get a linear-light sRGB and avoid the transfer function, or to keep the result in OKLCH:
|
|
89
89
|
|
|
90
90
|
```js
|
|
91
91
|
import { gamutMapOKLCH, sRGBGamut, sRGBLinear, OKLCH } from "@texel/color";
|
package/package.json
CHANGED
package/src/core.js
CHANGED
|
@@ -64,6 +64,7 @@ export const serialize = (input, inputSpace, outputSpace = inputSpace) => {
|
|
|
64
64
|
}
|
|
65
65
|
const id = outputSpace.id;
|
|
66
66
|
if (id == "srgb") {
|
|
67
|
+
// uses the legacy rgb() format
|
|
67
68
|
const r = floatToByte(tmp3[0]);
|
|
68
69
|
const g = floatToByte(tmp3[1]);
|
|
69
70
|
const b = floatToByte(tmp3[2]);
|
|
@@ -72,7 +73,8 @@ export const serialize = (input, inputSpace, outputSpace = inputSpace) => {
|
|
|
72
73
|
} else {
|
|
73
74
|
const alphaSuffix = alpha === 1 ? "" : ` / ${alpha}`;
|
|
74
75
|
if (id == "oklab" || id == "oklch") {
|
|
75
|
-
|
|
76
|
+
// older versions of Safari don't support oklch with 0..1 L but do support %
|
|
77
|
+
return `${id}(${tmp3[0] * 100}% ${tmp3[1]} ${tmp3[2]}${alphaSuffix})`;
|
|
76
78
|
} else {
|
|
77
79
|
return `color(${id} ${tmp3[0]} ${tmp3[1]} ${tmp3[2]}${alphaSuffix})`;
|
|
78
80
|
}
|
|
@@ -84,6 +86,13 @@ const stripAlpha = (coords) => {
|
|
|
84
86
|
return coords;
|
|
85
87
|
};
|
|
86
88
|
|
|
89
|
+
const parseFloatValue = str => parseFloat(str) || 0;
|
|
90
|
+
|
|
91
|
+
const parseColorValue = (str, is255 = false) => {
|
|
92
|
+
if (is255) return clamp(parseFloatValue(str) / 0xff, 0, 0xff);
|
|
93
|
+
else return str.includes('%') ? parseFloatValue(str) / 100 : parseFloatValue(str);
|
|
94
|
+
};
|
|
95
|
+
|
|
87
96
|
export const deserialize = (input) => {
|
|
88
97
|
if (typeof input !== "string") {
|
|
89
98
|
throw new Error(`expected a string as input`);
|
|
@@ -105,26 +114,19 @@ export const deserialize = (input) => {
|
|
|
105
114
|
throw new Error(`could not parse color string ${input}`);
|
|
106
115
|
}
|
|
107
116
|
const fn = parts[1].toLowerCase();
|
|
108
|
-
if (/^rgba?$/i.test(fn)) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.map((v, i) =>
|
|
113
|
-
i < 3 ? clamp(parseInt(v, 10) || 0, 0, 255) / 255 : parseFloat(v)
|
|
114
|
-
);
|
|
115
|
-
const expectedLen = hasAlpha ? 4 : 3;
|
|
116
|
-
if (coords.length !== expectedLen) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`got ${fn} with incorrect number of coords, expected ${expectedLen}`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
117
|
+
if (/^rgba?$/i.test(fn) && parts[2].includes(',')) {
|
|
118
|
+
const coords = parts[2].split(',').map((v, i) => {
|
|
119
|
+
return parseColorValue(v.trim(), i < 3)
|
|
120
|
+
});
|
|
121
121
|
return {
|
|
122
122
|
id: "srgb",
|
|
123
123
|
coords: stripAlpha(coords),
|
|
124
124
|
};
|
|
125
125
|
} else {
|
|
126
126
|
let id, coordsStrings;
|
|
127
|
-
|
|
127
|
+
let div255 = false;
|
|
128
|
+
|
|
129
|
+
if (/^color$/i.test(fn)) {
|
|
128
130
|
const params =
|
|
129
131
|
/([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s/]+)(?:\s?\/\s?([^\s]+))?/.exec(
|
|
130
132
|
parts[2]
|
|
@@ -133,8 +135,15 @@ export const deserialize = (input) => {
|
|
|
133
135
|
throw new Error(`could not parse color() function ${input}`);
|
|
134
136
|
id = params[1].toLowerCase();
|
|
135
137
|
coordsStrings = params.slice(2, 6);
|
|
136
|
-
} else
|
|
137
|
-
|
|
138
|
+
} else {
|
|
139
|
+
if (/^(oklab|oklch)$/i.test(fn)) {
|
|
140
|
+
id = fn;
|
|
141
|
+
} else if (/rgba?/i.test(fn)) {
|
|
142
|
+
id = 'srgb';
|
|
143
|
+
div255 = true;
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error(`unknown color function ${fn}`);
|
|
146
|
+
}
|
|
138
147
|
const params =
|
|
139
148
|
/([^\s]+)\s+([^\s]+)\s+([^\s/]+)(?:\s?\/\s?([^\s]+))?/.exec(parts[2]);
|
|
140
149
|
if (!params)
|
|
@@ -146,7 +155,9 @@ export const deserialize = (input) => {
|
|
|
146
155
|
coordsStrings = coordsStrings.slice(0, 3);
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
const coords = coordsStrings.map((f) =>
|
|
158
|
+
const coords = coordsStrings.map((f, i) => {
|
|
159
|
+
return parseColorValue(f.trim(), div255 && i < 3);
|
|
160
|
+
});
|
|
150
161
|
if (coords.length < 3 || coords.length > 4)
|
|
151
162
|
throw new Error(`invalid number of coordinates`);
|
|
152
163
|
return {
|
|
@@ -156,6 +167,7 @@ export const deserialize = (input) => {
|
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
};
|
|
170
|
+
|
|
159
171
|
export const parse = (input, targetSpace, out = vec3()) => {
|
|
160
172
|
if (!targetSpace)
|
|
161
173
|
throw new Error(`must specify a target space to parse into`);
|
package/src/gamut.js
CHANGED
|
@@ -182,13 +182,15 @@ export const findGamutIntersectionOKLCH = (a, b, l1, c1, l0, cusp, gamut) => {
|
|
|
182
182
|
|
|
183
183
|
// Find the intersection for upper and lower half separately
|
|
184
184
|
if ((l1 - l0) * cusp[1] - (cusp[0] - l0) * c1 <= 0.0) {
|
|
185
|
+
const denom = (c1 * cusp[0] + cusp[1] * (l0 - l1));
|
|
185
186
|
// Lower half
|
|
186
|
-
t =
|
|
187
|
+
t = denom === 0 ? 0 : (cusp[1] * l0) / denom;
|
|
187
188
|
} else {
|
|
188
189
|
// Upper half
|
|
189
190
|
|
|
190
191
|
// First intersect with triangle
|
|
191
|
-
|
|
192
|
+
const denom = (c1 * (cusp[0] - 1.0) + cusp[1] * (l0 - l1));
|
|
193
|
+
t = denom === 0 ? 0 : (cusp[1] * (l0 - 1.0)) / denom;
|
|
192
194
|
|
|
193
195
|
// Then one step Halley's method
|
|
194
196
|
let dl = l1 - l0;
|
package/test/spaces/hsl.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
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
|
+
|
|
1
6
|
import { sRGB, sRGBLinear } from "../../src/index.js";
|
|
2
7
|
|
|
3
8
|
export const HSL = {
|
package/test/spaces/lab.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Lab aka CIELAB aka L*a*b* (uses a D50 WHITE_D50 point and has to be adapted)
|
|
2
2
|
// refer to CSS Color Module Level 4 Spec for more details
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// Reference:
|
|
5
|
+
// https://github.com/color-js/color.js/blob/cfe55d358adb6c2e23c8a897282adf42904fd32d/src/spaces/lab.js
|
|
4
6
|
import { D50_to_D65_M, D65_to_D50_M } from "../../src/index.js";
|
|
5
7
|
|
|
6
8
|
// K * e = 2^3 = 8
|
package/test/test.js
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
DisplayP3Gamut,
|
|
45
45
|
deserialize,
|
|
46
46
|
parse,
|
|
47
|
+
MapToL,
|
|
47
48
|
} from "../src/index.js";
|
|
48
49
|
|
|
49
50
|
test("should convert XYZ in different whitepoints", async (t) => {
|
|
@@ -230,61 +231,98 @@ test("should serialize", async (t) => {
|
|
|
230
231
|
t.deepEqual(serialize([0, 0.5, 1], sRGB), "rgb(0, 128, 255)");
|
|
231
232
|
t.deepEqual(serialize([0, 0.5, 1], sRGBLinear), "color(srgb-linear 0 0.5 1)");
|
|
232
233
|
t.deepEqual(serialize([1, 0, 0], OKLCH, sRGB), "rgb(255, 255, 255)");
|
|
233
|
-
t.deepEqual(serialize([1, 0, 0], OKLCH), "oklch(
|
|
234
|
-
t.deepEqual(serialize([1, 0, 0], OKLab), "oklab(
|
|
234
|
+
t.deepEqual(serialize([1, 0, 0], OKLCH), "oklch(100% 0 0)");
|
|
235
|
+
t.deepEqual(serialize([1, 0, 0], OKLab), "oklab(100% 0 0)");
|
|
235
236
|
t.deepEqual(
|
|
236
237
|
serialize([1, 0, 0, 0.4523], OKLCH, sRGB),
|
|
237
238
|
"rgba(255, 255, 255, 0.4523)"
|
|
238
239
|
);
|
|
239
240
|
t.deepEqual(
|
|
240
241
|
serialize([1, 0, 0, 0.4523], OKLCH, OKLCH),
|
|
241
|
-
"oklch(
|
|
242
|
+
"oklch(100% 0 0 / 0.4523)"
|
|
242
243
|
);
|
|
243
|
-
t.deepEqual(serialize([1, 0, 0, 0.4523], OKLCH), "oklch(
|
|
244
|
+
t.deepEqual(serialize([1, 0, 0, 0.4523], OKLCH), "oklch(100% 0 0 / 0.4523)");
|
|
244
245
|
t.deepEqual(
|
|
245
246
|
serialize([1, 0, 0, 0.4523], DisplayP3),
|
|
246
247
|
"color(display-p3 1 0 0 / 0.4523)"
|
|
247
248
|
);
|
|
248
249
|
});
|
|
249
250
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
251
|
+
// not yet finished
|
|
252
|
+
// test("should parse to a color coord", async (t) => {
|
|
253
|
+
// t.deepEqual(parse("rgb(0, 128, 255)", sRGB), [0, 128 / 0xff, 255 / 0xff]);
|
|
254
|
+
// t.deepEqual(parse("rgba(0, 128, 255, .25)", sRGB), [
|
|
255
|
+
// 0,
|
|
256
|
+
// 128 / 0xff,
|
|
257
|
+
// 255 / 0xff,
|
|
258
|
+
// 0.25,
|
|
259
|
+
// ]);
|
|
260
|
+
// let outVec = [0, 0, 0];
|
|
261
|
+
// t.deepEqual(parse("rgba(0, 128, 255, 1)", sRGB), [0, 128 / 0xff, 255 / 0xff]);
|
|
262
|
+
// let out;
|
|
263
|
+
// out = parse("rgba(0, 128, 255, 1)", sRGB, outVec);
|
|
264
|
+
// t.deepEqual(out, [0, 128 / 0xff, 255 / 0xff]);
|
|
265
|
+
// t.equal(out, outVec);
|
|
266
|
+
|
|
267
|
+
// // trims to 3
|
|
268
|
+
// outVec = [0, 0, 0, 0];
|
|
269
|
+
// out = parse("rgba(0, 128, 255, 1)", sRGB, outVec);
|
|
270
|
+
// t.deepEqual(out, [0, 128 / 0xff, 255 / 0xff]);
|
|
271
|
+
// t.equal(out, outVec);
|
|
272
|
+
|
|
273
|
+
// // ensures 4
|
|
274
|
+
// outVec = [0, 0, 0, 0];
|
|
275
|
+
// out = parse("rgba(0, 128, 255, 0.91)", sRGB, outVec);
|
|
276
|
+
// t.deepEqual(out, [0, 128 / 0xff, 255 / 0xff, 0.91]);
|
|
277
|
+
// t.equal(out, outVec);
|
|
278
|
+
|
|
279
|
+
// t.deepEqual(
|
|
280
|
+
// serialize(parse("oklch(1 0 0)", sRGB), sRGB),
|
|
281
|
+
// "rgb(255, 255, 255)"
|
|
282
|
+
// );
|
|
283
|
+
// });
|
|
282
284
|
|
|
283
285
|
test("should deserialize color string information", async (t) => {
|
|
284
286
|
t.deepEqual(deserialize("rgb(0, 128, 255)"), {
|
|
285
287
|
coords: [0, 128 / 0xff, 255 / 0xff],
|
|
286
288
|
id: "srgb",
|
|
287
289
|
});
|
|
290
|
+
t.deepEqual(deserialize("rgba(0, 128, 255)"), {
|
|
291
|
+
coords: [0, 128 / 0xff, 255 / 0xff],
|
|
292
|
+
id: "srgb",
|
|
293
|
+
});
|
|
294
|
+
t.deepEqual(deserialize("rgba(0, 128, 255, 50%)"), {
|
|
295
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 0.5],
|
|
296
|
+
id: "srgb",
|
|
297
|
+
});
|
|
298
|
+
t.deepEqual(deserialize("rgb(0, 128, 255, 0.5)"), {
|
|
299
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 0.5],
|
|
300
|
+
id: "srgb",
|
|
301
|
+
});
|
|
302
|
+
t.deepEqual(deserialize("rgb(0 128 255)"), {
|
|
303
|
+
coords: [0, 128 / 0xff, 255 / 0xff],
|
|
304
|
+
id: "srgb",
|
|
305
|
+
});
|
|
306
|
+
t.deepEqual(deserialize("rgb(0 128 255 / 0.5)"), {
|
|
307
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 0.5],
|
|
308
|
+
id: "srgb",
|
|
309
|
+
});
|
|
310
|
+
t.deepEqual(deserialize("rgb(0 128 255 / 1e-2)"), {
|
|
311
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 1e-2],
|
|
312
|
+
id: "srgb",
|
|
313
|
+
});
|
|
314
|
+
t.deepEqual(deserialize("rgb(0 128 255 / 50%)"), {
|
|
315
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 0.5],
|
|
316
|
+
id: "srgb",
|
|
317
|
+
});
|
|
318
|
+
t.deepEqual(deserialize("rgb(0 128 255 / 0.35)"), {
|
|
319
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 0.35],
|
|
320
|
+
id: "srgb",
|
|
321
|
+
});
|
|
322
|
+
t.deepEqual(deserialize("RGBA(0 128 255 / 0.35)"), {
|
|
323
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 0.35],
|
|
324
|
+
id: "srgb",
|
|
325
|
+
});
|
|
288
326
|
t.deepEqual(deserialize("rgba(0, 128, 255, 0.35)"), {
|
|
289
327
|
coords: [0, 128 / 0xff, 255 / 0xff, 0.35],
|
|
290
328
|
id: "srgb",
|
|
@@ -297,10 +335,22 @@ test("should deserialize color string information", async (t) => {
|
|
|
297
335
|
id: "srgb",
|
|
298
336
|
coords: [1, 0, 0.8, 0.8],
|
|
299
337
|
});
|
|
338
|
+
t.deepEqual(deserialize("COLOR(sRGB-Linear 0 0.5 1)"), {
|
|
339
|
+
id: "srgb-linear",
|
|
340
|
+
coords: [0, 0.5, 1],
|
|
341
|
+
});
|
|
342
|
+
t.deepEqual(deserialize("COLOR(sRGB-Linear 0 50% 1)"), {
|
|
343
|
+
id: "srgb-linear",
|
|
344
|
+
coords: [0, 0.5, 1],
|
|
345
|
+
});
|
|
300
346
|
t.deepEqual(deserialize("color(srgb-linear 0 0.5 1)"), {
|
|
301
347
|
id: "srgb-linear",
|
|
302
348
|
coords: [0, 0.5, 1],
|
|
303
349
|
});
|
|
350
|
+
t.deepEqual(deserialize("color(srgb-linear 0 1e-2 1)"), {
|
|
351
|
+
id: "srgb-linear",
|
|
352
|
+
coords: [0, 1e-2, 1],
|
|
353
|
+
});
|
|
304
354
|
t.deepEqual(deserialize("color(srgb-linear 0 0.5 1/0.25)"), {
|
|
305
355
|
id: "srgb-linear",
|
|
306
356
|
coords: [0, 0.5, 1, 0.25],
|
|
@@ -401,6 +451,28 @@ test("should handle problematic coords", async (t) => {
|
|
|
401
451
|
),
|
|
402
452
|
true
|
|
403
453
|
);
|
|
454
|
+
|
|
455
|
+
t.deepEqual(
|
|
456
|
+
convert([1, 0, 0], OKLCH, OKLab, undefined),
|
|
457
|
+
[1, 0, 0],
|
|
458
|
+
"handles [1,0,0] OKLCH to OKLab gamut map"
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
t.deepEqual(
|
|
462
|
+
arrayAlmostEqual(convert([1, 0, 0], OKLCH, sRGBLinear), [1, 1, 1]),
|
|
463
|
+
true,
|
|
464
|
+
"handles [1,1,1] OKLCH to sRGBLinear"
|
|
465
|
+
);
|
|
466
|
+
t.deepEqual(
|
|
467
|
+
gamutMapOKLCH([1, 0, 0], sRGBGamut, OKLCH, undefined, MapToL),
|
|
468
|
+
[1, 0, 0],
|
|
469
|
+
"handles [1,0,0] OKLCH to sRGB gamut map"
|
|
470
|
+
);
|
|
471
|
+
t.deepEqual(
|
|
472
|
+
gamutMapOKLCH([0, 0, 0], sRGBGamut, OKLCH, undefined, MapToL),
|
|
473
|
+
[0, 0, 0],
|
|
474
|
+
"handles [0,0,0] OKLCH to sRGB gamut map"
|
|
475
|
+
);
|
|
404
476
|
});
|
|
405
477
|
|
|
406
478
|
function roundToNDecimals(value, digits) {
|