@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 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 Ottoson](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:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@texel/color",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "a minimal and modern color library",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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
- return `${id}(${tmp3[0]} ${tmp3[1]} ${tmp3[2]}${alphaSuffix})`;
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 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
- }
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
- if (fn === "color") {
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 if (/^(oklab|oklch)$/.test(fn)) {
137
- id = fn;
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) => parseFloat(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 = (cusp[1] * l0) / (c1 * cusp[0] + cusp[1] * (l0 - l1));
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
- t = (cusp[1] * (l0 - 1.0)) / (c1 * (cusp[0] - 1.0) + cusp[1] * (l0 - l1));
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;
@@ -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 = {
@@ -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
- // Source code reference: Colorjs.io
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(1 0 0)");
234
- t.deepEqual(serialize([1, 0, 0], OKLab), "oklab(1 0 0)");
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(1 0 0 / 0.4523)"
242
+ "oklch(100% 0 0 / 0.4523)"
242
243
  );
243
- t.deepEqual(serialize([1, 0, 0, 0.4523], OKLCH), "oklch(1 0 0 / 0.4523)");
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
- 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
- });
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) {