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 +3 -0
- package/babel.config.json +6 -0
- package/jest.config.js +9 -0
- package/package.json +29 -0
- package/src/bit.ts +27 -0
- package/src/color.test.ts +66 -0
- package/src/convert.ts +392 -0
- package/src/core.ts +58 -0
- package/src/functions.ts +27 -0
- package/src/index.ts +4 -0
- package/src/parse.ts +392 -0
- package/src/transform.ts +58 -0
- package/src/transformString.ts +7 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
package/jest.config.js
ADDED
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
|
+
}
|
package/src/functions.ts
ADDED
|
@@ -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
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
|
+
}
|
package/src/transform.ts
ADDED
|
@@ -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
|
+
}
|