1 | const colorString = require('color-string');
|
2 | const convert = require('color-convert');
|
3 |
|
4 | const skippedModels = [
|
5 |
|
6 | 'keyword',
|
7 |
|
8 |
|
9 | 'gray',
|
10 |
|
11 |
|
12 | 'hex',
|
13 | ];
|
14 |
|
15 | const hashedModelKeys = {};
|
16 | for (const model of Object.keys(convert)) {
|
17 | hashedModelKeys[[...convert[model].labels].sort().join('')] = model;
|
18 | }
|
19 |
|
20 | const limiters = {};
|
21 |
|
22 | function Color(object, model) {
|
23 | if (!(this instanceof Color)) {
|
24 | return new Color(object, model);
|
25 | }
|
26 |
|
27 | if (model && model in skippedModels) {
|
28 | model = null;
|
29 | }
|
30 |
|
31 | if (model && !(model in convert)) {
|
32 | throw new Error('Unknown model: ' + model);
|
33 | }
|
34 |
|
35 | let i;
|
36 | let channels;
|
37 |
|
38 | if (object == null) {
|
39 | this.model = 'rgb';
|
40 | this.color = [0, 0, 0];
|
41 | this.valpha = 1;
|
42 | } else if (object instanceof Color) {
|
43 | this.model = object.model;
|
44 | this.color = [...object.color];
|
45 | this.valpha = object.valpha;
|
46 | } else if (typeof object === 'string') {
|
47 | const result = colorString.get(object);
|
48 | if (result === null) {
|
49 | throw new Error('Unable to parse color from string: ' + object);
|
50 | }
|
51 |
|
52 | this.model = result.model;
|
53 | channels = convert[this.model].channels;
|
54 | this.color = result.value.slice(0, channels);
|
55 | this.valpha = typeof result.value[channels] === 'number' ? result.value[channels] : 1;
|
56 | } else if (object.length > 0) {
|
57 | this.model = model || 'rgb';
|
58 | channels = convert[this.model].channels;
|
59 | const newArray = Array.prototype.slice.call(object, 0, channels);
|
60 | this.color = zeroArray(newArray, channels);
|
61 | this.valpha = typeof object[channels] === 'number' ? object[channels] : 1;
|
62 | } else if (typeof object === 'number') {
|
63 |
|
64 | this.model = 'rgb';
|
65 | this.color = [
|
66 | (object >> 16) & 0xFF,
|
67 | (object >> 8) & 0xFF,
|
68 | object & 0xFF,
|
69 | ];
|
70 | this.valpha = 1;
|
71 | } else {
|
72 | this.valpha = 1;
|
73 |
|
74 | const keys = Object.keys(object);
|
75 | if ('alpha' in object) {
|
76 | keys.splice(keys.indexOf('alpha'), 1);
|
77 | this.valpha = typeof object.alpha === 'number' ? object.alpha : 0;
|
78 | }
|
79 |
|
80 | const hashedKeys = keys.sort().join('');
|
81 | if (!(hashedKeys in hashedModelKeys)) {
|
82 | throw new Error('Unable to parse color from object: ' + JSON.stringify(object));
|
83 | }
|
84 |
|
85 | this.model = hashedModelKeys[hashedKeys];
|
86 |
|
87 | const {labels} = convert[this.model];
|
88 | const color = [];
|
89 | for (i = 0; i < labels.length; i++) {
|
90 | color.push(object[labels[i]]);
|
91 | }
|
92 |
|
93 | this.color = zeroArray(color);
|
94 | }
|
95 |
|
96 |
|
97 | if (limiters[this.model]) {
|
98 | channels = convert[this.model].channels;
|
99 | for (i = 0; i < channels; i++) {
|
100 | const limit = limiters[this.model][i];
|
101 | if (limit) {
|
102 | this.color[i] = limit(this.color[i]);
|
103 | }
|
104 | }
|
105 | }
|
106 |
|
107 | this.valpha = Math.max(0, Math.min(1, this.valpha));
|
108 |
|
109 | if (Object.freeze) {
|
110 | Object.freeze(this);
|
111 | }
|
112 | }
|
113 |
|
114 | Color.prototype = {
|
115 | toString() {
|
116 | return this.string();
|
117 | },
|
118 |
|
119 | toJSON() {
|
120 | return this[this.model]();
|
121 | },
|
122 |
|
123 | string(places) {
|
124 | let self = this.model in colorString.to ? this : this.rgb();
|
125 | self = self.round(typeof places === 'number' ? places : 1);
|
126 | const args = self.valpha === 1 ? self.color : [...self.color, this.valpha];
|
127 | return colorString.to[self.model](args);
|
128 | },
|
129 |
|
130 | percentString(places) {
|
131 | const self = this.rgb().round(typeof places === 'number' ? places : 1);
|
132 | const args = self.valpha === 1 ? self.color : [...self.color, this.valpha];
|
133 | return colorString.to.rgb.percent(args);
|
134 | },
|
135 |
|
136 | array() {
|
137 | return this.valpha === 1 ? [...this.color] : [...this.color, this.valpha];
|
138 | },
|
139 |
|
140 | object() {
|
141 | const result = {};
|
142 | const {channels} = convert[this.model];
|
143 | const {labels} = convert[this.model];
|
144 |
|
145 | for (let i = 0; i < channels; i++) {
|
146 | result[labels[i]] = this.color[i];
|
147 | }
|
148 |
|
149 | if (this.valpha !== 1) {
|
150 | result.alpha = this.valpha;
|
151 | }
|
152 |
|
153 | return result;
|
154 | },
|
155 |
|
156 | unitArray() {
|
157 | const rgb = this.rgb().color;
|
158 | rgb[0] /= 255;
|
159 | rgb[1] /= 255;
|
160 | rgb[2] /= 255;
|
161 |
|
162 | if (this.valpha !== 1) {
|
163 | rgb.push(this.valpha);
|
164 | }
|
165 |
|
166 | return rgb;
|
167 | },
|
168 |
|
169 | unitObject() {
|
170 | const rgb = this.rgb().object();
|
171 | rgb.r /= 255;
|
172 | rgb.g /= 255;
|
173 | rgb.b /= 255;
|
174 |
|
175 | if (this.valpha !== 1) {
|
176 | rgb.alpha = this.valpha;
|
177 | }
|
178 |
|
179 | return rgb;
|
180 | },
|
181 |
|
182 | round(places) {
|
183 | places = Math.max(places || 0, 0);
|
184 | return new Color([...this.color.map(roundToPlace(places)), this.valpha], this.model);
|
185 | },
|
186 |
|
187 | alpha(value) {
|
188 | if (value !== undefined) {
|
189 | return new Color([...this.color, Math.max(0, Math.min(1, value))], this.model);
|
190 | }
|
191 |
|
192 | return this.valpha;
|
193 | },
|
194 |
|
195 |
|
196 | red: getset('rgb', 0, maxfn(255)),
|
197 | green: getset('rgb', 1, maxfn(255)),
|
198 | blue: getset('rgb', 2, maxfn(255)),
|
199 |
|
200 | hue: getset(['hsl', 'hsv', 'hsl', 'hwb', 'hcg'], 0, value => ((value % 360) + 360) % 360),
|
201 |
|
202 | saturationl: getset('hsl', 1, maxfn(100)),
|
203 | lightness: getset('hsl', 2, maxfn(100)),
|
204 |
|
205 | saturationv: getset('hsv', 1, maxfn(100)),
|
206 | value: getset('hsv', 2, maxfn(100)),
|
207 |
|
208 | chroma: getset('hcg', 1, maxfn(100)),
|
209 | gray: getset('hcg', 2, maxfn(100)),
|
210 |
|
211 | white: getset('hwb', 1, maxfn(100)),
|
212 | wblack: getset('hwb', 2, maxfn(100)),
|
213 |
|
214 | cyan: getset('cmyk', 0, maxfn(100)),
|
215 | magenta: getset('cmyk', 1, maxfn(100)),
|
216 | yellow: getset('cmyk', 2, maxfn(100)),
|
217 | black: getset('cmyk', 3, maxfn(100)),
|
218 |
|
219 | x: getset('xyz', 0, maxfn(95.047)),
|
220 | y: getset('xyz', 1, maxfn(100)),
|
221 | z: getset('xyz', 2, maxfn(108.833)),
|
222 |
|
223 | l: getset('lab', 0, maxfn(100)),
|
224 | a: getset('lab', 1),
|
225 | b: getset('lab', 2),
|
226 |
|
227 | keyword(value) {
|
228 | if (value !== undefined) {
|
229 | return new Color(value);
|
230 | }
|
231 |
|
232 | return convert[this.model].keyword(this.color);
|
233 | },
|
234 |
|
235 | hex(value) {
|
236 | if (value !== undefined) {
|
237 | return new Color(value);
|
238 | }
|
239 |
|
240 | return colorString.to.hex(this.rgb().round().color);
|
241 | },
|
242 |
|
243 | hexa(value) {
|
244 | if (value !== undefined) {
|
245 | return new Color(value);
|
246 | }
|
247 |
|
248 | const rgbArray = this.rgb().round().color;
|
249 |
|
250 | let alphaHex = Math.round(this.valpha * 255).toString(16).toUpperCase();
|
251 | if (alphaHex.length === 1) {
|
252 | alphaHex = '0' + alphaHex;
|
253 | }
|
254 |
|
255 | return colorString.to.hex(rgbArray) + alphaHex;
|
256 | },
|
257 |
|
258 | rgbNumber() {
|
259 | const rgb = this.rgb().color;
|
260 | return ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | (rgb[2] & 0xFF);
|
261 | },
|
262 |
|
263 | luminosity() {
|
264 |
|
265 | const rgb = this.rgb().color;
|
266 |
|
267 | const lum = [];
|
268 | for (const [i, element] of rgb.entries()) {
|
269 | const chan = element / 255;
|
270 | lum[i] = (chan <= 0.04045) ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
|
271 | }
|
272 |
|
273 | return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
274 | },
|
275 |
|
276 | contrast(color2) {
|
277 |
|
278 | const lum1 = this.luminosity();
|
279 | const lum2 = color2.luminosity();
|
280 |
|
281 | if (lum1 > lum2) {
|
282 | return (lum1 + 0.05) / (lum2 + 0.05);
|
283 | }
|
284 |
|
285 | return (lum2 + 0.05) / (lum1 + 0.05);
|
286 | },
|
287 |
|
288 | level(color2) {
|
289 |
|
290 | const contrastRatio = this.contrast(color2);
|
291 | if (contrastRatio >= 7) {
|
292 | return 'AAA';
|
293 | }
|
294 |
|
295 | return (contrastRatio >= 4.5) ? 'AA' : '';
|
296 | },
|
297 |
|
298 | isDark() {
|
299 |
|
300 | const rgb = this.rgb().color;
|
301 | const yiq = (rgb[0] * 2126 + rgb[1] * 7152 + rgb[2] * 722) / 10000;
|
302 | return yiq < 128;
|
303 | },
|
304 |
|
305 | isLight() {
|
306 | return !this.isDark();
|
307 | },
|
308 |
|
309 | negate() {
|
310 | const rgb = this.rgb();
|
311 | for (let i = 0; i < 3; i++) {
|
312 | rgb.color[i] = 255 - rgb.color[i];
|
313 | }
|
314 |
|
315 | return rgb;
|
316 | },
|
317 |
|
318 | lighten(ratio) {
|
319 | const hsl = this.hsl();
|
320 | hsl.color[2] += hsl.color[2] * ratio;
|
321 | return hsl;
|
322 | },
|
323 |
|
324 | darken(ratio) {
|
325 | const hsl = this.hsl();
|
326 | hsl.color[2] -= hsl.color[2] * ratio;
|
327 | return hsl;
|
328 | },
|
329 |
|
330 | saturate(ratio) {
|
331 | const hsl = this.hsl();
|
332 | hsl.color[1] += hsl.color[1] * ratio;
|
333 | return hsl;
|
334 | },
|
335 |
|
336 | desaturate(ratio) {
|
337 | const hsl = this.hsl();
|
338 | hsl.color[1] -= hsl.color[1] * ratio;
|
339 | return hsl;
|
340 | },
|
341 |
|
342 | whiten(ratio) {
|
343 | const hwb = this.hwb();
|
344 | hwb.color[1] += hwb.color[1] * ratio;
|
345 | return hwb;
|
346 | },
|
347 |
|
348 | blacken(ratio) {
|
349 | const hwb = this.hwb();
|
350 | hwb.color[2] += hwb.color[2] * ratio;
|
351 | return hwb;
|
352 | },
|
353 |
|
354 | grayscale() {
|
355 |
|
356 | const rgb = this.rgb().color;
|
357 | const value = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;
|
358 | return Color.rgb(value, value, value);
|
359 | },
|
360 |
|
361 | fade(ratio) {
|
362 | return this.alpha(this.valpha - (this.valpha * ratio));
|
363 | },
|
364 |
|
365 | opaquer(ratio) {
|
366 | return this.alpha(this.valpha + (this.valpha * ratio));
|
367 | },
|
368 |
|
369 | rotate(degrees) {
|
370 | const hsl = this.hsl();
|
371 | let hue = hsl.color[0];
|
372 | hue = (hue + degrees) % 360;
|
373 | hue = hue < 0 ? 360 + hue : hue;
|
374 | hsl.color[0] = hue;
|
375 | return hsl;
|
376 | },
|
377 |
|
378 | mix(mixinColor, weight) {
|
379 |
|
380 |
|
381 | if (!mixinColor || !mixinColor.rgb) {
|
382 | throw new Error('Argument to "mix" was not a Color instance, but rather an instance of ' + typeof mixinColor);
|
383 | }
|
384 |
|
385 | const color1 = mixinColor.rgb();
|
386 | const color2 = this.rgb();
|
387 | const p = weight === undefined ? 0.5 : weight;
|
388 |
|
389 | const w = 2 * p - 1;
|
390 | const a = color1.alpha() - color2.alpha();
|
391 |
|
392 | const w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2;
|
393 | const w2 = 1 - w1;
|
394 |
|
395 | return Color.rgb(
|
396 | w1 * color1.red() + w2 * color2.red(),
|
397 | w1 * color1.green() + w2 * color2.green(),
|
398 | w1 * color1.blue() + w2 * color2.blue(),
|
399 | color1.alpha() * p + color2.alpha() * (1 - p));
|
400 | },
|
401 | };
|
402 |
|
403 |
|
404 | for (const model of Object.keys(convert)) {
|
405 | if (skippedModels.includes(model)) {
|
406 | continue;
|
407 | }
|
408 |
|
409 | const {channels} = convert[model];
|
410 |
|
411 |
|
412 | Color.prototype[model] = function (...args) {
|
413 | if (this.model === model) {
|
414 | return new Color(this);
|
415 | }
|
416 |
|
417 | if (args.length > 0) {
|
418 | return new Color(args, model);
|
419 | }
|
420 |
|
421 | return new Color([...assertArray(convert[this.model][model].raw(this.color)), this.valpha], model);
|
422 | };
|
423 |
|
424 |
|
425 | Color[model] = function (...args) {
|
426 | let color = args[0];
|
427 | if (typeof color === 'number') {
|
428 | color = zeroArray(args, channels);
|
429 | }
|
430 |
|
431 | return new Color(color, model);
|
432 | };
|
433 | }
|
434 |
|
435 | function roundTo(number, places) {
|
436 | return Number(number.toFixed(places));
|
437 | }
|
438 |
|
439 | function roundToPlace(places) {
|
440 | return function (number) {
|
441 | return roundTo(number, places);
|
442 | };
|
443 | }
|
444 |
|
445 | function getset(model, channel, modifier) {
|
446 | model = Array.isArray(model) ? model : [model];
|
447 |
|
448 | for (const m of model) {
|
449 | (limiters[m] || (limiters[m] = []))[channel] = modifier;
|
450 | }
|
451 |
|
452 | model = model[0];
|
453 |
|
454 | return function (value) {
|
455 | let result;
|
456 |
|
457 | if (value !== undefined) {
|
458 | if (modifier) {
|
459 | value = modifier(value);
|
460 | }
|
461 |
|
462 | result = this[model]();
|
463 | result.color[channel] = value;
|
464 | return result;
|
465 | }
|
466 |
|
467 | result = this[model]().color[channel];
|
468 | if (modifier) {
|
469 | result = modifier(result);
|
470 | }
|
471 |
|
472 | return result;
|
473 | };
|
474 | }
|
475 |
|
476 | function maxfn(max) {
|
477 | return function (v) {
|
478 | return Math.max(0, Math.min(max, v));
|
479 | };
|
480 | }
|
481 |
|
482 | function assertArray(value) {
|
483 | return Array.isArray(value) ? value : [value];
|
484 | }
|
485 |
|
486 | function zeroArray(array, length) {
|
487 | for (let i = 0; i < length; i++) {
|
488 | if (typeof array[i] !== 'number') {
|
489 | array[i] = 0;
|
490 | }
|
491 | }
|
492 |
|
493 | return array;
|
494 | }
|
495 |
|
496 | module.exports = Color;
|