UNPKG

11.2 kBJavaScriptView Raw
1const colorString = require('color-string');
2const convert = require('color-convert');
3
4const skippedModels = [
5 // To be honest, I don't really feel like keyword belongs in color convert, but eh.
6 'keyword',
7
8 // Gray conflicts with some method names, and has its own method defined.
9 'gray',
10
11 // Shouldn't really be in color-convert either...
12 'hex',
13];
14
15const hashedModelKeys = {};
16for (const model of Object.keys(convert)) {
17 hashedModelKeys[[...convert[model].labels].sort().join('')] = model;
18}
19
20const limiters = {};
21
22function 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) { // eslint-disable-line no-eq-null,eqeqeq
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 // This is always RGB - can be converted later on.
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 // Perform limitations (clamping, etc.)
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
114Color.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 // Rgb
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 // http://www.w3.org/TR/WCAG20/#relativeluminancedef
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 // http://www.w3.org/TR/WCAG20/#contrast-ratiodef
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 // https://www.w3.org/TR/WCAG/#contrast-enhanced
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 // YIQ equation from http://24ways.org/2010/calculating-color-contrast
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 // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
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 // Ported from sass implementation in C
380 // https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209
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// Model conversion methods and static constructors
404for (const model of Object.keys(convert)) {
405 if (skippedModels.includes(model)) {
406 continue;
407 }
408
409 const {channels} = convert[model];
410
411 // Conversion methods
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 // 'static' construction methods
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
435function roundTo(number, places) {
436 return Number(number.toFixed(places));
437}
438
439function roundToPlace(places) {
440 return function (number) {
441 return roundTo(number, places);
442 };
443}
444
445function 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
476function maxfn(max) {
477 return function (v) {
478 return Math.max(0, Math.min(max, v));
479 };
480}
481
482function assertArray(value) {
483 return Array.isArray(value) ? value : [value];
484}
485
486function 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
496module.exports = Color;