color-bits 0.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 ADDED
@@ -0,0 +1,3 @@
1
+ # color-bits
2
+
3
+ Fast & efficient color manipulation library.
@@ -0,0 +1,6 @@
1
+ {
2
+ "presets": [
3
+ ["@babel/preset-env", { "targets": { "node": "current" } }],
4
+ "@babel/preset-typescript"
5
+ ]
6
+ }
package/jest.config.js ADDED
@@ -0,0 +1,9 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ export default {
3
+ extensionsToTreatAsEsm: ['.ts'],
4
+ transform: {
5
+ '\\.ts$': ['ts-jest', {
6
+ useESM: true,
7
+ }],
8
+ },
9
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "color-bits",
3
+ "version": "0.1.0",
4
+ "main": "index.js",
5
+ "exports": {
6
+ ".": "./build/index.js",
7
+ "./*": "./build/*"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "description": "",
13
+ "devDependencies": {
14
+ "@babel/preset-env": "^7.25.4",
15
+ "@babel/preset-typescript": "^7.24.7",
16
+ "@jest/globals": "^29.7.0",
17
+ "@types/chai": "^4.3.19",
18
+ "@types/jest": "^29.5.12",
19
+ "chai": "^5.1.1",
20
+ "jest": "^29.7.0",
21
+ "ts-jest": "^29.2.5",
22
+ "typescript": "^5.5.4"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "start": "tsc --watch",
27
+ "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --watch"
28
+ }
29
+ }
package/src/bit.ts ADDED
@@ -0,0 +1,27 @@
1
+ // Bitwise functions
2
+ //
3
+ // The color representation would ideally be 32-bits unsigned, but JS bitwise
4
+ // operators only work as 32-bits signed. The range of Smi values on V8 is also
5
+ // 32-bits signed. Those two factors make it that it's much more efficient to just
6
+ // use signed integers to represent the data.
7
+ //
8
+ // Colors with a R channel >= 0x80 will be a negative number, but that's not really
9
+ // an issue at any point because the bits for signed and unsigned integers are always
10
+ // the same, only their interpretation changes.
11
+
12
+ const INT32_TO_UINT32_OFFSET = 2 ** 32;
13
+
14
+ export function cast(n: number) {
15
+ if (n < 0) {
16
+ return n + INT32_TO_UINT32_OFFSET;
17
+ }
18
+ return n;
19
+ }
20
+
21
+ export function get(n: number, offset: number) {
22
+ return (n >> offset) & 0xff;
23
+ }
24
+
25
+ export function set(n: number, offset: number, byte: number) {
26
+ return n ^ ((n ^ (byte << offset)) & (0xff << offset));
27
+ }
@@ -0,0 +1,66 @@
1
+ import { expect } from 'chai'
2
+ import * as Color from './index';
3
+
4
+ const c = Color.from;
5
+
6
+ describe('Color', () => {
7
+ it('can encode/decode the representation', () => {
8
+ const color = Color.from(0x599eff80)
9
+ expect(Color.getRed(color)).to.equal(0x59)
10
+ expect(Color.getGreen(color)).to.equal(0x9e)
11
+ expect(Color.getBlue(color)).to.equal(0xff)
12
+ expect(Color.getAlpha(color)).to.equal(0x80)
13
+ });
14
+
15
+ it('can set channels', () => {
16
+ const color = Color.parse('#ffffff')
17
+ expect(Color.setRed(color, 0)).to.equal(c(0x00ffffff))
18
+ expect(Color.setGreen(color, 0)).to.equal(c(0xff00ffff))
19
+ expect(Color.setBlue(color, 0)).to.equal(c(0xffff00ff))
20
+ expect(Color.setAlpha(color, 0)).to.equal(c(0xffffff00))
21
+ });
22
+
23
+ describe('.parse():', () => {
24
+ it('parses CSS hexadecimal', () => {
25
+ expect(Color.parse('#59f')).to.equal(c(0x5599ffff));
26
+ expect(Color.parse('#5599ff')).to.equal(c(0x5599ffff));
27
+ expect(Color.parse('#5599ffff')).to.equal(c(0x5599ffff));
28
+ });
29
+
30
+ it('parses CSS color spaces', () => {
31
+ ['rgb', 'rgba'].forEach(type => {
32
+ expect(Color.parse(`${type}(255 153 85)`)).to.equal(c(0xff9955ff));
33
+ expect(Color.parse(`${type}(255, 153, 85)`)).to.equal(c(0xff9955ff));
34
+ expect(Color.parse(`${type}(255 153 85 / 50%)`)).to.equal(c(0xff995580));
35
+ expect(Color.parse(`${type}(255 153 85 / .5)`)).to.equal(c(0xff995580));
36
+ expect(Color.parse(`${type}(255 153 85 / 0.5)`)).to.equal(c(0xff995580));
37
+ });
38
+
39
+ ['hsl', 'hsla'].forEach(type => {
40
+ expect(Color.parse(`${type}(50deg 80% 40% / 50%)`)).to.equal(c(0xb89c1480));
41
+ expect(Color.parse(`${type}(50deg 80% 40% / 0.5)`)).to.equal(c(0xb89c1480));
42
+ expect(Color.parse(`${type}(0 80% 40% / 0.5)`)).to.equal(c(0xb8141480));
43
+ expect(Color.parse(`${type}(none 80% 40% / 0.5)`)).to.equal(c(0xb8141480));
44
+ expect(Color.parse(`${type}(1turn 80% 40% / 0.5)`)).to.equal(c(0xb8141480));
45
+ expect(Color.parse(`${type}(400grad 80% 40% / 0.5)`)).to.equal(c(0xb8141480));
46
+ expect(Color.parse(`${type}(0rad 80% 40% / 0.5)`)).to.equal(c(0xb8141480));
47
+ });
48
+
49
+ expect(Color.parse('hwb(12 50% 0%)')).to.equal(c(0xff9980ff));
50
+ expect(Color.parse('hwb(50deg 30% 40%)')).to.equal(c(0x998c4dff));
51
+ expect(Color.parse('hwb(0.5turn 10% 0% / .5)')).to.equal(c(0x1affff80));
52
+
53
+ // FIXME:
54
+ // expect(Color.parse('lab(50% 40 59.5 / 0.5)')).to.equal(c(0xbf570080))
55
+ // expect(Color.parse('lch(52.2% 72.2 50 / 0.5)')).to.equal(c(0xcd561a80))
56
+
57
+ expect(Color.parse('color(srgb 1 0.5 0 / 0.5)')).to.equal(c(0xff800080))
58
+ expect(Color.parse('color(srgb-linear 1 0.5 0 / 0.5)')).to.equal(c(0xffbc0080))
59
+ expect(Color.parse('color(display-p3 1 0.5 0 / 0.5)')).to.equal(c(0xff760080))
60
+ expect(Color.parse('color(a98-rgb 1 0.5 0 / 0.5)')).to.equal(c(0xff810080))
61
+ expect(Color.parse('color(prophoto-rgb 1 0.5 0 / 0.5)')).to.equal(c(0xff630080))
62
+ expect(Color.parse('color(rec2020 1 0.5 0 / 0.5)')).to.equal(c(0xff720080))
63
+ });
64
+
65
+ });
66
+ });
package/src/convert.ts ADDED
@@ -0,0 +1,392 @@
1
+ // Copyright 2022 The Chromium Authors. All rights reserved.
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ //
5
+ // Source: https://github.com/ChromeDevTools/devtools-frontend/blob/c51201e6ee70370f7f1ac8a1a49dca7d4561aeaa/front_end/core/common/ColorConverter.ts
6
+ // License: https://github.com/ChromeDevTools/devtools-frontend/blob/c51201e6ee70370f7f1ac8a1a49dca7d4561aeaa/LICENSE
7
+
8
+ /**
9
+ * Implementation of this module and all the tests are heavily influenced by
10
+ * https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/color_conversions.cc
11
+ */
12
+
13
+ // https://en.wikipedia.org/wiki/CIELAB_color_space#Converting_between_CIELAB_and_CIEXYZ_coordinates
14
+ const D50_X = 0.9642;
15
+ const D50_Y = 1.0;
16
+ const D50_Z = 0.8251;
17
+
18
+ type Matrix3x3 = [
19
+ [number, number, number],
20
+ [number, number, number],
21
+ [number, number, number],
22
+ ];
23
+
24
+ type Vector3 = [number, number, number];
25
+
26
+ function multiply(matrix: Matrix3x3, other: Vector3): Vector3 {
27
+ const dst = [0, 0, 0] as Vector3;
28
+ for (let row = 0; row < 3; ++row) {
29
+ dst[row] = matrix[row][0] * other[0] + matrix[row][1] * other[1] +
30
+ matrix[row][2] * other[2];
31
+ }
32
+ return dst;
33
+ }
34
+
35
+ // A transfer function mapping encoded values to linear values,
36
+ // represented by this 7-parameter piecewise function:
37
+ //
38
+ // linear = sign(encoded) * (c*|encoded| + f) , 0 <= |encoded| < d
39
+ // = sign(encoded) * ((a*|encoded| + b)^g + e), d <= |encoded|
40
+ //
41
+ // (A simple gamma transfer function sets g to gamma and a to 1.)
42
+ class TransferFunction {
43
+ g: number;
44
+ a: number;
45
+ b: number;
46
+ c: number;
47
+ d: number;
48
+ e: number;
49
+ f: number;
50
+
51
+ constructor(g: number, a: number, b: number = 0, c: number = 0, d: number = 0, e: number = 0, f: number = 0) {
52
+ this.g = g;
53
+ this.a = a;
54
+ this.b = b;
55
+ this.c = c;
56
+ this.d = d;
57
+ this.e = e;
58
+ this.f = f;
59
+ }
60
+
61
+ eval(val: number): number {
62
+ const sign = val < 0 ? -1.0 : 1.0;
63
+ const abs = val * sign;
64
+
65
+ // 0 <= |encoded| < d path
66
+ if (abs < this.d) {
67
+ return sign * (this.c * abs + this.f);
68
+ }
69
+
70
+ // d <= |encoded| path
71
+ return sign * (Math.pow(this.a * abs + this.b, this.g) + this.e);
72
+ }
73
+ }
74
+
75
+ const NAMED_TRANSFER_FN = {
76
+ sRGB: new TransferFunction(2.4, (1 / 1.055), (0.055 / 1.055), (1 / 12.92), 0.04045, 0.0, 0.0),
77
+ sRGB_INVERSE: new TransferFunction(0.416667, 1.13728, -0, 12.92, 0.0031308, -0.0549698, -0),
78
+
79
+ proPhotoRGB: new TransferFunction(1.8, 1),
80
+ proPhotoRGB_INVERSE: new TransferFunction(0.555556, 1, -0, 0, 0, 0, 0),
81
+
82
+ k2Dot2: new TransferFunction(2.2, 1.0),
83
+ k2Dot2_INVERSE: new TransferFunction(0.454545, 1),
84
+
85
+ rec2020: new TransferFunction(2.22222, 0.909672, 0.0903276, 0.222222, 0.0812429, 0, 0),
86
+ rec2020_INVERSE: new TransferFunction(0.45, 1.23439, -0, 4.5, 0.018054, -0.0993195, -0),
87
+ };
88
+
89
+ const NAMED_GAMUTS = {
90
+ sRGB: [
91
+ [0.436065674, 0.385147095, 0.143066406],
92
+ [0.222488403, 0.716873169, 0.060607910],
93
+ [0.013916016, 0.097076416, 0.714096069],
94
+ ] as Matrix3x3,
95
+ sRGB_INVERSE: [
96
+ [3.134112151374599, -1.6173924597114966, -0.4906334036481285],
97
+ [-0.9787872938826594, 1.9162795854799963, 0.0334547139520088],
98
+ [0.07198304248352326, -0.2289858493321844, 1.4053851325241447],
99
+ ] as Matrix3x3,
100
+ displayP3: [
101
+ [0.515102, 0.291965, 0.157153],
102
+ [0.241182, 0.692236, 0.0665819],
103
+ [-0.00104941, 0.0418818, 0.784378],
104
+ ] as Matrix3x3,
105
+ displayP3_INVERSE: [
106
+ [2.404045155982687, -0.9898986932663839, -0.3976317191366333],
107
+ [-0.8422283799266768, 1.7988505115115485, 0.016048170293157416],
108
+ [0.04818705979712955, -0.09737385156228891, 1.2735066448052303],
109
+ ] as Matrix3x3,
110
+ adobeRGB: [
111
+ [0.60974, 0.20528, 0.14919],
112
+ [0.31111, 0.62567, 0.06322],
113
+ [0.01947, 0.06087, 0.74457],
114
+ ] as Matrix3x3,
115
+ adobeRGB_INVERSE: [
116
+ [1.9625385510109137, -0.6106892546501431, -0.3413827467482388],
117
+ [-0.9787580455521, 1.9161624707082339, 0.03341676594241408],
118
+ [0.028696263137883395, -0.1406807819331586, 1.349252109991369],
119
+ ] as Matrix3x3,
120
+ rec2020: [
121
+ [0.673459, 0.165661, 0.125100],
122
+ [0.279033, 0.675338, 0.0456288],
123
+ [-0.00193139, 0.0299794, 0.797162],
124
+ ] as Matrix3x3,
125
+ rec2020_INVERSE: [
126
+ [1.647275201661012, -0.3936024771460771, -0.23598028884792507],
127
+ [-0.6826176165196962, 1.647617775014935, 0.01281626807852422],
128
+ [0.029662725298529837, -0.06291668721366285, 1.2533964313435522],
129
+ ] as Matrix3x3,
130
+ xyz: [
131
+ [1.0, 0.0, 0.0],
132
+ [0.0, 1.0, 0.0],
133
+ [0.0, 0.0, 1.0],
134
+ ] as Matrix3x3,
135
+
136
+ };
137
+
138
+ function degToRad(deg: number): number {
139
+ return deg * (Math.PI / 180);
140
+ }
141
+
142
+ function radToDeg(rad: number): number {
143
+ return rad * (180 / Math.PI);
144
+ }
145
+
146
+ function applyTransferFns(fn: TransferFunction, r: number, g: number, b: number): [number, number, number] {
147
+ return [fn.eval(r), fn.eval(g), fn.eval(b)];
148
+ }
149
+
150
+ const OKLAB_TO_LMS_MATRIX = [
151
+ [0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339],
152
+ [1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402],
153
+ [1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399],
154
+ ] as Matrix3x3;
155
+
156
+ // Inverse of the OKLAB_TO_LMS_MATRIX
157
+ const LMS_TO_OKLAB_MATRIX = [
158
+ [0.2104542553, 0.7936177849999999, -0.0040720468],
159
+ [1.9779984951000003, -2.4285922049999997, 0.4505937099000001],
160
+ [0.025904037099999982, 0.7827717662, -0.8086757660000001],
161
+ ] as Matrix3x3;
162
+
163
+ const XYZ_TO_LMS_MATRIX = [
164
+ [0.8190224432164319, 0.3619062562801221, -0.12887378261216414],
165
+ [0.0329836671980271, 0.9292868468965546, 0.03614466816999844],
166
+ [0.048177199566046255, 0.26423952494422764, 0.6335478258136937],
167
+ ] as Matrix3x3;
168
+ // Inverse of XYZ_TO_LMS_MATRIX
169
+ const LMS_TO_XYZ_MATRIX = [
170
+ [1.226879873374156, -0.5578149965554814, 0.2813910501772159],
171
+ [-0.040575762624313734, 1.1122868293970596, -0.07171106666151703],
172
+ [-0.07637294974672144, -0.4214933239627915, 1.586924024427242],
173
+ ] as Matrix3x3;
174
+
175
+ const PRO_PHOTO_TO_XYZD50_MATRIX = [
176
+ [0.7976700747153241, 0.13519395152800417, 0.03135596341127167],
177
+ [0.28803902352472205, 0.7118744007923554, 0.00008661179538844252],
178
+ [2.739876695467402e-7, -0.0000014405226518969991, 0.825211112593861],
179
+ ] as Matrix3x3;
180
+ // Inverse of PRO_PHOTO_TO_XYZD50_MATRIX
181
+ const XYZD50_TO_PRO_PHOTO_MATRIX = [
182
+ [1.3459533710138858, -0.25561367037652133, -0.051116041522131374],
183
+ [-0.544600415668951, 1.5081687311475767, 0.020535163968720935],
184
+ [-0.0000013975622054109725, 0.000002717590904589903, 1.2118111696814942],
185
+ ] as Matrix3x3;
186
+
187
+ const XYZD65_TO_XYZD50_MATRIX = [
188
+ [1.0478573189120088, 0.022907374491829943, -0.050162247377152525],
189
+ [0.029570500050499514, 0.9904755577034089, -0.017061518194840468],
190
+ [-0.00924047197558879, 0.015052921526981566, 0.7519708530777581],
191
+ ] as Matrix3x3;
192
+ // Inverse of XYZD65_TO_XYZD50_MATRIX
193
+ const XYZD50_TO_XYZD65_MATRIX = [
194
+ [0.9555366447632887, -0.02306009252137888, 0.06321844147263304],
195
+ [-0.028315378228764922, 1.009951351591575, 0.021026001591792402],
196
+ [0.012308773293784308, -0.02050053471777469, 1.3301947294775631],
197
+ ] as Matrix3x3;
198
+
199
+ const XYZD65_TO_SRGB_MATRIX = [
200
+ [3.2408089365140573, -1.5375788839307314, -0.4985609572551541],
201
+ [-0.9692732213205414, 1.876110235238969, 0.041560501141251774],
202
+ [0.05567030990267439, -0.2040007921971802, 1.0571046720577026],
203
+ ] as Matrix3x3;
204
+
205
+ export function labToXyzd50(l: number, a: number, b: number): [number, number, number] {
206
+ let y = (l + 16.0) / 116.0;
207
+ let x = y + a / 500.0;
208
+ let z = y - b / 200.0;
209
+
210
+ function labInverseTransferFunction(t: number): number {
211
+ const delta = (24.0 / 116.0);
212
+
213
+ if (t <= delta) {
214
+ return (108.0 / 841.0) * (t - (16.0 / 116.0));
215
+ }
216
+
217
+ return t * t * t;
218
+ }
219
+
220
+ x = labInverseTransferFunction(x) * D50_X;
221
+ y = labInverseTransferFunction(y) * D50_Y;
222
+ z = labInverseTransferFunction(z) * D50_Z;
223
+
224
+ return [x, y, z];
225
+ }
226
+
227
+ export function xyzd50ToLab(x: number, y: number, z: number): [number, number, number] {
228
+ function labTransferFunction(t: number): number {
229
+ const deltaLimit: number = (24.0 / 116.0) * (24.0 / 116.0) * (24.0 / 116.0);
230
+
231
+ if (t <= deltaLimit) {
232
+ return (841.0 / 108.0) * t + (16.0 / 116.0);
233
+ }
234
+ return Math.pow(t, 1.0 / 3.0);
235
+ }
236
+
237
+ x = labTransferFunction(x / D50_X);
238
+ y = labTransferFunction(y / D50_Y);
239
+ z = labTransferFunction(z / D50_Z);
240
+
241
+ const l = 116.0 * y - 16.0;
242
+ const a = 500.0 * (x - y);
243
+ const b = 200.0 * (y - z);
244
+
245
+ return [l, a, b];
246
+ }
247
+
248
+ export function oklabToXyzd65(l: number, a: number, b: number): [number, number, number] {
249
+ const labInput = [l, a, b] as Vector3;
250
+ const lmsIntermediate = multiply(OKLAB_TO_LMS_MATRIX, labInput);
251
+ lmsIntermediate[0] = lmsIntermediate[0] * lmsIntermediate[0] * lmsIntermediate[0];
252
+ lmsIntermediate[1] = lmsIntermediate[1] * lmsIntermediate[1] * lmsIntermediate[1];
253
+ lmsIntermediate[2] = lmsIntermediate[2] * lmsIntermediate[2] * lmsIntermediate[2];
254
+ const xyzOutput = multiply(LMS_TO_XYZ_MATRIX, lmsIntermediate);
255
+ return xyzOutput;
256
+ }
257
+
258
+ export function xyzd65ToOklab(x: number, y: number, z: number): [number, number, number] {
259
+ const xyzInput = [x, y, z] as Vector3;
260
+ const lmsIntermediate = multiply(XYZ_TO_LMS_MATRIX, xyzInput);
261
+
262
+ lmsIntermediate[0] = Math.pow(lmsIntermediate[0], 1.0 / 3.0);
263
+ lmsIntermediate[1] = Math.pow(lmsIntermediate[1], 1.0 / 3.0);
264
+ lmsIntermediate[2] = Math.pow(lmsIntermediate[2], 1.0 / 3.0);
265
+
266
+ const labOutput = multiply(LMS_TO_OKLAB_MATRIX, lmsIntermediate);
267
+ return [labOutput[0], labOutput[1], labOutput[2]];
268
+ }
269
+
270
+ export function lchToLab(l: number, c: number, h: number|undefined): [number, number, number] {
271
+ if (h === undefined) {
272
+ return [l, 0, 0];
273
+ }
274
+
275
+ return [l, c * Math.cos(degToRad(h)), c * Math.sin(degToRad(h))];
276
+ }
277
+
278
+ export function labToLch(l: number, a: number, b: number): [number, number, number] {
279
+ return [l, Math.sqrt(a * a + b * b), radToDeg(Math.atan2(b, a))];
280
+ }
281
+
282
+ export function displayP3ToXyzd50(r: number, g: number, b: number): [number, number, number] {
283
+ const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.sRGB, r, g, b);
284
+ const rgbInput = [mappedR, mappedG, mappedB] as Vector3;
285
+ const xyzOutput = multiply(NAMED_GAMUTS.displayP3, rgbInput);
286
+ return xyzOutput;
287
+ }
288
+
289
+ export function xyzd50ToDisplayP3(x: number, y: number, z: number): [number, number, number] {
290
+ const xyzInput = [x, y, z] as Vector3;
291
+ const rgbOutput = multiply(NAMED_GAMUTS.displayP3_INVERSE, xyzInput);
292
+ return applyTransferFns(
293
+ NAMED_TRANSFER_FN.sRGB_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]);
294
+ }
295
+
296
+ export function proPhotoToXyzd50(r: number, g: number, b: number): [number, number, number] {
297
+ const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.proPhotoRGB, r, g, b);
298
+ const rgbInput = [mappedR, mappedG, mappedB] as Vector3;
299
+ const xyzOutput = multiply(PRO_PHOTO_TO_XYZD50_MATRIX, rgbInput);
300
+ return xyzOutput;
301
+ }
302
+
303
+ export function xyzd50ToProPhoto(x: number, y: number, z: number): [number, number, number] {
304
+ const xyzInput = [x, y, z] as Vector3;
305
+ const rgbOutput = multiply(XYZD50_TO_PRO_PHOTO_MATRIX, xyzInput);
306
+ return applyTransferFns(
307
+ NAMED_TRANSFER_FN.proPhotoRGB_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]);
308
+ }
309
+
310
+ export function adobeRGBToXyzd50(r: number, g: number, b: number): [number, number, number] {
311
+ const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.k2Dot2, r, g, b);
312
+ const rgbInput = [mappedR, mappedG, mappedB] as Vector3;
313
+ const xyzOutput = multiply(NAMED_GAMUTS.adobeRGB, rgbInput);
314
+ return xyzOutput;
315
+ }
316
+
317
+ export function xyzd50ToAdobeRGB(x: number, y: number, z: number): [number, number, number] {
318
+ const xyzInput = [x, y, z] as Vector3;
319
+ const rgbOutput = multiply(NAMED_GAMUTS.adobeRGB_INVERSE, xyzInput);
320
+ return applyTransferFns(
321
+ NAMED_TRANSFER_FN.k2Dot2_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]);
322
+ }
323
+
324
+ export function rec2020ToXyzd50(r: number, g: number, b: number): [number, number, number] {
325
+ const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.rec2020, r, g, b);
326
+ const rgbInput = [mappedR, mappedG, mappedB] as Vector3;
327
+ const xyzOutput = multiply(NAMED_GAMUTS.rec2020, rgbInput);
328
+ return xyzOutput;
329
+ }
330
+
331
+ export function xyzd50ToRec2020(x: number, y: number, z: number): [number, number, number] {
332
+ const xyzInput = [x, y, z] as Vector3;
333
+ const rgbOutput = multiply(NAMED_GAMUTS.rec2020_INVERSE, xyzInput);
334
+ return applyTransferFns(
335
+ NAMED_TRANSFER_FN.rec2020_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]);
336
+ }
337
+
338
+ export function xyzd50ToD65(x: number, y: number, z: number): [number, number, number] {
339
+ const xyzInput = [x, y, z] as Vector3;
340
+ const xyzOutput = multiply(XYZD50_TO_XYZD65_MATRIX, xyzInput);
341
+ return xyzOutput;
342
+ }
343
+
344
+ export function xyzd65ToD50(x: number, y: number, z: number): [number, number, number] {
345
+ const xyzInput = [x, y, z] as Vector3;
346
+ const xyzOutput = multiply(XYZD65_TO_XYZD50_MATRIX, xyzInput);
347
+ return xyzOutput;
348
+ }
349
+
350
+ export function xyzd65TosRGBLinear(x: number, y: number, z: number): [number, number, number] {
351
+ const xyzInput = [x, y, z] as Vector3;
352
+ const rgbResult = multiply(XYZD65_TO_SRGB_MATRIX, xyzInput);
353
+ return rgbResult;
354
+ }
355
+
356
+ export function xyzd50TosRGBLinear(x: number, y: number, z: number): [number, number, number] {
357
+ const xyzInput = [x, y, z] as Vector3;
358
+ const rgbResult = multiply(NAMED_GAMUTS.sRGB_INVERSE, xyzInput);
359
+ return rgbResult;
360
+ }
361
+
362
+ export function srgbLinearToXyzd50(r: number, g: number, b: number): [number, number, number] {
363
+ const rgbInput = [r, g, b] as Vector3;
364
+ const xyzOutput = multiply(NAMED_GAMUTS.sRGB, rgbInput);
365
+ return xyzOutput;
366
+ }
367
+
368
+ export function srgbToXyzd50(r: number, g: number, b: number): [number, number, number] {
369
+ const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.sRGB, r, g, b);
370
+ const rgbInput = [mappedR, mappedG, mappedB] as Vector3;
371
+ const xyzOutput = multiply(NAMED_GAMUTS.sRGB, rgbInput);
372
+ return xyzOutput;
373
+ }
374
+
375
+ export function xyzd50ToSrgb(x: number, y: number, z: number): [number, number, number] {
376
+ const xyzInput = [x, y, z] as Vector3;
377
+ const rgbOutput = multiply(NAMED_GAMUTS.sRGB_INVERSE, xyzInput);
378
+ return applyTransferFns(
379
+ NAMED_TRANSFER_FN.sRGB_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]);
380
+ }
381
+
382
+ export function oklchToXyzd50(lInput: number, c: number, h: number): [number, number, number] {
383
+ const [l, a, b] = lchToLab(lInput, c, h);
384
+ const [x65, y65, z65] = oklabToXyzd65(l, a, b);
385
+ return xyzd65ToD50(x65, y65, z65);
386
+ }
387
+
388
+ export function xyzd50ToOklch(x: number, y: number, z: number): [number, number, number] {
389
+ const [x65, y65, z65] = xyzd50ToD65(x, y, z);
390
+ const [l, a, b] = xyzd65ToOklab(x65, y65, z65);
391
+ return labToLch(l, a, b);
392
+ }
package/src/core.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { cast, get, set } from './bit';
2
+
3
+ export type Color = number;
4
+
5
+ export const OFFSET_R = 24;
6
+ export const OFFSET_G = 16;
7
+ export const OFFSET_B = 8;
8
+ export const OFFSET_A = 0;
9
+
10
+ export function newColor(r: number, g: number, b: number, a: number) {
11
+ return (
12
+ (r << OFFSET_R) +
13
+ (g << OFFSET_G) +
14
+ (b << OFFSET_B) +
15
+ (a << OFFSET_A)
16
+ );
17
+ }
18
+
19
+ export function from(hex: number) {
20
+ return newColor(
21
+ get(hex, OFFSET_R),
22
+ get(hex, OFFSET_G),
23
+ get(hex, OFFSET_B),
24
+ get(hex, OFFSET_A),
25
+ );
26
+ }
27
+
28
+ export function toNumber(color: Color) {
29
+ return cast(color);
30
+ }
31
+
32
+ export function getRed(c: Color) { return get(c, OFFSET_R); }
33
+ export function getGreen(c: Color) { return get(c, OFFSET_G); }
34
+ export function getBlue(c: Color) { return get(c, OFFSET_B); }
35
+ export function getAlpha(c: Color) { return get(c, OFFSET_A); }
36
+
37
+ export function setRed(c: Color, value: number) { return set(c, OFFSET_R, value); }
38
+ export function setGreen(c: Color, value: number) { return set(c, OFFSET_G, value); }
39
+ export function setBlue(c: Color, value: number) { return set(c, OFFSET_B, value); }
40
+ export function setAlpha(c: Color, value: number) { return set(c, OFFSET_A, value); }
41
+
42
+ /**
43
+ * Map 8-bits value to its hexadecimal representation
44
+ * ['00', '01', '02', ..., 'fe', 'ff']
45
+ */
46
+ const FORMAT_HEX =
47
+ Array.from({ length: 256 })
48
+ .map((_, byte) => byte.toString(16).padStart(2, '0'))
49
+
50
+ export function format(color: Color): string {
51
+ /*
52
+ * Implementing this as `cast(color).toString(16).padStart(8, '0')` would be simpler,
53
+ * but `cast()` makes the color a non-Smi value on V8, as well as allocates more strings
54
+ * and makes more function calls.
55
+ * This version is about 4 times faster than the simple one.
56
+ */
57
+ return '#' + FORMAT_HEX[getRed(color)] + FORMAT_HEX[getGreen(color)] + FORMAT_HEX[getBlue(color)] + FORMAT_HEX[getAlpha(color)];
58
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ Color,
3
+ getRed,
4
+ getGreen,
5
+ getBlue,
6
+ } from './core';
7
+
8
+ /**
9
+ * The relative brightness of any point in a color space, normalized to 0 for
10
+ * darkest black and 1 for lightest white.
11
+ * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
12
+ * @returns The relative brightness of the color in the range 0 - 1
13
+ */
14
+ export function getLuminance(color: Color) {
15
+ const r = getRed(color) / 255;
16
+ const g = getGreen(color) / 255;
17
+ const b = getBlue(color) / 255;
18
+
19
+ const apply = (v: number) => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
20
+
21
+ const r1 = apply(r)
22
+ const g1 = apply(g)
23
+ const b1 = apply(b)
24
+
25
+ // Truncate at 3 digits
26
+ return Number((0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1).toFixed(3));
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './core';
2
+ export * from './parse';
3
+ export * from './functions';
4
+ export * from './transform';
package/src/parse.ts ADDED
@@ -0,0 +1,392 @@
1
+ import { Color, newColor } from './core';
2
+ import * as convert from './convert'
3
+
4
+ const HASH = '#'.charCodeAt(0);
5
+ const PERCENT = '%'.charCodeAt(0);
6
+ const G = 'g'.charCodeAt(0);
7
+ const N = 'n'.charCodeAt(0);
8
+ const D = 'd'.charCodeAt(0);
9
+ const E = 'e'.charCodeAt(0);
10
+
11
+ /**
12
+ * Approximative CSS colorspace string pattern, e.g. rgb(), color()
13
+ */
14
+ const PATTERN = (() => {
15
+ const NAME = '(\\w+)'
16
+ const SEPARATOR = '[\\s,\\/]'
17
+ const VALUE = '([^\\s,\\/]+)'
18
+ const SEPARATOR_THEN_VALUE = `(?:${SEPARATOR}+${VALUE})`
19
+
20
+ return new RegExp(
21
+ `${NAME}\\(
22
+ ${SEPARATOR}*${VALUE}
23
+ ${SEPARATOR_THEN_VALUE}
24
+ ${SEPARATOR_THEN_VALUE}
25
+ ${SEPARATOR_THEN_VALUE}?
26
+ ${SEPARATOR_THEN_VALUE}?
27
+ ${SEPARATOR}*
28
+ \\)`.replace(/\s/g, '')
29
+ )
30
+ })();
31
+
32
+
33
+ /**
34
+ * Parse CSS color
35
+ * @param color CSS color string: #xxx, #xxxxxx, #xxxxxxxx, rgb(), rgba(), hsl(), hsla(), color()
36
+ */
37
+ export function parse(color: string): Color {
38
+ if (color.charCodeAt(0) === HASH) {
39
+ return parseHex(color);
40
+ } else {
41
+ return parseColor(color);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Parse hexadecimal CSS color
47
+ * @param color Hex color string: #xxx, #xxxxxx, #xxxxxxxx
48
+ */
49
+ export function parseHex(hex: string): Color {
50
+ let r = 0x00;
51
+ let g = 0x00;
52
+ let b = 0x00;
53
+ let a = 0xff;
54
+
55
+ switch (hex.length) {
56
+ // #59f
57
+ case 4: {
58
+ r = (hexValue(hex.charCodeAt(1)) << 4) + hexValue(hex.charCodeAt(1));
59
+ g = (hexValue(hex.charCodeAt(2)) << 4) + hexValue(hex.charCodeAt(2));
60
+ b = (hexValue(hex.charCodeAt(3)) << 4) + hexValue(hex.charCodeAt(3));
61
+ break;
62
+ }
63
+ // #5599ff
64
+ case 7: {
65
+ r = (hexValue(hex.charCodeAt(1)) << 4) + hexValue(hex.charCodeAt(2));
66
+ g = (hexValue(hex.charCodeAt(3)) << 4) + hexValue(hex.charCodeAt(4));
67
+ b = (hexValue(hex.charCodeAt(5)) << 4) + hexValue(hex.charCodeAt(6));
68
+ break;
69
+ }
70
+ // #5599ff88
71
+ case 9: {
72
+ r = (hexValue(hex.charCodeAt(1)) << 4) + hexValue(hex.charCodeAt(2));
73
+ g = (hexValue(hex.charCodeAt(3)) << 4) + hexValue(hex.charCodeAt(4));
74
+ b = (hexValue(hex.charCodeAt(5)) << 4) + hexValue(hex.charCodeAt(6));
75
+ a = (hexValue(hex.charCodeAt(7)) << 4) + hexValue(hex.charCodeAt(8));
76
+ break;
77
+ }
78
+ default: {
79
+ break;
80
+ }
81
+ }
82
+
83
+ return newColor(r, g, b, a)
84
+ }
85
+
86
+ // https://lemire.me/blog/2019/04/17/parsing-short-hexadecimal-strings-efficiently/
87
+ function hexValue(c: number) {
88
+ return (c & 0xF) + 9 * (c >> 6)
89
+ }
90
+
91
+
92
+ /**
93
+ * Parse CSS color
94
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
95
+ * @param color CSS color string: rgb(), rgba(), hsl(), hsla(), color()
96
+ */
97
+ export function parseColor(color: string): Color {
98
+ const match = PATTERN.exec(color);
99
+ if (match === null) {
100
+ throw new Error(`Color.parse(): invalid CSS color: "${color}"`);
101
+ }
102
+
103
+ const format = match[1];
104
+ const p1 = match[2];
105
+ const p2 = match[3];
106
+ const p3 = match[4];
107
+ const p4 = match[5];
108
+ const p5 = match[6];
109
+
110
+ switch (format) {
111
+ case 'rgb':
112
+ case 'rgba': {
113
+ const r = parseColorChannel(p1);
114
+ const g = parseColorChannel(p2);
115
+ const b = parseColorChannel(p3);
116
+ const a = p4 ? parseAlphaChannel(p4) : 255;
117
+
118
+ return newColor(r, g, b, a);
119
+ }
120
+ case 'hsl':
121
+ case 'hsla': {
122
+ const h = parseAngle(p1);
123
+ const s = parsePercentage(p2);
124
+ const l = parsePercentage(p3);
125
+ const a = p4 ? parseAlphaChannel(p4) : 255;
126
+
127
+ // https://stackoverflow.com/a/9493060/3112706
128
+ let r, g, b;
129
+ if (s === 0) {
130
+ r = g = b = Math.round(l * 255); // achromatic
131
+ } else {
132
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
133
+ const p = 2 * l - q;
134
+ r = Math.round(hueToRGB(p, q, h + 1 / 3) * 255);
135
+ g = Math.round(hueToRGB(p, q, h) * 255);
136
+ b = Math.round(hueToRGB(p, q, h - 1 / 3) * 255);
137
+ }
138
+
139
+ return newColor(r, g, b, a);
140
+ }
141
+ case 'hwb': {
142
+ const h = parseAngle(p1);
143
+ const w = parsePercentage(p2);
144
+ const bl = parsePercentage(p3);
145
+ const a = p4 ? parseAlphaChannel(p4) : 255;
146
+
147
+ /* https://drafts.csswg.org/css-color/#hwb-to-rgb */
148
+ const s = 1.0;
149
+ const l = 0.5;
150
+
151
+ // Same as HSL to RGB
152
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
153
+ const p = 2 * l - q;
154
+ let r = Math.round(hueToRGB(p, q, h + 1 / 3) * 255);
155
+ let g = Math.round(hueToRGB(p, q, h) * 255);
156
+ let b = Math.round(hueToRGB(p, q, h - 1 / 3) * 255);
157
+
158
+ // Then HWB
159
+ r = hwbApply(r, w, bl);
160
+ g = hwbApply(g, w, bl);
161
+ b = hwbApply(b, w, bl);
162
+
163
+ return newColor(r, g, b, a);
164
+ }
165
+ case 'lab': {
166
+ const l = parsePercentageOrValue(p1);
167
+ const aa = parsePercentageOrValue(p2);
168
+ const b = parsePercentageOrValue(p3);
169
+ const a = p4 ? parseAlphaChannel(p4) : 255;
170
+ return newColorFromArray(a,
171
+ convert.xyzd50ToSrgb(...convert.labToXyzd50(l, aa, b))
172
+ )
173
+ }
174
+ case 'lch': {
175
+ const l = parsePercentageOrValue(p1);
176
+ const c = parsePercentageOrValue(p2);
177
+ const h = parsePercentageOrValue(p3);
178
+ const a = p4 ? parseAlphaChannel(p4) : 255;
179
+ return newColorFromArray(a,
180
+ convert.xyzd50ToSrgb(...convert.labToXyzd50(...convert.lchToLab(l, c, h)))
181
+ )
182
+ }
183
+ case 'oklab': {
184
+ const l = parsePercentageOrValue(p1);
185
+ const aa = parsePercentageOrValue(p2);
186
+ const b = parsePercentageOrValue(p3);
187
+ const a = p4 ? parseAlphaChannel(p4) : 255;
188
+ return newColorFromArray(a,
189
+ convert.xyzd50ToSrgb(...convert.oklchToXyzd50(l, aa, b))
190
+ )
191
+ }
192
+ case 'oklch': {
193
+ const l = parsePercentageOrValue(p1);
194
+ const c = parsePercentageOrValue(p2);
195
+ const h = parsePercentageOrValue(p3);
196
+ const a = p4 ? parseAlphaChannel(p4) : 255;
197
+ return newColorFromArray(a,
198
+ convert.xyzd50ToSrgb(...convert.oklchToXyzd50(l, c, h))
199
+ )
200
+ }
201
+ case 'color': {
202
+ // https://drafts.csswg.org/css-color-4/#color-function
203
+
204
+ const colorspace = p1;
205
+ const c1 = parsePercentageOrValue(p2);
206
+ const c2 = parsePercentageOrValue(p3);
207
+ const c3 = parsePercentageOrValue(p4);
208
+ const a = p5 ? parseAlphaChannel(p5) : 255;
209
+
210
+ switch (colorspace) {
211
+ // RGB color spaces
212
+ case 'srgb': {
213
+ return newColorFromArray(a,
214
+ [c1, c2, c3]
215
+ )
216
+ }
217
+ case 'srgb-linear': {
218
+ return newColorFromArray(a,
219
+ convert.xyzd50ToSrgb(...convert.srgbLinearToXyzd50(c1, c2, c3))
220
+ )
221
+ }
222
+ case 'display-p3': {
223
+ return newColorFromArray(a,
224
+ convert.xyzd50ToSrgb(...convert.displayP3ToXyzd50(c1, c2, c3))
225
+ )
226
+ }
227
+ case 'a98-rgb': {
228
+ return newColorFromArray(a,
229
+ convert.xyzd50ToSrgb(...convert.adobeRGBToXyzd50(c1, c2, c3))
230
+ )
231
+ }
232
+ case 'prophoto-rgb': {
233
+ return newColorFromArray(a,
234
+ convert.xyzd50ToSrgb(...convert.proPhotoToXyzd50(c1, c2, c3))
235
+ )
236
+ }
237
+ case 'rec2020': {
238
+ return newColorFromArray(a,
239
+ convert.xyzd50ToSrgb(...convert.rec2020ToXyzd50(c1, c2, c3))
240
+ )
241
+ }
242
+ // XYZ color spaces
243
+ case 'xyz':
244
+ case 'xyz-d65': {
245
+ return newColorFromArray(a,
246
+ convert.xyzd50ToSrgb(...convert.xyzd65ToD50(c1, c2, c3))
247
+ )
248
+ }
249
+ case 'xyz-d50': {
250
+ return newColorFromArray(a,
251
+ convert.xyzd50ToSrgb(c1, c2, c3)
252
+ )
253
+ }
254
+ default:
255
+ }
256
+ }
257
+ default:
258
+ }
259
+ throw new Error(`Color.parse(): invalid CSS color: "${color}"`);
260
+ }
261
+
262
+ /**
263
+ * Accepts: "50%", "128"
264
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#values
265
+ * @returns a value in the 0 to 255 range
266
+ */
267
+ function parseColorChannel(channel: string): number {
268
+ if (channel.charCodeAt(channel.length - 1) === PERCENT) {
269
+ return Math.round((parseFloat(channel) / 100) * 255);
270
+ }
271
+ return Math.round(parseFloat(channel));
272
+ }
273
+
274
+ /**
275
+ * Accepts: "50%", ".5", "0.5"
276
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/alpha-value
277
+ * @returns a value in the [0, 255] range
278
+ */
279
+ function parseAlphaChannel(channel: string): number {
280
+ return Math.round(parseAlphaValue(channel) * 255);
281
+ }
282
+
283
+ /**
284
+ * Accepts: "50%", ".5", "0.5"
285
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/alpha-value
286
+ * @returns a value in the [0, 1] range
287
+ */
288
+ function parseAlphaValue(channel: string): number {
289
+ if (channel.charCodeAt(0) === N) {
290
+ return 0;
291
+ }
292
+ if (channel.charCodeAt(channel.length - 1) === PERCENT) {
293
+ return parseFloat(channel) / 100;
294
+ }
295
+ return parseFloat(channel);
296
+ }
297
+
298
+ /**
299
+ * Accepts: "360", "360deg", "400grad", "6.28rad", "1turn", "none"
300
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/angle
301
+ * @returns a value in the 0.0 to 1.0 range
302
+ */
303
+ function parseAngle(angle: string): number {
304
+ let factor = 1;
305
+ switch (angle.charCodeAt(angle.length - 1)) {
306
+ case E: {
307
+ // 'none'
308
+ return 0;
309
+ }
310
+ case D: {
311
+ // 'rad', 'grad'
312
+ if (angle.charCodeAt(Math.max(0, angle.length - 4)) === G) {
313
+ // 'grad'
314
+ factor = 400;
315
+ } else {
316
+ // 'rad'
317
+ factor = 2 * Math.PI; // TAU
318
+ }
319
+ break;
320
+ }
321
+ case N: {
322
+ // 'turn'
323
+ factor = 1;
324
+ break;
325
+ }
326
+ // case G: // 'deg', but no need to check as it's also the default
327
+ default: {
328
+ factor = 360;
329
+ }
330
+ }
331
+ return parseFloat(angle) / factor;
332
+ }
333
+
334
+ /**
335
+ * Accepts: "100%", "none"
336
+ * @returns a value in the 0.0 to 1.0 range
337
+ */
338
+ function parsePercentage(value: string): number {
339
+ if (value.charCodeAt(0) === N) {
340
+ return 0;
341
+ }
342
+ return parseFloat(value) / 100;
343
+ }
344
+
345
+ /**
346
+ * Accepts: "1.0", "100%", "none"
347
+ * @returns a value in the 0.0 to 1.0 range
348
+ */
349
+ function parsePercentageOrValue(value: string): number {
350
+ if (value.charCodeAt(0) === N) {
351
+ return 0;
352
+ }
353
+ if (value.charCodeAt(value.length - 1) === PERCENT) {
354
+ return parseFloat(value) / 100;
355
+ }
356
+ return parseFloat(value);
357
+ }
358
+
359
+
360
+ // HSL functions
361
+
362
+ function hueToRGB(p: number, q: number, t: number) {
363
+ if (t < 0) { t += 1 };
364
+ if (t > 1) { t -= 1 };
365
+ if (t < 1 / 6) { return p + (q - p) * 6 * t };
366
+ if (t < 1 / 2) { return q };
367
+ if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6 };
368
+ { return p };
369
+ }
370
+
371
+ // HWB functions
372
+
373
+ function hwbApply(channel: number, w: number, b: number) {
374
+ let result = channel / 255
375
+
376
+ result *= 1 - w - b
377
+ result += w
378
+
379
+ return Math.round(result * 255)
380
+ }
381
+
382
+
383
+ function clamp(value: number) {
384
+ return Math.max(0, Math.min(255, value))
385
+ }
386
+
387
+ function newColorFromArray(a: number, rgb: [number, number, number]) {
388
+ const r = clamp(Math.round(rgb[0] * 255))
389
+ const g = clamp(Math.round(rgb[1] * 255))
390
+ const b = clamp(Math.round(rgb[2] * 255))
391
+ return newColor(r, g, b, a)
392
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ Color,
3
+ getRed,
4
+ getGreen,
5
+ getBlue,
6
+ getAlpha,
7
+ setAlpha,
8
+ newColor,
9
+ } from './core';
10
+
11
+ /**
12
+ * Modifies color alpha channel.
13
+ * @param color - Color
14
+ * @param value - Value in the range [0, 1]
15
+ */
16
+ export function alpha(color: Color, value: number): Color {
17
+ return setAlpha(color, Math.round(value * 255))
18
+ }
19
+
20
+ /**
21
+ * Darkens a color.
22
+ * @param color - Color
23
+ * @param coefficient - Multiplier in the range [0, 1]
24
+ */
25
+ export function darken(color: Color, coefficient: number): Color {
26
+ const r = getRed(color);
27
+ const g = getGreen(color);
28
+ const b = getBlue(color);
29
+ const a = getAlpha(color);
30
+
31
+ const factor = 1 - coefficient;
32
+
33
+ return newColor(
34
+ r * factor,
35
+ g * factor,
36
+ b * factor,
37
+ a,
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Lighten a color.
43
+ * @param color - Color
44
+ * @param coefficient - Multiplier in the range [0, 1]
45
+ */
46
+ export function lighten(color: Color, coefficient: number): Color {
47
+ const r = getRed(color);
48
+ const g = getGreen(color);
49
+ const b = getBlue(color);
50
+ const a = getAlpha(color);
51
+
52
+ return newColor(
53
+ r + (255 - r) * coefficient,
54
+ g + (255 - g) * coefficient,
55
+ b + (255 - b) * coefficient,
56
+ a,
57
+ )
58
+ }
@@ -0,0 +1,7 @@
1
+ import { from } from './core'
2
+ import { parse } from './parse'
3
+ import * as Transform from './transform'
4
+
5
+ export function alpha(color: string, value: number) { return from(Transform.alpha(parse(color), value)) }
6
+ export function darken(color: string, value: number) { return from(Transform.darken(parse(color), value)) }
7
+ export function lighten(color: string, value: number) { return from(Transform.lighten(parse(color), value)) }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ES2022",
4
+ "lib": ["ES2017"],
5
+ "strict": true,
6
+ "noImplicitAny": true,
7
+ "esModuleInterop": true,
8
+ "preserveConstEnums": true,
9
+ "outDir": "./build",
10
+ "sourceMap": true
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["**/*.test.ts"]
14
+ }