UNPKG

4.97 kBJavaScriptView Raw
1'use strict';
2
3// these algorithms are sourced from https://drafts.csswg.org/css-color/#color-conversion-code
4
5/**
6 * @param {number[]} RGB
7 */
8function lin_sRGB(RGB) {
9 // convert an array of sRGB values in the range 0.0 - 1.0
10 // to linear light (un-companded) form.
11 // https://en.wikipedia.org/wiki/SRGB
12 return RGB.map((val) => {
13 if (val < 0.04045) {
14 return val / 12.92;
15 }
16
17 return ((val + 0.055) / 1.055) ** 2.4;
18 });
19}
20
21/**
22 * @param {number[][]} matrix
23 * @param {number[]} vector
24 */
25function matrixMultiple3d(matrix, vector) {
26 return [
27 matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
28 matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
29 matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2],
30 ];
31}
32
33/**
34 * @param {number[]} srgb
35 */
36function srgb2xyz(srgb) {
37 return matrixMultiple3d(
38 [
39 [0.4124564, 0.3575761, 0.1804375],
40 [0.2126729, 0.7151522, 0.072175],
41 [0.0193339, 0.119192, 0.9503041],
42 ],
43 srgb,
44 );
45}
46
47/**
48 * @param {number[]} xyz
49 */
50function chromaticAdaptationD65_D50(xyz) {
51 return matrixMultiple3d(
52 [
53 [1.0478112, 0.0228866, -0.050127],
54 [0.0295424, 0.9904844, -0.0170491],
55 [-0.0092345, 0.0150436, 0.7521316],
56 ],
57 xyz,
58 );
59}
60
61/**
62 * @param {number[]} xyzIn
63 */
64function xyz2lab(xyzIn) {
65 // Assuming XYZ is relative to D50, convert to CIE Lab
66 // from CIE standard, which now defines these as a rational fraction
67 const ε = 216 / 24389; // 6^3/29^3
68 const κ = 24389 / 27; // 29^3/3^3
69 const white = [0.9642, 1.0, 0.8249]; // D50 reference white
70
71 // compute xyz, which is XYZ scaled relative to reference white
72 const xyz = xyzIn.map((value, i) => value / white[i]);
73
74 // now compute f
75 const f = xyz.map((value) => {
76 if (value > ε) {
77 return Math.cbrt(value);
78 }
79
80 return (κ * value + 16) / 116;
81 });
82
83 return [
84 116 * f[1] - 16, // L
85 500 * (f[0] - f[1]), // a
86 200 * (f[1] - f[2]), // b
87 ];
88}
89
90/**
91 * @param {number} r
92 * @param {number} g
93 * @param {number} b
94 */
95function rgb2hsl(r, g, b) {
96 r /= 255;
97 g /= 255;
98 b /= 255;
99 let h;
100 let s;
101 let l;
102 const M = Math.max(r, g, b);
103 const m = Math.min(r, g, b);
104 const d = M - m;
105
106 if (d === 0) {
107 h = 0;
108 } else if (M === r) {
109 h = ((g - b) / d) % 6;
110 } else if (M === g) {
111 h = (b - r) / d + 2;
112 } else {
113 h = (r - g) / d + 4;
114 }
115
116 h *= 60;
117
118 if (h < 0) {
119 h += 360;
120 }
121
122 l = (M + m) / 2;
123
124 s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
125
126 s *= 100;
127 l *= 100;
128
129 return [Math.round(h), Math.round(s), Math.round(l)];
130}
131
132/**
133 * @param {number} rgb_r
134 * @param {number} rgb_g
135 * @param {number} rgb_b
136 */
137function rgb2hwb(rgb_r, rgb_g, rgb_b) {
138 rgb_r /= 255;
139 rgb_g /= 255;
140 rgb_b /= 255;
141
142 const w = Math.min(rgb_r, rgb_g, rgb_b);
143 const v = Math.max(rgb_r, rgb_g, rgb_b);
144
145 const b = 1 - v;
146
147 if (v === w) {
148 return [0, Math.round(w * 100), Math.round(b * 100)];
149 }
150
151 const f = rgb_r === w ? rgb_g - rgb_b : rgb_g === w ? rgb_b - rgb_r : rgb_r - rgb_g;
152 const i = rgb_r === w ? 3 : rgb_g === w ? 5 : 1;
153
154 return [
155 Math.round(((i - f / (v - w)) / 6) * 360) % 360,
156 Math.round(w * 100),
157 Math.round(b * 100),
158 ];
159}
160
161/**
162 * @param {number} value
163 */
164function perc255(value) {
165 return `${Math.round((value * 100) / 255)}%`;
166}
167
168/**
169 * @param {string} hexString
170 * @returns {string[]}
171 */
172function generateColorFuncs(hexString) {
173 if (hexString.length !== 7) {
174 throw new Error(
175 `Invalid hex string color definition (${hexString}) - expected 6 character hex string`,
176 );
177 }
178
179 const rgb = [0, 0, 0];
180
181 for (let i = 0; i < 3; i += 1) {
182 rgb[i] = Number.parseInt(hexString.substr(2 * i + 1, 2), 16);
183 }
184
185 const hsl = rgb2hsl(rgb[0], rgb[1], rgb[2]);
186 const hwb = rgb2hwb(rgb[0], rgb[1], rgb[2]);
187 const func = [];
188 const rgbStr = `${rgb[0]},${rgb[1]},${rgb[2]}`;
189 const rgbPercStr = `${perc255(rgb[0])},${perc255(rgb[1])},${perc255(rgb[2])}`;
190 const hslStr = `${hsl[0]},${hsl[1]}%,${hsl[2]}%`;
191 const hwbStr = `${hwb[0]},${hwb[1]}%,${hwb[2]}%`;
192
193 // *very* convoluted process, just to be able to establish if the color
194 // is gray -- or not.
195 const linRgb = lin_sRGB([rgb[0] / 255, rgb[1] / 255, rgb[2] / 255]);
196 const xyz_d65 = srgb2xyz(linRgb);
197 const xyz_d50 = chromaticAdaptationD65_D50(xyz_d65);
198 const lab = xyz2lab(xyz_d50);
199
200 func.push(
201 `rgb(${rgbStr})`,
202 `rgba(${rgbStr},1)`,
203 `rgba(${rgbStr},100%)`,
204 `rgb(${rgbPercStr})`,
205 `rgba(${rgbPercStr},1)`,
206 `rgba(${rgbPercStr},100%)`,
207 `hsl(${hslStr})`,
208 `hsla(${hslStr},1)`,
209 `hsla(${hslStr},100%)`,
210 `hwb(${hwbStr})`,
211 `hwb(${hwbStr},1)`,
212 `hwb(${hwbStr},100%)`,
213 );
214
215 // technically, this should be 0 - but then #808080 wouldn't even be gray
216 if (lab[1] * lab[1] < 0.01 && lab[2] * lab[2] < 0.01) {
217 // yay! gray!
218 const grayStr = Math.round(lab[0]);
219
220 func.push(
221 `gray(${grayStr})`,
222 `gray(${grayStr},1)`,
223 `gray(${grayStr},100%)`,
224 `gray(${grayStr}%)`,
225 `gray(${grayStr}%,1)`,
226 `gray(${grayStr}%,100%)`,
227 );
228 }
229
230 return func;
231}
232
233module.exports = generateColorFuncs;