@texel/color 1.0.4 → 1.0.5
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 +25 -3
- package/package.json +1 -1
- package/src/core.js +115 -9
- package/src/gamut.js +1 -1
- package/test/test.js +88 -0
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ gamutMapOKLCH(oklch, sRGBGamut, sRGB, rgb, MapToL);
|
|
|
116
116
|
|
|
117
117
|
The `cusp` can also be passed as the last parameter, allowing for faster evaluation for known hues. See below for calculating the cusp.
|
|
118
118
|
|
|
119
|
-
>
|
|
119
|
+
> **Note:** If you map to an OKLab-based target (OKLCH, OKHSL etc), the final step of RGB clipping will be skipped. This produces more predictable OKLab and OKLCH based results, but you will likely want to perform a final clampedRGB() step when converting to a displayable color.
|
|
120
120
|
|
|
121
121
|
#### `LC = findCuspOKLCH(a, b, gamut, out = [0, 0])`
|
|
122
122
|
|
|
@@ -147,19 +147,35 @@ gamutMapOKLCH(oklch, gamut, gamut.space, out, MapToCuspL, cuspLC);
|
|
|
147
147
|
|
|
148
148
|
The `a` and `b` can also be from OKLab coordinates, but must be normalized so `a^2 + b^2 == 1`.
|
|
149
149
|
|
|
150
|
-
#### `str = serialize(coords, inputSpace
|
|
150
|
+
#### `str = serialize(coords, inputSpace, outputSpace = inputSpace)`
|
|
151
151
|
|
|
152
|
-
Turns the specified `coords` (assumed to be in `inputSpace`) into a string, first converting if needed to the specified `outputSpace`. If the space is sRGB, a plain `rgb(r,g,b)` string (in bytes) will be used for browser compatibility and performance, otherwise a CSS color string will be returned. Note that not all spaces, such as certain linear spaces, are currently supported by CSS.
|
|
152
|
+
Turns the specified `coords` (assumed to be in `inputSpace`) into a string, first converting if needed to the specified `outputSpace`. If the space is sRGB, a plain `rgb(r,g,b)` string (in bytes) will be used for browser compatibility and performance, otherwise a CSS color string will be returned. Note that not all spaces, such as certain linear spaces, are currently supported by CSS. You can optionally pass an `alpha` component (0..1 range) as the fourth element in the `coords` array for it to be considered.
|
|
153
153
|
|
|
154
154
|
```js
|
|
155
155
|
import { serialize, sRGB, DisplayP3, OKLCH } from "@texel/color";
|
|
156
156
|
|
|
157
157
|
serialize([0, 0.5, 1], sRGB); // "rgb(0, 128, 255)"
|
|
158
|
+
serialize([0, 0.5, 1, 0.5], sRGB); // "rgba(0, 128, 255, 0.5)"
|
|
158
159
|
serialize([0, 0.5, 1], DisplayP3); // "color(display-p3 0 0.5 1)"
|
|
160
|
+
serialize([0, 0.5, 1, 0.35], DisplayP3); // "color(display-p3 0 0.5 1 / 0.35)"
|
|
159
161
|
serialize([1, 0, 0], OKLCH, sRGB); // "rgb(255, 255, 255)"
|
|
160
162
|
serialize([1, 0, 0], OKLCH); // "oklch(1 0 0)"
|
|
161
163
|
```
|
|
162
164
|
|
|
165
|
+
#### `coords = deserialize(colorString)`
|
|
166
|
+
|
|
167
|
+
The inverse of `serialize`, this will take a string and determine the color space `id` it is referencing, and the 3 or 4 (for alpha) `coords`. This is intentionally limited in functionality, only supporting hex RGB, `rgb()` and `rgba()` bytes, and `oklch()`, `oklab()`, and plain `color()` functions with no modifiers.
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
import { deserialize } from "@texel/color";
|
|
171
|
+
|
|
172
|
+
const { coords, id } = deserialize("color(display-p3 0 0.5 1 / 0.35)");
|
|
173
|
+
console.log(id); // "display-p3"
|
|
174
|
+
console.log(coords); // [ 0, 0.5, 1, 0.35 ]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> **Note:** Parsing is still a WIP area of API design, and complex CSS color string handling is not within the scope of this library.
|
|
178
|
+
|
|
163
179
|
#### `delta = deltaEOK(oklabA, oklabB)`
|
|
164
180
|
|
|
165
181
|
Performs a color difference in OKLab space between two coordinates. As this is a perceptually uniform color space that improves upon CIELAB and its flaws, it should be suitable as a replacement for the CIEDE2000 color difference equation in many situations.
|
|
@@ -293,6 +309,8 @@ import {
|
|
|
293
309
|
LMS_to_XYZ_M,
|
|
294
310
|
XYZ_to_LMS_M,
|
|
295
311
|
sRGB,
|
|
312
|
+
OKHSLToOKLab,
|
|
313
|
+
DisplayP3Gamut,
|
|
296
314
|
} from "@texel/color";
|
|
297
315
|
|
|
298
316
|
console.log(XYZ_to_linear_sRGB_M); // [ [a,b,c], ... ]
|
|
@@ -301,6 +319,10 @@ OKLab_from(xyzD65, XYZ_to_LMS_M); // XYZ D65 -> OKLab
|
|
|
301
319
|
transform(xyzD65, XYZ_to_linear_sRGB_M); // XYZ D65 -> sRGBLinear
|
|
302
320
|
sRGB.fromBase(in_linear_sRGB, out_sRGB); // linear to gamma transfer function
|
|
303
321
|
sRGB.toBase(in_sRGB, out_linear_sRGB); // linear to gamma transfer function
|
|
322
|
+
|
|
323
|
+
// OKHSL in a non-sRGB gamut
|
|
324
|
+
// also see OKHSVToOKLab and their inverse functions
|
|
325
|
+
OKHSLToOKLab([h, s, l], DisplayP3Gamut, optionalOutVec);
|
|
304
326
|
```
|
|
305
327
|
|
|
306
328
|
## Notes
|
package/package.json
CHANGED
package/src/core.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { floatToByte, vec3 } from "./util.js";
|
|
1
|
+
import { clamp, floatToByte, hexToRGB, vec3 } from "./util.js";
|
|
2
2
|
import { LMS_to_OKLab_M, OKLab_to_LMS_M } from "./conversion_matrices.js";
|
|
3
|
-
import { XYZ } from "./spaces.js";
|
|
3
|
+
import { listColorSpaces, sRGB, XYZ } from "./spaces.js";
|
|
4
4
|
|
|
5
5
|
const tmp3 = vec3();
|
|
6
6
|
|
|
@@ -51,25 +51,131 @@ const vec3Copy = (input, output) => {
|
|
|
51
51
|
|
|
52
52
|
export const serialize = (input, inputSpace, outputSpace = inputSpace) => {
|
|
53
53
|
if (!inputSpace) throw new Error(`must specify an input space`);
|
|
54
|
+
// extract alpha if present
|
|
55
|
+
let alpha = 1;
|
|
56
|
+
if (input.length > 3) {
|
|
57
|
+
alpha = input[3];
|
|
58
|
+
}
|
|
59
|
+
// copy into temp
|
|
60
|
+
vec3Copy(input, tmp3);
|
|
61
|
+
// convert if needed
|
|
54
62
|
if (inputSpace !== outputSpace) {
|
|
55
63
|
convert(input, inputSpace, outputSpace, tmp3);
|
|
56
|
-
} else {
|
|
57
|
-
vec3Copy(input, tmp3);
|
|
58
64
|
}
|
|
59
|
-
|
|
60
65
|
const id = outputSpace.id;
|
|
61
66
|
if (id == "srgb") {
|
|
62
67
|
const r = floatToByte(tmp3[0]);
|
|
63
68
|
const g = floatToByte(tmp3[1]);
|
|
64
69
|
const b = floatToByte(tmp3[2]);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return `${id}(${tmp3[0]} ${tmp3[1]} ${tmp3[2]})`;
|
|
70
|
+
const rgb = `${r}, ${g}, ${b}`;
|
|
71
|
+
return alpha === 1 ? `rgb(${rgb})` : `rgba(${rgb}, ${alpha})`;
|
|
68
72
|
} else {
|
|
69
|
-
|
|
73
|
+
const alphaSuffix = alpha === 1 ? "" : ` / ${alpha}`;
|
|
74
|
+
if (id == "oklab" || id == "oklch") {
|
|
75
|
+
return `${id}(${tmp3[0]} ${tmp3[1]} ${tmp3[2]}${alphaSuffix})`;
|
|
76
|
+
} else {
|
|
77
|
+
return `color(${id} ${tmp3[0]} ${tmp3[1]} ${tmp3[2]}${alphaSuffix})`;
|
|
78
|
+
}
|
|
70
79
|
}
|
|
71
80
|
};
|
|
72
81
|
|
|
82
|
+
const stripAlpha = (coords) => {
|
|
83
|
+
if (coords.length >= 4 && coords[3] === 1) return coords.slice(0, 3);
|
|
84
|
+
return coords;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const deserialize = (input) => {
|
|
88
|
+
if (typeof input !== "string") {
|
|
89
|
+
throw new Error(`expected a string as input`);
|
|
90
|
+
}
|
|
91
|
+
input = input.trim();
|
|
92
|
+
if (input.charAt(0) === "#") {
|
|
93
|
+
const rgbIn = input.slice(0, 7);
|
|
94
|
+
let alphaByte = input.length > 7 ? parseInt(input.slice(7, 9), 16) : 255;
|
|
95
|
+
let alpha = isNaN(alphaByte) ? 1 : alphaByte / 255;
|
|
96
|
+
const coords = hexToRGB(rgbIn);
|
|
97
|
+
if (alpha !== 1) coords.push(alpha);
|
|
98
|
+
return {
|
|
99
|
+
id: "srgb",
|
|
100
|
+
coords,
|
|
101
|
+
};
|
|
102
|
+
} else {
|
|
103
|
+
const parts = /^(rgb|rgba|oklab|oklch|color)\((.+)\)$/i.exec(input);
|
|
104
|
+
if (!parts) {
|
|
105
|
+
throw new Error(`could not parse color string ${input}`);
|
|
106
|
+
}
|
|
107
|
+
const fn = parts[1].toLowerCase();
|
|
108
|
+
if (/^rgba?$/i.test(fn)) {
|
|
109
|
+
const hasAlpha = fn == "rgba";
|
|
110
|
+
const coords = parts[2]
|
|
111
|
+
.split(",")
|
|
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
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
id: "srgb",
|
|
123
|
+
coords: stripAlpha(coords),
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
let id, coordsStrings;
|
|
127
|
+
if (fn === "color") {
|
|
128
|
+
const params =
|
|
129
|
+
/([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s/]+)(?:\s?\/\s?([^\s]+))?/.exec(
|
|
130
|
+
parts[2]
|
|
131
|
+
);
|
|
132
|
+
if (!params)
|
|
133
|
+
throw new Error(`could not parse color() function ${input}`);
|
|
134
|
+
id = params[1].toLowerCase();
|
|
135
|
+
coordsStrings = params.slice(2, 6);
|
|
136
|
+
} else if (/^(oklab|oklch)$/.test(fn)) {
|
|
137
|
+
id = fn;
|
|
138
|
+
const params =
|
|
139
|
+
/([^\s]+)\s+([^\s]+)\s+([^\s/]+)(?:\s?\/\s?([^\s]+))?/.exec(parts[2]);
|
|
140
|
+
if (!params)
|
|
141
|
+
throw new Error(`could not parse color() function ${input}`);
|
|
142
|
+
coordsStrings = params.slice(1, 6);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (coordsStrings[3] == null) {
|
|
146
|
+
coordsStrings = coordsStrings.slice(0, 3);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const coords = coordsStrings.map((f) => parseFloat(f));
|
|
150
|
+
if (coords.length < 3 || coords.length > 4)
|
|
151
|
+
throw new Error(`invalid number of coordinates`);
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
coords: stripAlpha(coords),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
export const parse = (input, targetSpace, out = vec3()) => {
|
|
160
|
+
if (!targetSpace)
|
|
161
|
+
throw new Error(`must specify a target space to parse into`);
|
|
162
|
+
|
|
163
|
+
const { coords, id } = deserialize(input);
|
|
164
|
+
const space = listColorSpaces().find((f) => id === f.id);
|
|
165
|
+
if (!space) throw new Error(`could not find space with the id ${id}`);
|
|
166
|
+
const alpha = coords.length === 4 ? coords[3] : 1;
|
|
167
|
+
|
|
168
|
+
// copy 3D coords to output and convert
|
|
169
|
+
vec3Copy(coords, out);
|
|
170
|
+
convert(out, space, targetSpace, out);
|
|
171
|
+
|
|
172
|
+
// store alpha
|
|
173
|
+
if (alpha !== 1) out[3] = alpha;
|
|
174
|
+
// reduce to 3D
|
|
175
|
+
if (alpha == 1 && out.length === 4) out.pop();
|
|
176
|
+
return out;
|
|
177
|
+
};
|
|
178
|
+
|
|
73
179
|
export const convert = (input, fromSpace, toSpace, out = vec3()) => {
|
|
74
180
|
// place into output
|
|
75
181
|
vec3Copy(input, out);
|
package/src/gamut.js
CHANGED
|
@@ -334,7 +334,7 @@ export const gamutMapOKLCH = (
|
|
|
334
334
|
// will be applied. The OKLCH result is _basically_ in gamut, but not exactly; you'll need to clip at final stage.
|
|
335
335
|
// I've documented this behaviour in the readme.
|
|
336
336
|
const targetSpaceBase = targetSpace.base ?? targetSpace;
|
|
337
|
-
if (targetSpaceBase ==
|
|
337
|
+
if (targetSpaceBase.id == "oklab") {
|
|
338
338
|
return convert(out, OKLCH, targetSpace, out);
|
|
339
339
|
}
|
|
340
340
|
|
package/test/test.js
CHANGED
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
A98RGBLinear,
|
|
43
43
|
XYZ_to_linear_A98RGB_M,
|
|
44
44
|
DisplayP3Gamut,
|
|
45
|
+
deserialize,
|
|
46
|
+
parse,
|
|
45
47
|
} from "../src/index.js";
|
|
46
48
|
|
|
47
49
|
test("should convert XYZ in different whitepoints", async (t) => {
|
|
@@ -229,6 +231,92 @@ test("should serialize", async (t) => {
|
|
|
229
231
|
t.deepEqual(serialize([0, 0.5, 1], sRGBLinear), "color(srgb-linear 0 0.5 1)");
|
|
230
232
|
t.deepEqual(serialize([1, 0, 0], OKLCH, sRGB), "rgb(255, 255, 255)");
|
|
231
233
|
t.deepEqual(serialize([1, 0, 0], OKLCH), "oklch(1 0 0)");
|
|
234
|
+
t.deepEqual(serialize([1, 0, 0], OKLab), "oklab(1 0 0)");
|
|
235
|
+
t.deepEqual(
|
|
236
|
+
serialize([1, 0, 0, 0.4523], OKLCH, sRGB),
|
|
237
|
+
"rgba(255, 255, 255, 0.4523)"
|
|
238
|
+
);
|
|
239
|
+
t.deepEqual(
|
|
240
|
+
serialize([1, 0, 0, 0.4523], OKLCH, OKLCH),
|
|
241
|
+
"oklch(1 0 0 / 0.4523)"
|
|
242
|
+
);
|
|
243
|
+
t.deepEqual(serialize([1, 0, 0, 0.4523], OKLCH), "oklch(1 0 0 / 0.4523)");
|
|
244
|
+
t.deepEqual(
|
|
245
|
+
serialize([1, 0, 0, 0.4523], DisplayP3),
|
|
246
|
+
"color(display-p3 1 0 0 / 0.4523)"
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("should parse to a color coord", async (t) => {
|
|
251
|
+
t.deepEqual(parse("rgb(0, 128, 255)", sRGB), [0, 128 / 0xff, 255 / 0xff]);
|
|
252
|
+
t.deepEqual(parse("rgba(0, 128, 255, .25)", sRGB), [
|
|
253
|
+
0,
|
|
254
|
+
128 / 0xff,
|
|
255
|
+
255 / 0xff,
|
|
256
|
+
0.25,
|
|
257
|
+
]);
|
|
258
|
+
let outVec = [0, 0, 0];
|
|
259
|
+
t.deepEqual(parse("rgba(0, 128, 255, 1)", sRGB), [0, 128 / 0xff, 255 / 0xff]);
|
|
260
|
+
let out;
|
|
261
|
+
out = parse("rgba(0, 128, 255, 1)", sRGB, outVec);
|
|
262
|
+
t.deepEqual(out, [0, 128 / 0xff, 255 / 0xff]);
|
|
263
|
+
t.equal(out, outVec);
|
|
264
|
+
|
|
265
|
+
// trims to 3
|
|
266
|
+
outVec = [0, 0, 0, 0];
|
|
267
|
+
out = parse("rgba(0, 128, 255, 1)", sRGB, outVec);
|
|
268
|
+
t.deepEqual(out, [0, 128 / 0xff, 255 / 0xff]);
|
|
269
|
+
t.equal(out, outVec);
|
|
270
|
+
|
|
271
|
+
// ensures 4
|
|
272
|
+
outVec = [0, 0, 0, 0];
|
|
273
|
+
out = parse("rgba(0, 128, 255, 0.91)", sRGB, outVec);
|
|
274
|
+
t.deepEqual(out, [0, 128 / 0xff, 255 / 0xff, 0.91]);
|
|
275
|
+
t.equal(out, outVec);
|
|
276
|
+
|
|
277
|
+
t.deepEqual(
|
|
278
|
+
serialize(parse("oklch(1 0 0)", sRGB), sRGB),
|
|
279
|
+
"rgb(255, 255, 255)"
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("should deserialize color string information", async (t) => {
|
|
284
|
+
t.deepEqual(deserialize("rgb(0, 128, 255)"), {
|
|
285
|
+
coords: [0, 128 / 0xff, 255 / 0xff],
|
|
286
|
+
id: "srgb",
|
|
287
|
+
});
|
|
288
|
+
t.deepEqual(deserialize("rgba(0, 128, 255, 0.35)"), {
|
|
289
|
+
coords: [0, 128 / 0xff, 255 / 0xff, 0.35],
|
|
290
|
+
id: "srgb",
|
|
291
|
+
});
|
|
292
|
+
t.deepEqual(deserialize("#ff00cc"), {
|
|
293
|
+
id: "srgb",
|
|
294
|
+
coords: [1, 0, 0.8],
|
|
295
|
+
});
|
|
296
|
+
t.deepEqual(deserialize("#ff00cccc"), {
|
|
297
|
+
id: "srgb",
|
|
298
|
+
coords: [1, 0, 0.8, 0.8],
|
|
299
|
+
});
|
|
300
|
+
t.deepEqual(deserialize("color(srgb-linear 0 0.5 1)"), {
|
|
301
|
+
id: "srgb-linear",
|
|
302
|
+
coords: [0, 0.5, 1],
|
|
303
|
+
});
|
|
304
|
+
t.deepEqual(deserialize("color(srgb-linear 0 0.5 1/0.25)"), {
|
|
305
|
+
id: "srgb-linear",
|
|
306
|
+
coords: [0, 0.5, 1, 0.25],
|
|
307
|
+
});
|
|
308
|
+
t.deepEqual(deserialize("color(srgb-linear 0 0.5 1 / 0.25)"), {
|
|
309
|
+
id: "srgb-linear",
|
|
310
|
+
coords: [0, 0.5, 1, 0.25],
|
|
311
|
+
});
|
|
312
|
+
t.deepEqual(deserialize("oklch(1 0 0)"), {
|
|
313
|
+
id: "oklch",
|
|
314
|
+
coords: [1, 0, 0],
|
|
315
|
+
});
|
|
316
|
+
t.deepEqual(deserialize("oklch(1 0 0/0.25)"), {
|
|
317
|
+
id: "oklch",
|
|
318
|
+
coords: [1, 0, 0, 0.25],
|
|
319
|
+
});
|
|
232
320
|
});
|
|
233
321
|
|
|
234
322
|
test("utils", async (t) => {
|