1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | const chroma = require('chroma-js');
|
14 | const { catmullRom2bezier, prepareCurve } = require('./curve');
|
15 |
|
16 | const colorSpaces = {
|
17 | CAM02: 'jab',
|
18 | CAM02p: 'jch',
|
19 | HEX: 'hex',
|
20 | HSL: 'hsl',
|
21 | HSLuv: 'hsluv',
|
22 | HSV: 'hsv',
|
23 | LAB: 'lab',
|
24 | LCH: 'lch',
|
25 | RGB: 'rgb',
|
26 | };
|
27 |
|
28 | function round(x, n = 0) {
|
29 | const ten = 10 ** n;
|
30 | return Math.round(x * ten) / ten;
|
31 | }
|
32 |
|
33 | function multiplyRatios(ratio, multiplier) {
|
34 | let r;
|
35 |
|
36 |
|
37 |
|
38 | if (ratio > 1) {
|
39 | r = (ratio - 1) * multiplier + 1;
|
40 | } else if (ratio < -1) {
|
41 | r = (ratio + 1) * multiplier - 1;
|
42 | } else {
|
43 | r = 1;
|
44 | }
|
45 |
|
46 | return round(r, 2);
|
47 | }
|
48 |
|
49 | function cArray(c) {
|
50 | return chroma(String(c)).hsluv();
|
51 | }
|
52 |
|
53 | function smoothScale(ColorsArray, domains, space) {
|
54 | const points = [[], [], []];
|
55 | ColorsArray.forEach((color, i) => points.forEach((point, j) => point.push(domains[i], color[j])));
|
56 | if (space === 'hcl') {
|
57 | const point = points[1];
|
58 | for (let i = 1; i < point.length; i += 2) {
|
59 | if (Number.isNaN(point[i])) {
|
60 | point[i] = 0;
|
61 | }
|
62 | }
|
63 | }
|
64 | points.forEach((point) => {
|
65 | const nans = [];
|
66 |
|
67 | for (let i = 1; i < point.length; i += 2) {
|
68 | if (Number.isNaN(point[i])) {
|
69 | nans.push(i);
|
70 | } else {
|
71 | nans.forEach((j) => { point[j] = point[i]; });
|
72 | nans.length = 0;
|
73 | break;
|
74 | }
|
75 | }
|
76 |
|
77 | if (nans.length) {
|
78 |
|
79 | const safeJChHue = chroma('#ccc').jch()[2];
|
80 | nans.forEach((j) => { point[j] = safeJChHue; });
|
81 | }
|
82 | nans.length = 0;
|
83 |
|
84 | for (let i = point.length - 1; i > 0; i -= 2) {
|
85 | if (Number.isNaN(point[i])) {
|
86 | nans.push(i);
|
87 | } else {
|
88 | nans.forEach((j) => { point[j] = point[i]; });
|
89 | break;
|
90 | }
|
91 | }
|
92 |
|
93 | for (let i = 1; i < point.length; i += 2) {
|
94 | if (Number.isNaN(point[i])) {
|
95 | point.splice(i - 1, 2);
|
96 | i -= 2;
|
97 | }
|
98 | }
|
99 |
|
100 | if (space in { hcl: 1, hsl: 1, hsluv: 1, hsv: 1, jch: 1 }) {
|
101 | let prev = point[1];
|
102 | let addon = 0;
|
103 | for (let i = 3; i < point.length; i += 2) {
|
104 | const p = point[i] + addon;
|
105 | const zero = Math.abs(prev - p);
|
106 | const plus = Math.abs(prev - (p + 360));
|
107 | const minus = Math.abs(prev - (p - 360));
|
108 | if (plus < zero && plus < minus) {
|
109 | addon += 360;
|
110 | }
|
111 | if (minus < zero && minus < plus) {
|
112 | addon -= 360;
|
113 | }
|
114 | point[i] += addon;
|
115 | prev = point[i];
|
116 | }
|
117 | }
|
118 | });
|
119 | const prep = points.map((point) => catmullRom2bezier(point).map((curve) => prepareCurve(...curve)));
|
120 | return (d) => {
|
121 | const ch = prep.map((p) => {
|
122 | for (let i = 0; i < p.length; i++) {
|
123 | const res = p[i](d);
|
124 | if (res != null) {
|
125 | return res;
|
126 | }
|
127 | }
|
128 | return null;
|
129 | });
|
130 |
|
131 | if (space === 'jch' && ch[1] < 0) {
|
132 | ch[1] = 0;
|
133 | }
|
134 |
|
135 | return chroma[space](...ch).hex();
|
136 | };
|
137 | }
|
138 |
|
139 | function makePowScale(exp = 1, domains = [0, 1], range = [0, 1]) {
|
140 | const m = (range[1] - range[0]) / (domains[1] ** exp - domains[0] ** exp);
|
141 | const c = range[0] - m * domains[0] ** exp;
|
142 | return (x) => m * x ** exp + c;
|
143 | }
|
144 |
|
145 | function createScale({
|
146 | swatches,
|
147 | colorKeys,
|
148 | colorspace = 'LAB',
|
149 | shift = 1,
|
150 | fullScale = true,
|
151 | smooth = false,
|
152 | asFun = false,
|
153 | } = {}) {
|
154 | const space = colorSpaces[colorspace];
|
155 | if (!space) {
|
156 | throw new Error(`Colorspace “${colorspace}” not supported`);
|
157 | }
|
158 | if (!colorKeys) {
|
159 | throw new Error(`Colorkeys missing: returned “${colorKeys}”`);
|
160 | }
|
161 |
|
162 | let domains = colorKeys
|
163 | .map((key) => swatches - swatches * (chroma(key).hsluv()[2] / 100))
|
164 | .sort((a, b) => a - b)
|
165 | .concat(swatches);
|
166 |
|
167 | domains.unshift(0);
|
168 |
|
169 |
|
170 | let sqrtDomains = makePowScale(shift, [1, swatches], [1, swatches]);
|
171 | sqrtDomains = domains.map((d) => Math.max(0, sqrtDomains(d)));
|
172 |
|
173 |
|
174 | domains = sqrtDomains;
|
175 |
|
176 | const sortedColor = colorKeys
|
177 |
|
178 | .map((c, i) => ({ colorKeys: cArray(c), index: i }))
|
179 |
|
180 | .sort((c1, c2) => c2.colorKeys[2] - c1.colorKeys[2])
|
181 |
|
182 | .map((data) => colorKeys[data.index]);
|
183 |
|
184 | let ColorsArray = [];
|
185 |
|
186 | let scale;
|
187 | if (fullScale) {
|
188 | const white = space === 'lch' ? chroma.lch(...chroma('#fff').lch()) : '#fff';
|
189 | const black = space === 'lch' ? chroma.lch(...chroma('#000').lch()) : '#000';
|
190 | ColorsArray = [
|
191 | white,
|
192 | ...sortedColor,
|
193 | black,
|
194 | ];
|
195 | } else {
|
196 | ColorsArray = sortedColor;
|
197 | }
|
198 |
|
199 | if (smooth) {
|
200 | const stringColors = ColorsArray;
|
201 | ColorsArray = ColorsArray.map((d) => chroma(String(d))[space]());
|
202 | if (space === 'hcl') {
|
203 |
|
204 | ColorsArray.forEach((c) => { c[1] = Number.isNaN(c[1]) ? 0 : c[1]; });
|
205 | }
|
206 | if (space === 'jch') {
|
207 |
|
208 |
|
209 | for (let i = 0; i < stringColors.length; i++) {
|
210 | const color = chroma(stringColors[i]).hcl();
|
211 | if (Number.isNaN(color[0])) {
|
212 | ColorsArray[i][2] = NaN;
|
213 | }
|
214 | }
|
215 | }
|
216 | scale = smoothScale(ColorsArray, domains, space);
|
217 | } else {
|
218 | scale = chroma.scale(ColorsArray.map((color) => {
|
219 | if (typeof color === 'object' && color.constructor === chroma.Color) {
|
220 | return color;
|
221 | }
|
222 | return String(color);
|
223 | })).domain(domains).mode(space);
|
224 | }
|
225 | if (asFun) {
|
226 | return scale;
|
227 | }
|
228 |
|
229 | const Colors = new Array(swatches).fill().map((_, d) => chroma(scale(d)).hex());
|
230 |
|
231 | const colors = Colors.filter((el) => el != null);
|
232 |
|
233 | return colors;
|
234 | }
|
235 |
|
236 | function removeDuplicates(originalArray, prop) {
|
237 | const newArray = [];
|
238 | const lookupObject = {};
|
239 | const keys1 = Object.keys(originalArray);
|
240 |
|
241 | keys1.forEach((i) => { lookupObject[originalArray[i][prop]] = originalArray[i]; });
|
242 |
|
243 | const keys2 = Object.keys(lookupObject);
|
244 | keys2.forEach((i) => newArray.push(lookupObject[i]));
|
245 | return newArray;
|
246 | }
|
247 |
|
248 | function uniq(a) {
|
249 | return Array.from(new Set(a));
|
250 | }
|
251 |
|
252 |
|
253 | function filterNaN(x) {
|
254 | if (Number.isNaN(x)) {
|
255 | return 0;
|
256 | }
|
257 | return x;
|
258 | }
|
259 |
|
260 |
|
261 | function convertColorValue(color, format, object = false) {
|
262 | if (!color) {
|
263 | throw new Error(`Cannot convert color value of “${color}”`);
|
264 | }
|
265 | if (!colorSpaces[format]) {
|
266 | throw new Error(`Cannot convert to colorspace “${format}”`);
|
267 | }
|
268 | const space = colorSpaces[format];
|
269 | const colorObj = chroma(String(color))[space]();
|
270 | if (format === 'HSL') {
|
271 | colorObj.pop();
|
272 | }
|
273 | if (format === 'HEX') {
|
274 | if (object) {
|
275 | const rgb = chroma(String(color)).rgb();
|
276 | return { r: rgb[0], g: rgb[1], b: rgb[2] };
|
277 | }
|
278 | return colorObj;
|
279 | }
|
280 |
|
281 | const colorObject = {};
|
282 | let newColorObj = colorObj.map(filterNaN);
|
283 |
|
284 | newColorObj = newColorObj.map((ch, i) => {
|
285 | let rnd = round(ch);
|
286 | let j = i;
|
287 | if (space === 'hsluv') {
|
288 | j += 2;
|
289 | }
|
290 | let letter = space.charAt(j);
|
291 | if (space === 'jch' && letter === 'c') {
|
292 | letter = 'C';
|
293 | }
|
294 | colorObject[letter === 'j' ? 'J' : letter] = rnd;
|
295 | if (space in { lab: 1, lch: 1, jab: 1, jch: 1 }) {
|
296 | if (!object) {
|
297 | if (letter === 'l' || letter === 'j') {
|
298 | rnd += '%';
|
299 | }
|
300 | if (letter === 'h') {
|
301 | rnd += 'deg';
|
302 | }
|
303 | }
|
304 | } else if (space !== 'hsluv') {
|
305 | if (letter === 's' || letter === 'l' || letter === 'v') {
|
306 | colorObject[letter] = round(ch, 2);
|
307 | if (!object) {
|
308 | rnd = round(ch * 100);
|
309 | rnd += '%';
|
310 | }
|
311 | } else if (letter === 'h' && !object) {
|
312 | rnd += 'deg';
|
313 | }
|
314 | }
|
315 | return rnd;
|
316 | });
|
317 |
|
318 | const stringName = space;
|
319 | const stringValue = `${stringName}(${newColorObj.join(', ')})`;
|
320 |
|
321 | if (object) {
|
322 | return colorObject;
|
323 | }
|
324 | return stringValue;
|
325 | }
|
326 |
|
327 | function luminance(r, g, b) {
|
328 | const a = [r, g, b].map((v) => {
|
329 | v /= 255;
|
330 | return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
331 | });
|
332 | return (a[0] * 0.2126) + (a[1] * 0.7152) + (a[2] * 0.0722);
|
333 | }
|
334 |
|
335 | function getContrast(color, base, baseV) {
|
336 | if (baseV === undefined) {
|
337 | const baseLightness = chroma.rgb(...base).hsluv()[2];
|
338 | baseV = round(baseLightness / 100, 2);
|
339 | }
|
340 |
|
341 | const colorLum = luminance(color[0], color[1], color[2]);
|
342 | const baseLum = luminance(base[0], base[1], base[2]);
|
343 |
|
344 | const cr1 = (colorLum + 0.05) / (baseLum + 0.05);
|
345 | const cr2 = (baseLum + 0.05) / (colorLum + 0.05);
|
346 |
|
347 | if (baseV < 0.5) {
|
348 |
|
349 | if (cr1 >= 1) {
|
350 | return cr1;
|
351 | }
|
352 |
|
353 | return -cr2;
|
354 | }
|
355 |
|
356 |
|
357 | if (cr1 < 1) {
|
358 | return cr2;
|
359 | }
|
360 |
|
361 | if (cr1 === 1) {
|
362 | return cr1;
|
363 | }
|
364 | return -cr1;
|
365 | }
|
366 |
|
367 | function minPositive(r) {
|
368 | if (!r) { throw new Error('Array undefined'); }
|
369 | if (!Array.isArray(r)) { throw new Error('Passed object is not an array'); }
|
370 | return Math.min(...r.filter((val) => val >= 1));
|
371 | }
|
372 |
|
373 | function ratioName(r) {
|
374 | if (!r) { throw new Error('Ratios undefined'); }
|
375 | r = r.sort((a, b) => a - b);
|
376 |
|
377 | const min = minPositive(r);
|
378 | const minIndex = r.indexOf(min);
|
379 | const nArr = [];
|
380 |
|
381 | const rNeg = r.slice(0, minIndex);
|
382 | const rPos = r.slice(minIndex, r.length);
|
383 |
|
384 |
|
385 | for (let i = 0; i < rNeg.length; i++) {
|
386 | const d = 1 / (rNeg.length + 1);
|
387 | const m = d * 100;
|
388 | const nVal = m * (i + 1);
|
389 | nArr.push(round(nVal));
|
390 | }
|
391 |
|
392 | for (let i = 0; i < rPos.length; i++) {
|
393 | nArr.push((i + 1) * 100);
|
394 | }
|
395 | nArr.sort((a, b) => a - b);
|
396 |
|
397 | return nArr;
|
398 | }
|
399 |
|
400 | const searchColors = (color, bgRgbArray, baseV, ratioValues) => {
|
401 | const colorLen = 3000;
|
402 | const colorScale = createScale({
|
403 | swatches: colorLen,
|
404 | colorKeys: color._colorKeys,
|
405 | colorspace: color._colorspace,
|
406 | shift: 1,
|
407 | smooth: color._smooth,
|
408 | asFun: true,
|
409 | });
|
410 | const ccache = {};
|
411 |
|
412 | const getContrast2 = (i) => {
|
413 | if (ccache[i]) {
|
414 | return ccache[i];
|
415 | }
|
416 | const rgb = chroma(colorScale(i)).rgb();
|
417 | const c = getContrast(rgb, bgRgbArray, baseV);
|
418 | ccache[i] = c;
|
419 |
|
420 | return c;
|
421 | };
|
422 | const colorSearch = (x) => {
|
423 | const first = getContrast2(0);
|
424 | const last = getContrast2(colorLen);
|
425 | const dir = first < last ? 1 : -1;
|
426 | const ε = 0.01;
|
427 | x += 0.005 * Math.sign(x);
|
428 | let step = colorLen / 2;
|
429 | let dot = step;
|
430 | let val = getContrast2(dot);
|
431 | let counter = 100;
|
432 | while (Math.abs(val - x) > ε && counter) {
|
433 | counter--;
|
434 | step /= 2;
|
435 | if (val < x) {
|
436 | dot += step * dir;
|
437 | } else {
|
438 | dot -= step * dir;
|
439 | }
|
440 | val = getContrast2(dot);
|
441 | }
|
442 | return round(dot, 3);
|
443 | };
|
444 | const outputColors = [];
|
445 | ratioValues.forEach((ratio) => outputColors.push(colorScale(colorSearch(+ratio))));
|
446 | return outputColors;
|
447 | };
|
448 |
|
449 | module.exports = {
|
450 | cArray,
|
451 | colorSpaces,
|
452 | convertColorValue,
|
453 | createScale,
|
454 | getContrast,
|
455 | luminance,
|
456 | minPositive,
|
457 | multiplyRatios,
|
458 | ratioName,
|
459 | removeDuplicates,
|
460 | round,
|
461 | searchColors,
|
462 | uniq,
|
463 | };
|