@texel/color 1.0.5 → 1.1.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  A minimal and modern color library for JavaScript. Especially useful for real-time applications, generative art, and graphics on the web.
6
6
 
7
7
  - Features: fast color conversion, color difference, gamut mapping, and serialization
8
- - Optimized for speed: approx 20-125 times faster than [Colorjs.io](https://colorjs.io/) (see [benchmarks](#benchmarks))
8
+ - Optimized for speed: approx 5-125 times faster than [Colorjs.io](https://colorjs.io/) (see [benchmarks](#benchmarks))
9
9
  - Optimized for low memory and minimal allocations: no arrays or objects are created within conversion and gamut mapping functions
10
10
  - Optimized for compact bundles: zero dependencies, and unused color spaces can be automatically tree-shaked away for small sizes (e.g. ~3.5kb minified if you only require OKLCH to sRGB conversion)
11
11
  - Optimized for accuracy: [high precision](#accuracy) color space matrices
@@ -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 the 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 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:
89
89
 
90
90
  ```js
91
91
  import { gamutMapOKLCH, sRGBGamut, sRGBLinear, OKLCH } from "@texel/color";
@@ -142,6 +142,7 @@ const cuspLC = findCuspOKLCH(a, b, gamut);
142
142
 
143
143
  // ... somewhere else in your program ...
144
144
  // pass 'cusp' parameter for faster evaluation
145
+ // expected that your OKLCH coord has the same hue as the cusp (H)
145
146
  gamutMapOKLCH(oklch, gamut, gamut.space, out, MapToCuspL, cuspLC);
146
147
  ```
147
148
 
@@ -162,7 +163,7 @@ serialize([1, 0, 0], OKLCH, sRGB); // "rgb(255, 255, 255)"
162
163
  serialize([1, 0, 0], OKLCH); // "oklch(1 0 0)"
163
164
  ```
164
165
 
165
- #### `coords = deserialize(colorString)`
166
+ #### `info = deserialize(colorString)`
166
167
 
167
168
  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
 
@@ -264,6 +265,10 @@ Converts the xyY coordinates to XYZ form, storing the results in `out` if specif
264
265
 
265
266
  Performs linear interpolation between min and max with the factor `t`.
266
267
 
268
+ #### `v = lerpAngle(min, max, t)`
269
+
270
+ Performs circular linear interpolation between min and max with the factor `t`, but where the min and max are considered to be angles (in degrees) allowing the value to wrap around within 0 to 360, interpolating to create the shortest arc.
271
+
267
272
  #### `c = clamp(value, min, max)`
268
273
 
269
274
  Clamps the `value` between min and max and returns the result.
@@ -325,19 +330,27 @@ sRGB.toBase(in_sRGB, out_linear_sRGB); // linear to gamma transfer function
325
330
  OKHSLToOKLab([h, s, l], DisplayP3Gamut, optionalOutVec);
326
331
  ```
327
332
 
333
+ ## Interpolation
334
+
335
+ The library currently only exposes `{ lerp, lerpAngle }` functions. To interpolate colors, you will need to build some additional logic, for example see the [example-interpolation.js](./test/example-interpolation.js) script which creates a color ramp in Canvas2D.
336
+
337
+ ## Custom Color Spaces
338
+
339
+ You can build custom color space objects to extend this library, such as adding support for CIELab and HSL. See [test/spaces/lab.js](./test/spaces/lab.js) and [test/spaces/hsl.js](./test/spaces/hsl.js) for examples of this. Some of these spaces may be added to the library at a later point, although the current focus is on "modern" spaces (such as OKLab that has largely made CIELab and HSL obsolete). Documentaiton on custom color spaces is WIP.
340
+
328
341
  ## Notes
329
342
 
330
343
  ### Why another library?
331
344
 
332
345
  [Colorjs](https://colorjs.io/) is fantastic and perhaps the current leading standard in JavaScript, but it's not very practical for creative coding and real-time web applications, where the requirements are often (1) leaner codebases, (2) highly optimized, and (3) minimal GC thrashing.
333
346
 
334
- Colorjs, and simialrly, [Culori](https://culorijs.org/)), are focused on matching CSS spec, which means it will very likely continue to grow in complexity over time, and performance will often be marred (for example, `@texel/color` cusp intersection gamut mapping is ~125 times faster than Colorjs and ~60 times faster than culori).
347
+ Colorjs, and simialrly, [Culori](https://culorijs.org/), are focused on matching CSS spec, which means it will very likely continue to grow in complexity over time, and performance will often be marred (for example, `@texel/color` cusp intersection gamut mapping is ~125 times faster than Colorjs and ~60 times faster than culori).
335
348
 
336
349
  There are many other options such as [color-space](https://www.npmjs.com/package/color-space) or [color-convert](https://www.npmjs.com/package/color-convert), however, these do not support modern spacse such as OKLab and OKHSL, and/or have dubious levels of accuracy (many libraries, for example, do not distinguish between D50 and D65 whitepoints in XYZ).
337
350
 
338
351
  ### Supported Spaces
339
352
 
340
- This library does not aim to target every color space; it only focuses on a limited "modern" set, i.e. OKLab, OKHSL and DeltaEOK have replaced CIELab, HSL, and CIEDE2000 for many practical purposes, allowing this library to be simpler and slimmer.
353
+ This library does not aim to target every color space; it only focuses on a limited "modern" set, i.e. OKLab, OKHSL and DeltaEOK have replaced CIELab, HSL, and CIEDE2000 for many practical purposes, allowing this library to be simpler and slimmer. Note that other spaces like CIELab and HSL are supported through 'custom color spaces'.
341
354
 
342
355
  ### Improvements & Techniques
343
356
 
@@ -365,26 +378,47 @@ There are a few benchmarks inside [test](./test):
365
378
  - [bench-node.js](./test/bench-node.js) - run with `npm run bench:node` to get a node profile
366
379
  - [bench-size.js](./test/bench-size.js) - run with `npm run bench:size` to get a small bundle size with esbuild
367
380
 
368
- Colorjs comparison benchmark on MacBook Air M2:
381
+ Results below, based on MacBook Air M2. Note that Colorjs performance depends on which API you use (the default class-based API is much slower than the procedural API).
382
+
383
+ <details>
384
+ <summary>Benchmark Against Colorjs.io</summary>
369
385
 
370
386
  ```
371
- OKLCH to sRGB with gamut mapping --
372
- Colorjs: 6146.67 ms
373
- Ours: 46.77 ms
374
- Speedup: 131.4x faster
375
-
376
- All Conversions --
377
- Colorjs: 10219.40 ms
378
- Ours: 431.13 ms
379
- Speedup: 23.7x faster
380
-
381
- Conversion + Gamut Mapping --
382
- Colorjs: 1936.29 ms
383
- Ours: 82.04 ms
384
- Speedup: 23.6x faster
387
+ conversion (Colorjs.io procedural API) --
388
+ Colorjs.io: 2955.88 ms
389
+ Ours: 457.86 ms
390
+ Speedup: 6.5x faster
391
+
392
+ conversion (Colorjs.io main API) --
393
+ Colorjs.io: 10034.38 ms
394
+ Ours: 452.11 ms
395
+ Speedup: 22.2x faster
396
+
397
+ gamut mapping OKLCH - sRGB (Colorjs.io procedural API) --
398
+ Colorjs.io: 5602.46 ms
399
+ Ours: 49.10 ms
400
+ Speedup: 114.1x faster
401
+
402
+ gamut mapping OKLCH - sRGB (Colorjs.io main API) --
403
+ Colorjs.io: 5913.80 ms
404
+ Ours: 44.91 ms
405
+ Speedup: 131.7x faster
406
+
407
+ gamut mapping all spaces to P3 (Colorjs.io procedural API) --
408
+ Colorjs.io: 4693.43 ms
409
+ Ours: 150.16 ms
410
+ Speedup: 31.3x faster
411
+
412
+ gamut mapping all spaces to P3 (Colorjs.io main API) --
413
+ Colorjs.io: 5478.16 ms
414
+ Ours: 145.88 ms
415
+ Speedup: 37.6x faster
385
416
  ```
386
417
 
387
- And against culori:
418
+ </details>
419
+
420
+ <details>
421
+ <summary>Benchmark Against Culori</summary>
388
422
 
389
423
  ```
390
424
  Testing with input type: Random Samling in OKLab L Planes
@@ -399,6 +433,8 @@ Ours: 23.05 ms
399
433
  Speedup: 68.9x faster
400
434
  ```
401
435
 
436
+ </details>
437
+
402
438
  ### Running Locally
403
439
 
404
440
  Clone, `npm install`, then `npm run` to list the available scripts, or `npm t` to run the tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@texel/color",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "a minimal and modern color library",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/core.js CHANGED
@@ -200,6 +200,12 @@ export const convert = (input, fromSpace, toSpace, out = vec3()) => {
200
200
  // and the base we want to get to, linear, OKLab, XYZ etc...
201
201
  let toBaseSpace = toSpace.base ?? toSpace;
202
202
 
203
+ // this is something we may support in future, if there is a nice
204
+ // zero-allocation way of achieving it
205
+ if (fromSpace.base || toBaseSpace.base) {
206
+ throw new Error(`Currently only base of depth=1 is supported`);
207
+ }
208
+
203
209
  if (fromBaseSpace === toBaseSpace) {
204
210
  // do nothing, spaces are the same
205
211
  } else {
@@ -220,39 +226,47 @@ export const convert = (input, fromSpace, toSpace, out = vec3()) => {
220
226
  if (fromBaseSpace.id === "oklab") {
221
227
  let mat = toBaseSpace.fromLMS_M;
222
228
  if (!mat) {
223
- // space doesn't support direct
229
+ // space doesn't support direct from OKLAB
224
230
  // let's convert OKLab to XYZ and then use that
225
231
  mat = XYZ.fromLMS_M;
226
232
  throughXYZ = true;
227
233
  xyzIn = true;
228
234
  }
235
+ // convert OKLAB to output (other space, or xyz)
229
236
  out = OKLab_to(out, mat, out);
230
237
  } else if (toBaseSpace.id === "oklab") {
231
238
  let mat = fromBaseSpace.toLMS_M;
232
239
  if (!mat) {
233
- // space doesn't support direct
240
+ // space doesn't support direct to OKLAB
241
+ // we will need to use XYZ as connection, then convert to OKLAB
234
242
  throughXYZ = true;
235
243
  outputOklab = true;
236
244
  } else {
237
- // direct from space to oklab
245
+ // direct from space to OKLAB
238
246
  out = OKLab_from(out, mat, out);
239
247
  }
240
248
  } else {
249
+ // any other spaces, we use XYZ D65 as a connection
241
250
  throughXYZ = true;
242
251
  }
243
252
 
244
253
  if (throughXYZ) {
245
254
  // First, convert to XYZ if we need to
246
255
  if (!xyzIn) {
247
- if (!fromBaseSpace.toXYZ_M)
248
- throw new Error(`no toXYZ_M on ${fromBaseSpace.id}`);
249
- out = transform(out, fromBaseSpace.toXYZ_M, out);
256
+ if (fromBaseSpace.toXYZ) {
257
+ out = fromBaseSpace.toXYZ(out, out);
258
+ } else if (fromBaseSpace.toXYZ_M) {
259
+ out = transform(out, fromBaseSpace.toXYZ_M, out);
260
+ } else {
261
+ throw new Error(`no toXYZ or toXYZ_M on ${fromBaseSpace.id}`);
262
+ }
250
263
  }
251
264
 
252
265
  // Then, adapt D50 <-> D65 if we need to
253
266
  if (fromBaseSpace.adapt) {
254
267
  out = transform(out, fromBaseSpace.adapt.to, out);
255
- } else if (toBaseSpace.adapt) {
268
+ }
269
+ if (toBaseSpace.adapt) {
256
270
  out = transform(out, toBaseSpace.adapt.from, out);
257
271
  }
258
272
 
@@ -260,10 +274,12 @@ export const convert = (input, fromSpace, toSpace, out = vec3()) => {
260
274
  if (!xyzOut) {
261
275
  if (outputOklab) {
262
276
  out = OKLab_from(out, XYZ.toLMS_M, out);
263
- } else {
264
- if (!toBaseSpace.fromXYZ_M)
265
- throw new Error(`no fromXYZ_M on ${toBaseSpace.id}`);
277
+ } else if (toBaseSpace.fromXYZ) {
278
+ out = toBaseSpace.fromXYZ(out, out);
279
+ } else if (toBaseSpace.fromXYZ_M) {
266
280
  out = transform(out, toBaseSpace.fromXYZ_M, out);
281
+ } else {
282
+ throw new Error(`no fromXYZ or fromXYZ_M on ${toBaseSpace.id}`);
267
283
  }
268
284
  }
269
285
  }
@@ -14,7 +14,7 @@ const ProPhotoRGBToGamma = (v) => (v >= Et ? v ** (1 / 1.8) : 16 * v);
14
14
 
15
15
  // Note: below is a possibly improved transfer function proposed by CSS Module 4 spec
16
16
  // It is currently not matching the outputs of Colorjs.io when dealing with particular
17
- // negative values, such as convert([1,1,1], OKlab, ProPhotoRGB)
17
+ // negative values, such as convert([1,1,1], OKLab, ProPhotoRGB)
18
18
 
19
19
  // const ProPhotoRGBToLinear = (val) => {
20
20
  // // convert an array of prophoto-rgb values
package/src/spaces/xyz.js CHANGED
@@ -2,6 +2,12 @@ import { vec3 } from "../util.js";
2
2
  import { transform } from "../core.js";
3
3
  import { LMS_to_XYZ_M, XYZ_to_LMS_M } from "../conversion_matrices.js";
4
4
 
5
+ // Note: for the time being, these are not exported
6
+ // It may be exported in a future version
7
+ // for compatibility, the four-digit chromaticity-derived ones everyone else uses
8
+ // const D50 = [0.3457 / 0.3585, 1.0, (1.0 - 0.3457 - 0.3585) / 0.3585];
9
+ // const D65 = [0.3127 / 0.329, 1.0, (1.0 - 0.3127 - 0.329) / 0.329];
10
+
5
11
  // Bradford chromatic adaptation from D65 to D50
6
12
  // The matrix below is the result of three operations:
7
13
  // - convert from XYZ to retinal cone domain
package/src/util.js CHANGED
@@ -26,9 +26,12 @@ export const hexToRGB = (str, out = vec3()) => {
26
26
  return out;
27
27
  };
28
28
 
29
- export const RGBtoHex = (rgb) =>
29
+ export const RGBToHex = (rgb) =>
30
30
  `#${rgb.map((n) => floatToByte(n).toString(16).padStart(2, "0")).join("")}`;
31
31
 
32
+ /** @deprecated use RGBToHex */
33
+ export const RGBtoHex = (rgb) => RGBToHex;
34
+
32
35
  export const isRGBInGamut = (lrgb, ep = GAMUT_EPSILON) => {
33
36
  const r = lrgb[0];
34
37
  const g = lrgb[1];
@@ -104,16 +107,9 @@ export const vec3 = () => [0, 0, 0];
104
107
  // return ((2 * da) % max) - da;
105
108
  // }
106
109
 
107
- // function shortAngleDistDeg(a0, a1) {
108
- // var max = 360;
109
- // var da = (a1 - a0) % max;
110
- // return ((2 * da) % max) - da;
111
- // }
112
-
113
- // function lerpAngleRad(a0, a1, t) {
114
- // return a0 + shortAngleDistRad(a0, a1) * t;
115
- // }
110
+ export const deltaAngle = (a0, a1) => {
111
+ var da = (a1 - a0) % 360;
112
+ return ((2 * da) % 360) - da;
113
+ };
116
114
 
117
- // function lerpAngleDeg(a0, a1, t) {
118
- // return radToDeg(lerpAngleRad(degToRad(a0), degToRad(a1), t));
119
- // }
115
+ export const lerpAngle = (a0, a1, t) => a0 + deltaAngle(a0, a1) * t;
@@ -1,10 +1,16 @@
1
1
  import Color from "colorjs.io";
2
+ import {
3
+ sRGB as sRGB_ColorJS,
4
+ OKLCH as OKLCH_ColorJS,
5
+ P3 as DisplayP3_ColorJS,
6
+ toGamut,
7
+ } from "colorjs.io/fn";
2
8
  import {
3
9
  convert,
4
10
  OKLCH,
11
+ OKLab,
5
12
  sRGB,
6
13
  sRGBGamut,
7
- listColorSpaces,
8
14
  DisplayP3Gamut,
9
15
  DisplayP3,
10
16
  gamutMapOKLCH,
@@ -14,27 +20,25 @@ import {
14
20
  MapToCuspL,
15
21
  } from "../src/index.js";
16
22
 
17
- const fixName = (name) => {
18
- return name
19
- .replace("display-", "")
20
- .replace("a98-rgb", "a98rgb")
21
- .replace("prophoto-rgb", "prophoto");
22
- };
23
+ import { getSupportedColorJSSpaces } from "./colorjs-fn.js";
24
+
25
+ const supportedSpaces = getSupportedColorJSSpaces();
26
+
27
+ // @texel/color space interfaces
28
+ const spaces = supportedSpaces.map((s) => s.space);
29
+
30
+ // Colorjs.io space interfaces & IDs
31
+ const colorJSSpaces = supportedSpaces.map((s) => s.colorJSSpace);
32
+ const colorJSSpaceIDs = supportedSpaces.map((s) => s.colorJSSpace.id);
23
33
 
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));
34
+ const N = 128 * 128;
27
35
 
28
- const vecs = Array(128 * 128)
36
+ // sampling variables within OKLab and converting to OKLCH
37
+ const vecs = Array(N)
29
38
  .fill()
30
39
  .map((_, i, lst) => {
31
40
  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
- );
41
+ return convert([t, t * 2 - 1, t * 2 - 1], OKLab, OKLCH);
38
42
  });
39
43
 
40
44
  const tmp = [0, 0, 0];
@@ -59,80 +63,165 @@ const oklchVecs = Array(512 * 256)
59
63
  return [t0, t0, H];
60
64
  });
61
65
 
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);
66
+ compare(
67
+ "conversion (Colorjs.io procedural API)",
68
+ () => {
69
+ for (let vec of vecs) {
70
+ for (let i = 0; i < colorJSSpaces.length; i++) {
71
+ for (let j = 0; j < colorJSSpaces.length; j++) {
72
+ const a = colorJSSpaces[i];
73
+ const b = colorJSSpaces[j];
74
+ const ret = b.from(a, vec);
75
+ }
76
+ }
77
+ }
78
+ },
79
+ () => {
80
+ for (let vec of vecs) {
81
+ for (let i = 0; i < spaces.length; i++) {
82
+ for (let j = 0; j < spaces.length; j++) {
83
+ const a = spaces[i];
84
+ const b = spaces[j];
85
+ convert(vec, a, b, tmp);
86
+ }
87
+ }
91
88
  }
92
89
  }
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);
90
+ );
91
+
92
+ compare(
93
+ "conversion (Colorjs.io main API)",
94
+ () => {
95
+ for (let vec of vecs) {
96
+ for (let i = 0; i < colorJSSpaceIDs.length; i++) {
97
+ for (let j = 0; j < colorJSSpaceIDs.length; j++) {
98
+ const a = colorJSSpaceIDs[i];
99
+ const b = colorJSSpaceIDs[j];
100
+ new Color(a, vec).to(b);
101
+ }
102
+ }
103
+ }
104
+ },
105
+ () => {
106
+ for (let vec of vecs) {
107
+ for (let i = 0; i < spaces.length; i++) {
108
+ for (let j = 0; j < spaces.length; j++) {
109
+ const a = spaces[i];
110
+ const b = spaces[j];
111
+ convert(vec, a, b, tmp);
112
+ }
113
+ }
103
114
  }
104
115
  }
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" });
116
+ );
117
+
118
+ compare(
119
+ "gamut mapping OKLCH - sRGB (Colorjs.io procedural API)",
120
+ () => {
121
+ let tmpColor = { space: sRGB_ColorJS, coords: [0, 0, 0], alpha: 1 };
122
+ for (let vec of oklchVecs) {
123
+ tmpColor.coords = sRGB_ColorJS.from(OKLCH_ColorJS, vec);
124
+ toGamut(tmpColor);
125
+ }
126
+ },
127
+ () => {
128
+ for (let vec of oklchVecs) {
129
+ // you can omit the cusp and it will be found on the fly,
130
+ // however the test will run slightly slower
131
+ const cusp = hueCusps[vec[2]];
132
+ gamutMapOKLCH(vec, sRGBGamut, sRGB, tmp, MapToCuspL, cusp);
133
+ }
120
134
  }
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);
135
+ );
136
+
137
+ compare(
138
+ "gamut mapping OKLCH - sRGB (Colorjs.io main API)",
139
+ () => {
140
+ for (let vec of oklchVecs) {
141
+ new Color("oklch", vec)
142
+ .to("srgb")
143
+ .toGamut({ space: "srgb", method: "css" });
144
+ }
145
+ },
146
+ () => {
147
+ for (let vec of oklchVecs) {
148
+ // you can omit the cusp and it will be found on the fly,
149
+ // however the test will run slightly slower
150
+ const cusp = hueCusps[vec[2]];
151
+ gamutMapOKLCH(vec, sRGBGamut, sRGB, tmp, MapToCuspL, cusp);
152
+ }
130
153
  }
154
+ );
155
+
156
+ compare(
157
+ "gamut mapping all spaces to P3 (Colorjs.io procedural API)",
158
+ () => {
159
+ let tmpColor = { space: DisplayP3_ColorJS, coords: [0, 0, 0], alpha: 1 };
160
+ for (let vec of vecs) {
161
+ for (let i = 0; i < colorJSSpaces.length; i++) {
162
+ const a = colorJSSpaces[i];
163
+ tmpColor.coords = DisplayP3_ColorJS.from(a, vec);
164
+ toGamut(tmpColor);
165
+ }
166
+ }
167
+ },
168
+ () => {
169
+ for (let vec of vecs) {
170
+ for (let i = 0; i < spaces.length; i++) {
171
+ const a = spaces[i];
172
+ convert(vec, a, OKLCH, tmp);
173
+ gamutMapOKLCH(tmp, DisplayP3Gamut, DisplayP3, tmp);
174
+ }
175
+ }
176
+ }
177
+ );
178
+
179
+ compare(
180
+ "gamut mapping all spaces to P3 (Colorjs.io main API)",
181
+ () => {
182
+ for (let vec of vecs) {
183
+ for (let i = 0; i < colorJSSpaceIDs.length; i++) {
184
+ const a = colorJSSpaceIDs[i];
185
+ new Color(a, vec).to("p3").toGamut({ space: "p3", method: "css" });
186
+ }
187
+ }
188
+ },
189
+ () => {
190
+ for (let vec of vecs) {
191
+ for (let i = 0; i < spaces.length; i++) {
192
+ const a = spaces[i];
193
+ convert(vec, a, OKLCH, tmp);
194
+ gamutMapOKLCH(tmp, DisplayP3Gamut, DisplayP3, tmp);
195
+ }
196
+ }
197
+ }
198
+ );
199
+
200
+ function print(label) {
201
+ console.log("%s --", label);
202
+ console.log("Colorjs.io: %s ms", elapsedColorjs.toFixed(2));
203
+ console.log("Ours: %s ms", elapsedOurs.toFixed(2));
204
+ if (elapsedColorjs > elapsedOurs)
205
+ console.log(
206
+ "Speedup: %sx faster",
207
+ (elapsedColorjs / elapsedOurs).toFixed(1)
208
+ );
209
+ else
210
+ console.log(
211
+ "Slowdown: %sx slower",
212
+ (elapsedOurs / elapsedColorjs).toFixed(1)
213
+ );
214
+ console.log();
131
215
  }
132
- elapsedOurs = performance.now() - now;
133
216
 
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));
217
+ function compare(label, colorJSFn, ourFn) {
218
+ now = performance.now();
219
+ colorJSFn();
220
+ elapsedColorjs = performance.now() - now;
221
+
222
+ now = performance.now();
223
+ ourFn();
224
+ elapsedOurs = performance.now() - now;
225
+
226
+ print(label);
227
+ }
@@ -0,0 +1,34 @@
1
+ import * as ColorJSFn from "colorjs.io/fn";
2
+ import { listColorSpaces } from "../src/index.js";
3
+
4
+ // Colorjs.io uses some different naming conventions than @texel/color
5
+ const getColorJSID = (name) => {
6
+ return name
7
+ .replace("display-", "")
8
+ .replace(/^xyz$/, "xyz-d65")
9
+ .replace("a98-rgb", "a98rgb")
10
+ .replace("prophoto-rgb", "prophoto");
11
+ };
12
+
13
+ // returns a list of ColorJS.io space IDs that are supported by @texel/color
14
+ // okhsl/okhsv is skipped due to it not being included in this npm version of colorjs.io
15
+ export function getSupportedColorJSSpaces() {
16
+ const spaceFns = Object.values(ColorJSFn);
17
+ const spaces = listColorSpaces().filter((s) => !/ok(hsv|hsl)/i.test(s.id));
18
+ return spaces.map((space) => {
19
+ const cjsID = getColorJSID(space.id);
20
+ const colorJSSpace = spaceFns.find((f) => f.id === cjsID);
21
+ if (!colorJSSpace)
22
+ throw new Error(`expected ${cjsID} to exist in colorjs.io/fn`);
23
+ return {
24
+ space,
25
+ colorJSSpace,
26
+ };
27
+ });
28
+ }
29
+
30
+ // Register all spaces
31
+ const spaces = getSupportedColorJSSpaces().map((s) => s.colorJSSpace);
32
+ for (let space of spaces) {
33
+ ColorJSFn.ColorSpace.register(space);
34
+ }
@@ -0,0 +1,107 @@
1
+ import canvasSketch from "canvas-sketch";
2
+ import {
3
+ convert,
4
+ DisplayP3Gamut,
5
+ gamutMapOKLCH,
6
+ lerp,
7
+ lerpAngle,
8
+ OKLab,
9
+ OKLCH,
10
+ serialize,
11
+ sRGB,
12
+ sRGBGamut,
13
+ } from "../src/index.js";
14
+
15
+ const settings = {
16
+ dimensions: [2048, 512],
17
+ attributes: {
18
+ // comment this out if you want sRGB output
19
+ colorSpace: "display-p3",
20
+ },
21
+ };
22
+
23
+ const mix = (() => {
24
+ const tmpA = [0, 0, 0];
25
+ const tmpB = [0, 0, 0];
26
+
27
+ // you can decide whether you'd like to interpolate in
28
+ // OKLab, OKLCH or another space (you may need to adjust interpolation
29
+ // if you use a custom space)
30
+ const interpolationSpace = OKLCH;
31
+
32
+ // e.g. mix({ space, coords }, { space, coords }, 0.5, sRGB)
33
+ return (a, b, t, outputSpace = sRGB, out = [0, 0, 0]) => {
34
+ // bring both spaces into the shared interpolation space
35
+ convert(a.coords, a.space, interpolationSpace, tmpA);
36
+ convert(b.coords, b.space, interpolationSpace, tmpB);
37
+
38
+ // now do interpolation
39
+ out[0] = lerp(tmpA[0], tmpB[0], t);
40
+ out[1] = lerp(tmpA[1], tmpB[1], t);
41
+ if (interpolationSpace.id === "oklch") {
42
+ // for cylindrical spaces, use a circular interpolation for Hue parameter
43
+ // note if you decide to use a custom space like HSL as your interpolation space,
44
+ // you'll have to use the first parameter instead...
45
+ out[2] = lerpAngle(tmpA[2], tmpB[2], t);
46
+ } else {
47
+ // otherwise can use a regular linear interpolation
48
+ out[2] = lerp(tmpA[2], tmpB[2], t);
49
+ }
50
+
51
+ // make sure we convert from interpolation space to the target output space
52
+ convert(out, interpolationSpace, outputSpace, out);
53
+ return out;
54
+ };
55
+ })();
56
+
57
+ // utility to create a ramp between two 'colors' as { space, coords }
58
+ function ramp(a, b, steps = 4, outputSpace = sRGB) {
59
+ return Array(steps)
60
+ .fill()
61
+ .map((_, i, lst) => {
62
+ const t = lst.length <= 1 ? 0 : i / (lst.length - 1);
63
+ return mix(a, b, t, outputSpace);
64
+ });
65
+ }
66
+
67
+ const sketch = ({ context }) => {
68
+ const { colorSpace = "srgb" } = context.getContextAttributes();
69
+ const gamut = colorSpace === "srgb" ? sRGBGamut : DisplayP3Gamut;
70
+
71
+ return ({ context, width, height }) => {
72
+ context.fillStyle = "white";
73
+ context.fillRect(0, 0, width, height);
74
+
75
+ const A = {
76
+ space: sRGB,
77
+ coords: [0, 0, 1],
78
+ };
79
+
80
+ const B = {
81
+ space: OKLCH,
82
+ coords: [0.55, 0.4, 30],
83
+ };
84
+
85
+ const slices = 16;
86
+ const sliceWidth = width / slices;
87
+
88
+ // the output space is whatever the canvas expects (sRGB or DisplayP3)
89
+ const outputSpace = gamut.space;
90
+
91
+ // create a ramp of colors in OKLCH
92
+ // then gamut map them to the outputSpace
93
+ const colors = ramp(A, B, slices, OKLCH).map((oklch) =>
94
+ gamutMapOKLCH(oklch, gamut, outputSpace)
95
+ );
96
+
97
+ for (let i = 0; i < slices; i++) {
98
+ const color = colors[i];
99
+
100
+ // turn the color (now in outputSpace) into a context string
101
+ context.fillStyle = serialize(color, outputSpace);
102
+ context.fillRect(i * sliceWidth, 0, sliceWidth, height);
103
+ }
104
+ };
105
+ };
106
+
107
+ canvasSketch(sketch, settings);
@@ -0,0 +1,82 @@
1
+ import { sRGB, sRGBLinear } from "../../src/index.js";
2
+
3
+ export const HSL = {
4
+ id: "hsl",
5
+ // Note: @texel/color currently only supports 1-level depth for color spaces
6
+ // (for performance & memory reasons) - so our base must be one without another base space
7
+ base: sRGBLinear,
8
+ // Adapted from https://drafts.csswg.org/css-color-4/better-rgbToHsl.js
9
+ fromBase: (rgb, out = [0, 0, 0]) => {
10
+ // from sRGBLinear (this space's base) to sRGB (for HSL conversion)
11
+ sRGB.fromBase(rgb, out);
12
+ const r = out[0];
13
+ const g = out[1];
14
+ const b = out[2];
15
+ let max = Math.max(r, g, b);
16
+ let min = Math.min(r, g, b);
17
+ let h = 0,
18
+ s = 0,
19
+ l = (min + max) / 2;
20
+ let d = max - min;
21
+
22
+ if (d !== 0) {
23
+ s = l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l);
24
+
25
+ switch (max) {
26
+ case r:
27
+ h = (g - b) / d + (g < b ? 6 : 0);
28
+ break;
29
+ case g:
30
+ h = (b - r) / d + 2;
31
+ break;
32
+ case b:
33
+ h = (r - g) / d + 4;
34
+ }
35
+
36
+ h = h * 60;
37
+ }
38
+
39
+ // Very out of gamut colors can produce negative saturation
40
+ // If so, just rotate the hue by 180 and use a positive saturation
41
+ // see https://github.com/w3c/csswg-drafts/issues/9222
42
+ if (s < 0) {
43
+ h += 180;
44
+ s = Math.abs(s);
45
+ }
46
+
47
+ if (h >= 360) {
48
+ h -= 360;
49
+ }
50
+
51
+ out[0] = h;
52
+ out[1] = s * 100;
53
+ out[2] = l * 100;
54
+ return out;
55
+ },
56
+ // Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
57
+ toBase: (hsl, out = [0, 0, 0]) => {
58
+ let h = hsl[0];
59
+ let s = hsl[1];
60
+ let l = hsl[2];
61
+ h = h % 360;
62
+ if (h < 0) {
63
+ h += 360;
64
+ }
65
+
66
+ s /= 100;
67
+ l /= 100;
68
+
69
+ const a = s * Math.min(l, 1 - l);
70
+ out[0] = f(h, l, a, 0);
71
+ out[1] = f(h, l, a, 8);
72
+ out[2] = f(h, l, a, 4);
73
+ // from sRGB to sRGBLinear (this space's base)
74
+ sRGB.toBase(out, out);
75
+ return out;
76
+ },
77
+ };
78
+
79
+ function f(h, l, a, n) {
80
+ let k = (n + h / 30) % 12;
81
+ return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
82
+ }
@@ -0,0 +1,58 @@
1
+ // Lab aka CIELAB aka L*a*b* (uses a D50 WHITE_D50 point and has to be adapted)
2
+ // refer to CSS Color Module Level 4 Spec for more details
3
+ // Source code reference: Colorjs.io
4
+ import { D50_to_D65_M, D65_to_D50_M } from "../../src/index.js";
5
+
6
+ // K * e = 2^3 = 8
7
+ const e = 216 / 24389; // 6^3/29^3 == (24/116)^3
8
+ const e3 = 24 / 116;
9
+ const K = 24389 / 27; // 29^3/3^3
10
+ const WHITE_D50 = [0.3457 / 0.3585, 1.0, (1.0 - 0.3457 - 0.3585) / 0.3585];
11
+
12
+ const fterm = (value) =>
13
+ value > e ? Math.cbrt(value) : (K * value + 16) / 116;
14
+
15
+ const inv = (value) =>
16
+ value > e3 ? Math.pow(value, 3) : (116 * value - 16) / K;
17
+
18
+ export const Lab = {
19
+ id: "lab",
20
+ adapt: {
21
+ // chromatic adaptation to and from D65
22
+ to: D50_to_D65_M,
23
+ from: D65_to_D50_M,
24
+ },
25
+ // Convert D50-adapted XYX to Lab
26
+ // CIE 15.3:2004 section 8.2.1.1
27
+ fromXYZ(xyz, out = [0, 0, 0]) {
28
+ // XYZ scaled relative to reference WHITE_D50, then modified
29
+ out[0] = fterm(xyz[0] / WHITE_D50[0]);
30
+ out[1] = fterm(xyz[1] / WHITE_D50[1]);
31
+ out[2] = fterm(xyz[2] / WHITE_D50[2]);
32
+ let L = 116 * out[1] - 16;
33
+ let a = 500 * (out[0] - out[1]);
34
+ let b = 200 * (out[1] - out[2]);
35
+ out[0] = L;
36
+ out[1] = a;
37
+ out[2] = b;
38
+ return out;
39
+ },
40
+ // Convert Lab to D50-adapted XYZ
41
+ // Same result as CIE 15.3:2004 Appendix D although the derivation is different
42
+ // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
43
+ toXYZ(Lab, out = [0, 0, 0]) {
44
+ // compute f, starting with the luminance-related term
45
+ const L = Lab[0];
46
+ const a = Lab[1];
47
+ const b = Lab[2];
48
+ out[1] = (L + 16) / 116;
49
+ out[0] = a / 500 + out[1];
50
+ out[2] = out[1] - b / 200;
51
+
52
+ // compute xyz and scale by WHITE_D50
53
+ out[0] = inv(out[0]) * WHITE_D50[0];
54
+ out[1] = (L > 8 ? Math.pow((L + 16) / 116, 3) : L / K) * WHITE_D50[1];
55
+ out[2] = inv(out[2]) * WHITE_D50[2];
56
+ return out;
57
+ },
58
+ };
@@ -1,16 +1,18 @@
1
1
  import test from "tape";
2
2
  import Color from "colorjs.io";
3
3
  import arrayAlmostEqual from "./almost-equal.js";
4
- import { listColorSpaces, convert } from "../src/index.js";
4
+ import { convert } from "../src/index.js";
5
+ import { getSupportedColorJSSpaces } from "./colorjs-fn.js";
5
6
 
6
7
  test("should approximately match colorjs.io conversions", async (t) => {
7
8
  // 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 spaces = getSupportedColorJSSpaces();
9
10
  const vecs = [
10
11
  [0.12341, 0.12001, 0.05212],
11
12
  [1, 1, 1],
12
13
  [1, 0, 0],
13
14
  [0, 0, 0],
15
+ [-0.5, -0.5, -0.5],
14
16
  // some other inputs
15
17
  [0.95, 1, 1.089],
16
18
  [0.45, 1.236, -0.019],
@@ -28,18 +30,15 @@ test("should approximately match colorjs.io conversions", async (t) => {
28
30
  // Math.random() * 2 - 1,
29
31
  // ]);
30
32
 
31
- const fixName = (name) => {
32
- return name
33
- .replace("display-", "")
34
- .replace("a98-rgb", "a98rgb")
35
- .replace("prophoto-rgb", "prophoto");
36
- };
37
-
38
33
  for (let vec of vecs) {
39
34
  for (let i = 0; i < spaces.length; i++) {
40
35
  for (let j = 0; j < spaces.length; j++) {
41
- const a = spaces[i];
42
- const b = spaces[j];
36
+ const A = spaces[i];
37
+ const B = spaces[j];
38
+
39
+ // @texel/color spaces
40
+ const a = A.space;
41
+ const b = B.space;
43
42
  const suffix = `${a.id}-to-${b.id}`;
44
43
 
45
44
  console.log(suffix);
@@ -47,8 +46,8 @@ test("should approximately match colorjs.io conversions", async (t) => {
47
46
  const tmp = vec.slice();
48
47
  const expected1 = convert(vec, a, b, tmp);
49
48
 
50
- const colorjsid_a = fixName(a.id);
51
- const colorjsid_b = fixName(b.id);
49
+ const colorjsid_a = A.colorJSSpace.id;
50
+ const colorjsid_b = B.colorJSSpace.id;
52
51
  t.deepEqual(expected0, tmp, `${suffix} copies into`);
53
52
  t.deepEqual(expected0, expected1, `${suffix} copies into`);
54
53
  t.equal(expected1, tmp, `${suffix} copies into and returns`);
@@ -0,0 +1,115 @@
1
+ import {
2
+ convert,
3
+ XYZD50,
4
+ sRGB,
5
+ XYZ,
6
+ ProPhotoRGB,
7
+ OKLCH,
8
+ sRGBLinear,
9
+ } from "../src/index.js";
10
+ import { Lab } from "./spaces/lab.js";
11
+ import { HSL } from "./spaces/hsl.js";
12
+ import test from "tape";
13
+ import Color from "colorjs.io";
14
+ import arrayAlmostEqual from "./almost-equal.js";
15
+
16
+ test("should approximately match colorjs.io CIELAB", async (t) => {
17
+ let input = [0.5, 0.1243, -0.123];
18
+ let inputSpace = OKLCH;
19
+ let outputSpace = Lab;
20
+ let expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
21
+ let result = convert(input, inputSpace, outputSpace);
22
+ t.deepEqual(expected, result);
23
+
24
+ inputSpace = XYZD50;
25
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
26
+ result = convert(input, inputSpace, outputSpace);
27
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
28
+
29
+ inputSpace = XYZ;
30
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
31
+ result = convert(input, inputSpace, outputSpace);
32
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
33
+
34
+ inputSpace = sRGB;
35
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
36
+ result = convert(input, inputSpace, outputSpace);
37
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
38
+
39
+ inputSpace = ProPhotoRGB;
40
+ expected = new Color("prophoto", input).to(outputSpace.id).coords;
41
+ result = convert(input, inputSpace, outputSpace);
42
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
43
+
44
+ inputSpace = Lab;
45
+ outputSpace = XYZ;
46
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
47
+ result = convert(input, inputSpace, outputSpace);
48
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
49
+
50
+ outputSpace = XYZD50;
51
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
52
+ result = convert(input, inputSpace, outputSpace);
53
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
54
+
55
+ outputSpace = Lab;
56
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
57
+ result = convert(input, inputSpace, outputSpace);
58
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
59
+
60
+ outputSpace = sRGBLinear;
61
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
62
+ result = convert(input, inputSpace, outputSpace);
63
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
64
+
65
+ outputSpace = sRGB;
66
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
67
+ result = convert(input, inputSpace, outputSpace);
68
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
69
+ });
70
+
71
+ test("should approximately match colorjs.io HSL", async (t) => {
72
+ let input = [30, 50, 50];
73
+ let inputSpace = HSL;
74
+ let outputSpace = OKLCH;
75
+ let expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
76
+ let result = convert(input, inputSpace, outputSpace);
77
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
78
+
79
+ outputSpace = XYZD50;
80
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
81
+ result = convert(input, inputSpace, outputSpace);
82
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
83
+
84
+ outputSpace = sRGB;
85
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
86
+ result = convert(input, inputSpace, outputSpace);
87
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
88
+
89
+ outputSpace = sRGBLinear;
90
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
91
+ result = convert(input, inputSpace, outputSpace);
92
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
93
+
94
+ input = [0.5, 0.2, 30];
95
+ inputSpace = OKLCH;
96
+ outputSpace = HSL;
97
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
98
+ result = convert(input, inputSpace, outputSpace);
99
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
100
+
101
+ inputSpace = XYZ;
102
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
103
+ result = convert(input, inputSpace, outputSpace);
104
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
105
+
106
+ inputSpace = sRGB;
107
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
108
+ result = convert(input, inputSpace, outputSpace);
109
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
110
+
111
+ inputSpace = sRGBLinear;
112
+ expected = new Color(inputSpace.id, input).to(outputSpace.id).coords;
113
+ result = convert(input, inputSpace, outputSpace);
114
+ t.deepEqual(arrayAlmostEqual(expected, result), true);
115
+ });
package/test/test.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  floatToByte,
6
6
  hexToRGB,
7
7
  isRGBInGamut,
8
- RGBtoHex,
8
+ RGBToHex,
9
9
  linear_sRGB_to_LMS_M,
10
10
  LMS_to_linear_sRGB_M,
11
11
  LMS_to_XYZ_M,
@@ -320,7 +320,7 @@ test("should deserialize color string information", async (t) => {
320
320
  });
321
321
 
322
322
  test("utils", async (t) => {
323
- t.deepEqual(RGBtoHex([0, 0.5, 1]), "#0080ff");
323
+ t.deepEqual(RGBToHex([0, 0.5, 1]), "#0080ff");
324
324
  t.deepEqual(hexToRGB("#0080ff"), [0, 0.5019607843137255, 1]);
325
325
  const tmp = [0, 0, 0];
326
326
  hexToRGB("#0080ff", tmp);