@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 ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2024 Matt DesLauriers
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
20
+ OR OTHER DEALINGS IN THE SOFTWARE.
21
+
package/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # @texel/color
2
+
3
+ ![generated](./test/banner.png)
4
+
5
+ A minimal and modern color library for JavaScript. Mainly useful for real-time applications, generative art, and graphics on the web.
6
+
7
+ - Features: fast color conversion, color difference, gamut mapping, and serialization
8
+ - Optimised for speed: approx 20-125 times faster than [Colorjs.io](https://colorjs.io/) (see [benchmarks](#benchmarks))
9
+ - Optimised for low memory and minimal allocations: no arrays or objects are created within conversion and gamut mapping functions
10
+ - Optimised 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
+ - Optimised for accuracy: [high precision](#accuracy) color space matrices
12
+ - Focused on a minimal and modern set of color spaces:
13
+ - xyz (D65), xyz-d50, oklab, oklch, okhsv, okhsl, srgb, srgb-linear, display-p3, display-p3-linear, rec2020, rec2020-linear, a98-rgb, a98-rgb-linear, prophoto-rgb, prophoto-rgb-linear
14
+
15
+ ## Install
16
+
17
+ Use [npm](https://npmjs.com/) to install and import the module.
18
+
19
+ ```sh
20
+ npm install @texel/color --save
21
+ ```
22
+
23
+ ## Examples
24
+
25
+ Converting OKLCH (cylindrical form of OKLab) to sRGB:
26
+
27
+ ```js
28
+ import { convert, OKLCH, sRGB } from "@texel/color";
29
+
30
+ // L = 0 .. 1
31
+ // C = 0 .. 0.4
32
+ // H = 0 .. 360 (degrees)
33
+ const rgb = convert([0.5, 0.15, 30], OKLCH, sRGB);
34
+
35
+ // Note sRGB output is in range 0 .. 1
36
+ // -> [ 0.658, 0.217, 0.165 ]
37
+ ```
38
+
39
+ You can also use wildcard imports:
40
+
41
+ ```js
42
+ import * as colors from "@texel/color";
43
+
44
+ const rgb = colors.convert([0.5, 0.15, 30], colors.OKLCH, colors.sRGB);
45
+ ```
46
+
47
+ > :bulb: Modern bundlers (esbuild, vite) will apply tree-shaking and remove any features that aren't needed, such as color spaces and gamut mapping functions that you didn't reference in your code. The above script results in a ~3.5kb minified bundle with esbuild.
48
+
49
+ Another example with gamut mapping and serialization for wide-gamut Canvas2D:
50
+
51
+ ```js
52
+ import { gamutMapOKLCH, DisplayP3Gamut, sRGBGamut, serialize } from "@texel/color";
53
+
54
+ // Some value that may or may not be in sRGB gamut
55
+ const oklch = [ 0.15, 0.425, 30 ];
56
+
57
+ // decide what gamut you want to map to
58
+ const isDisplayP3Supported = /* check env */;
59
+ const gamut = isDisplayP3Supported ? DisplayP3Gamut : sRGBGamut;
60
+
61
+ // map the input OKLCH to the R,G,B space (sRGB or DisplayP3)
62
+ const rgb = gamutMapOKLCH(oklch, gamut);
63
+
64
+ // get a CSS color string for your output space
65
+ const color = serialize(rgb, gamut.space);
66
+
67
+ // draw color to a Canvas2D context
68
+ const canvas = document.createElement('canvas');
69
+ const context = canvas.getContext('2d', {
70
+ colorSpace: gamut.id
71
+ });
72
+ context.fillStyle = color;
73
+ context.fillRect(0,0, canvas.width, canvas.height);
74
+ ```
75
+
76
+ ## API
77
+
78
+ #### `output = convert(coords, fromSpace, toSpace, output = [0, 0, 0])`
79
+
80
+ Converts the `coords` (typically `[r,g,b]` or `[l,c,h]` or similar), expected to be in `fromSpace`, to the specified `toSpace`. The from and to spaces are one of the [spaces](#color-spaces) interfaces.
81
+
82
+ You can pass `output`, which is a 3 dimensional vector, and the result will be stored into it. This can be used to avoid allocating any new memory in hot code paths.
83
+
84
+ The return value is the new coordinates in the destination space; such as `[r,g,b]` if `sRGB` space is the target. Note that most spaces use normalized and unbounded coordinates; so RGB spaces are in the range 0..1 and might be out of bounds (i.e. out of gamut). It's likely you will want to combine this with `gamutMapOKLCH`, see below.
85
+
86
+ #### `output = gamutMapOKLCH(oklch, gamut = sRGBGamut, targetSpace = gamut.space, out = [0, 0, 0], mapping = MapToCuspL, [cusp])`
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:
89
+
90
+ ```js
91
+ import { gamutMapOKLCH, sRGBGamut, sRGBLinear, OKLCH } from "@texel/color";
92
+
93
+ // gamut map to sRGB but return linear sRGB
94
+ const lrgb = gamutMapOKLCH(oklch, sRGBGamut, sRGBLinear);
95
+
96
+ // or gamut map to sRGB but return OKLCH
97
+ // note, when finally converting this back to sRGB you will likely
98
+ // want to clip the result to 0..1 bounds due to floating point loss
99
+ const lch = gamutMapOKLCH(oklch, sRGBGamut, OKLCH);
100
+ ```
101
+
102
+ You can specify an `out` array to avoid allocations, and the result will be stored into that array. You can also specify a `mapping` function which determines the strategy to use when gamut mapping, and can be one of the following:
103
+
104
+ ```js
105
+ import {
106
+ // possible mappings
107
+ MapToL,
108
+ MapToGray,
109
+ MapToCuspL,
110
+ MapToAdaptiveGray,
111
+ MapToAdaptiveCuspL,
112
+ } from "@texel/color";
113
+
114
+ // preserve lightness when performing sRGB gamut mapping
115
+ const rgb = [0, 0, 0];
116
+ gamutMapOKLCH(oklch, sRGBGamut, sRGB, rgb, MapToL);
117
+ ```
118
+
119
+ The `cusp` can also be passed as the last parameter, allowing for faster evaluation for known hues. See below for calculating the cusp.
120
+
121
+ #### `LC = findCuspOKLCH(a, b, gamut, out = [0, 0])`
122
+
123
+ Finds the 'cusp' of a given OKLab hue plane (denoted with normalized `a` and `b` values in OKLab space), returning the `[L, C]` (lightness and chroma). This is useful for pre-computing aspects of gamut mapping when you are working across a known hue:
124
+
125
+ ```js
126
+ import {
127
+ sRGBGamut,
128
+ findCuspOKLCH,
129
+ gamutMapOKLCH,
130
+ degToRad,
131
+ MapToCuspL,
132
+ } from "@texel/color";
133
+
134
+ const gamut = sRGBGamut;
135
+
136
+ // compute cusp once for this hue
137
+ const H = 30; // e.g. 30º hue
138
+ const hueAngle = degToRad(H);
139
+ const a = Math.cos(hueAngle);
140
+ const b = Math.sin(hueAngle);
141
+ const cuspLC = findCuspOKLCH(a, b, gamut);
142
+
143
+ // ... somewhere else in your program ...
144
+ // pass 'cusp' parameter for faster evaluation
145
+ gamutMapOKLCH(oklch, gamut, gamut.space, out, MapToCuspL, cuspLC);
146
+ ```
147
+
148
+ The `a` and `b` can also be from OKLab coordinates, but must be normalized so `a^2 + b^2 == 1`.
149
+
150
+ #### `str = serialize(coords, inputSpace = sRGB, outputSpace = inputSpace)`
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.
153
+
154
+ ```js
155
+ import { serialize, sRGB, DisplayP3, OKLCH } from "@texel/color";
156
+
157
+ serialize([0, 0.5, 1], sRGB); // "rgb(0, 128, 255)"
158
+ serialize([0, 0.5, 1], DisplayP3); // "color(display-p3 0 0.5 1)"
159
+ serialize([1, 0, 0], OKLCH, sRGB); // "rgb(255, 255, 255)"
160
+ serialize([1, 0, 0], OKLCH); // "oklch(1 0 0)"
161
+ ```
162
+
163
+ #### `delta = deltaEOK(oklabA, oklabB)`
164
+
165
+ 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.
166
+
167
+ #### `[utils]`
168
+
169
+ There are also a host of other [utilities](#utilities) exported by the module.
170
+
171
+ ## Color Spaces
172
+
173
+ The module exports a set of color spaces:
174
+
175
+ ```js
176
+ import {
177
+ XYZ, // using D65 whitepoint
178
+ XYZD50, // using D50 whitepoint
179
+ sRGB,
180
+ sRGBLinear,
181
+ DisplayP3,
182
+ DisplayP3Linear,
183
+ Rec2020,
184
+ Rec2020Linear,
185
+ A98RGB, // Adobe® 1998 RGB
186
+ A98RGBLinear,
187
+ ProPhotoRGB,
188
+ ProPhotoRGBLinear,
189
+ OKLab,
190
+ OKLCH,
191
+ OKHSL, // in sRGB gamut
192
+ OKHSV, // in sRGB gamut
193
+
194
+ // a function to list all spaces
195
+ listColorSpaces,
196
+ } from "@texel/color";
197
+
198
+ console.log(listColorSpaces()); // [XYZ, sRGB, sRGBLinear, ...]
199
+
200
+ console.log(sRGBLinear.id); // "srgb-linear"
201
+ console.log(sRGB.base); // -> sRGBLinear
202
+ console.log(sRGB.fromBase(someLinearRGB)); // -> [gamma-encoded sRGB...]
203
+ console.log(sRGB.toBase(someGammaRGB)); // -> [linear sRGB...]
204
+ ```
205
+
206
+ Note that not all spaces have a `base` field; if not specified, it's assumed the color space can pass through OKLab or XYZ as a root.
207
+
208
+ ## Color Gamuts
209
+
210
+ The module exports a set of "gamuts" which are boundaries defined by an approximation in OKLab space, allowing for fast gamut mapping. These interfaces are mainly used by the `gamutMapOKLCH` function.
211
+
212
+ ```js
213
+ import {
214
+ sRGBGamut,
215
+ DisplayP3Gamut,
216
+ Rec2020Gamut,
217
+ A98RGBGamut,
218
+
219
+ // a function to list all gamuts
220
+ listColorGamuts,
221
+ } from "@texel/color";
222
+
223
+ console.log(listColorGamuts()); // [sRGBGamut, ...]
224
+
225
+ console.log(sRGBGamut.space); // sRGB space
226
+ console.log(sRGBGamut.space.id); // 'srgb'
227
+ ```
228
+
229
+ Note: ProPhoto gamut is not yet supported, I would be open to a PR fixing it within the Python script.
230
+
231
+ ## Utilities
232
+
233
+ In addition to the core API, the module exports a number of utilities:
234
+
235
+ #### `b = floatToByte(f)`
236
+
237
+ Converts the float in range 0..1 to a byte in range 0..255, rounded and clamped.
238
+
239
+ #### `out = XYZ_to_xyY(xyz, out=[0,0,0])`
240
+
241
+ Converts the XYZ coordinates to xyY form, storing the result in `out` if specified before returning.
242
+
243
+ #### `out = xyY_to_XYZ(xyY, out=[0,0,0])`
244
+
245
+ Converts the xyY coordinates to XYZ form, storing the results in `out` if specified before returning.
246
+
247
+ #### `v = lerp(min, max, t)`
248
+
249
+ Performs linear interpolation between min and max with the factor `t`.
250
+
251
+ #### `c = clamp(value, min, max)`
252
+
253
+ Clamps the `value` between min and max and returns the result.
254
+
255
+ #### `out = clampedRGB(inRGB, out=[0,0,0])`
256
+
257
+ Clamps (i.e. clips) the RGB into the range 0..1, storing the result in `out` if specified before returning.
258
+
259
+ #### `inside = isRGBInGamut(rgb, epsilon = 0.000075)`
260
+
261
+ Returns `true` if the given `rgb` is inside its 0..1 gamut boundary, with a threshold of `epsilon`.
262
+
263
+ #### `rgb = hexToRGB(hex, out=[0,0,0])`
264
+
265
+ Converts the specified hex string (with or without a leading `#`) into a floating point RGB triplet in the range 0..1, storing the result in `out` if specified before returning the result.
266
+
267
+ #### `hex = RGBToHex(rgb)`
268
+
269
+ Converts the specified RGB triplet (floating point in the range 0..1) into a 6-character hex color string with a leading `#`.
270
+
271
+ #### `angle = constrainAngle(angle)`
272
+
273
+ Constrains the `angle` (in degrees) to 0..360, wrapping around if needed.
274
+
275
+ #### `degAngle = radToDeg(radAngle)`
276
+
277
+ Converts the angle (given in radians) to degrees.
278
+
279
+ #### `radAngle = degToRad(degAngle)`
280
+
281
+ Converts the angle (given in degrees) to radians.
282
+
283
+ ## Transformation Matrices
284
+
285
+ You can also import the lower level functions and matrices; this may be useful for granular conversions, or for example uploading the buffers to WebGPU for compute shaders.
286
+
287
+ ```js
288
+ import {
289
+ OKLab_to,
290
+ OKLab_from,
291
+ transform,
292
+ XYZ_to_linear_sRGB_M,
293
+ LMS_to_XYZ_M,
294
+ XYZ_to_LMS_M,
295
+ sRGB,
296
+ } from "@texel/color";
297
+
298
+ console.log(XYZ_to_linear_sRGB_M); // [ [a,b,c], ... ]
299
+ OKLab_to(oklab, LMS_to_XYZ_M); // OKLab -> XYZ D65
300
+ OKLab_from(xyzD65, XYZ_to_LMS_M); // XYZ D65 -> OKLab
301
+ transform(xyzD65, XYZ_to_linear_sRGB_M); // XYZ D65 -> sRGBLinear
302
+ sRGB.fromBase(in_linear_sRGB, out_sRGB); // linear to gamma transfer function
303
+ sRGB.toBase(in_sRGB, out_linear_sRGB); // linear to gamma transfer function
304
+ ```
305
+
306
+ ## Notes
307
+
308
+ ### Why another library?
309
+
310
+ [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. Colorjs is more focused on matching CSS spec, which means it will very likely continue to grow in complexity, and performance will often be marred (for example, `@texel/color` cusp intersection gamut mapping is ~125 times faster than Colorjs and its defaultt CSS based gamut mapping).
311
+
312
+ 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).
313
+
314
+ ### Supported Spaces
315
+
316
+ 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.
317
+
318
+ ### Improvements & Techniques
319
+
320
+ The module uses a few of the following practices for the significant optimization and bundle size improvements:
321
+
322
+ - Loops, closures, destructuring, and other syntax sugars are replaced with more optimized code paths and plain array access.
323
+ - Allocations in hot code paths have been removed, temporary arrays are re-used if needed.
324
+ - Certain conversions, such as OKLab to sRGB, do not need to pass through XYZ first, and can be directly converted using a known matrix.
325
+ - The API design is structured such that color spaces are generally not referenced internally, allowing them to be automatically tree-shaked.
326
+
327
+ ### Accuracy
328
+
329
+ All conversions have been tested to approximately equal Colorjs conversions, within a tolerance of 2<sup>-33</sup> (10 decimal places), in some cases it is more accurate than that.
330
+
331
+ This library uses [coloraide](https://github.com/facelessuser/coloraide) and its Python tools for computing conversion matrices and OKLab gamut approximations. Some matrices have been hard-coded into the script, and rational numbers are used where possible (as [suggested](https://github.com/w3c/csswg-drafts/pull/7320) by [CSS Color Module working draft spec](https://drafts.csswg.org/css-color-4/#color-conversion-code)).
332
+
333
+ If you think the matrices or accuracy could be improved, please open a PR.
334
+
335
+ ### Benchmarks
336
+
337
+ There are a few benchmarks inside [test](./test):
338
+
339
+ - [bench-colorjs.js](./test/bench-colorjs.js) - run with `npm run bench` to compare against colorjs
340
+ - [bench-node.js](./test/bench-node.js) - run with `npm run bench:node` to get a node profile
341
+ - [bench-size.js](./test/bench-size.js) - run with `npm run bench:size` to get a small bundle size with esbuild
342
+
343
+ Colorjs comparison benchmark on MacBook Air M2:
344
+
345
+ ```
346
+ OKLCH to sRGB with gamut mapping --
347
+ Colorjs: 6146.67 ms
348
+ Ours: 46.77 ms
349
+ Speedup: 131.4x faster
350
+
351
+ All Conversions --
352
+ Colorjs: 10219.40 ms
353
+ Ours: 431.13 ms
354
+ Speedup: 23.7x faster
355
+
356
+ Conversion + Gamut Mapping --
357
+ Colorjs: 1936.29 ms
358
+ Ours: 82.04 ms
359
+ Speedup: 23.6x faster
360
+ ```
361
+
362
+ ### Running Locally
363
+
364
+ Clone, `npm install`, then `npm run` to list the available scripts, or `npm t` to run the tests.
365
+
366
+ ## Attributions
367
+
368
+ This library was made possible due to the excellent prior work by many developers and engineers:
369
+
370
+ - [Colorjs.io](https://colorjs.io)
371
+ - [Coloraide](https://github.com/facelessuser/coloraide/)
372
+ - [CSS Color Module Level 4 Spec](https://www.w3.org/TR/css-color-4/)
373
+
374
+ ## License
375
+
376
+ MIT, see [LICENSE.md](http://github.com/mattdesl/@texel/color/blob/master/LICENSE.md) for details.
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@texel/color",
3
+ "version": "1.0.0",
4
+ "description": "an esoteric colour picker in OKLCH",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "license": "MIT",
8
+ "author": {
9
+ "name": "Matt DesLauriers",
10
+ "url": "https://github.com/mattdesl"
11
+ },
12
+ "devDependencies": {
13
+ "canvas-sketch": "^0.7.7",
14
+ "canvas-sketch-cli": "^1.11.21",
15
+ "colorjs.io": "^0.5.2",
16
+ "esbuild": "^0.23.0",
17
+ "faucet": "^0.0.4",
18
+ "pako": "^2.1.0",
19
+ "png-tools": "^1.0.4",
20
+ "prettier": "^3.3.3",
21
+ "tape": "^5.8.1",
22
+ "terser": "^5.31.3"
23
+ },
24
+ "scripts": {
25
+ "visualize": "canvas-sketch-cli test/canvas-graph.js --open",
26
+ "test": "faucet test/test*.js",
27
+ "bench": "node test/bench-colorjs.js",
28
+ "bench:node": "NODE_ENV=production node --prof --no-logfile-per-isolate test/bench-node.js && node --prof-process v8.log",
29
+ "bench:size": "esbuild test/bench-size.js --format=esm --bundle --minify --tree-shaking=true | wc -c",
30
+ "matrices": "python3 tools/print_matrices.py > src/conversion_matrices.js && prettier src/conversion_matrices.js --write"
31
+ },
32
+ "keywords": [
33
+ "oklch",
34
+ "oklab",
35
+ "okhsl",
36
+ "okhsv",
37
+ "display-p3",
38
+ "p3",
39
+ "displayp3",
40
+ "prophoto",
41
+ "a98rgb",
42
+ "adobe1998",
43
+ "prophotorgb",
44
+ "color",
45
+ "colour",
46
+ "picker",
47
+ "tool",
48
+ "rgb",
49
+ "srgb",
50
+ "convert",
51
+ "saturation",
52
+ "chroma",
53
+ "perceptual",
54
+ "uniform",
55
+ "perceptually"
56
+ ],
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git://github.com/texel-org/color.git"
60
+ },
61
+ "homepage": "https://github.com/texel-org/color",
62
+ "bugs": {
63
+ "url": "https://github.com/texel-org/color/issues"
64
+ }
65
+ }