UNPKG

14.6 kBJavaScriptView Raw
1/*! Fast Average Color | © 2020 Denis Seleznev | MIT License | https://github.com/fast-average-color/fast-average-color */
2function isIgnoredColor(arr, num, ignoredColor) {
3 return arr[num] === ignoredColor[0] && // red
4 arr[num + 1] === ignoredColor[1] && // green
5 arr[num + 2] === ignoredColor[2] && // blue
6 arr[num + 3] === ignoredColor[3]; // alpha
7}
8
9function dominantAlgorithm(arr, len, options) {
10 const
11 colorHash = {},
12 divider = 24,
13 ignoredColor = options.ignoredColor;
14
15 for (let i = 0; i < len; i += options.step) {
16 let
17 red = arr[i],
18 green = arr[i + 1],
19 blue = arr[i + 2],
20 alpha = arr[i + 3];
21
22 if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) {
23 continue;
24 }
25
26 const key = Math.round(red / divider) + ',' +
27 Math.round(green / divider) + ',' +
28 Math.round(blue / divider);
29
30 if (colorHash[key]) {
31 colorHash[key] = [
32 colorHash[key][0] + red * alpha,
33 colorHash[key][1] + green * alpha,
34 colorHash[key][2] + blue * alpha,
35 colorHash[key][3] + alpha,
36 colorHash[key][4] + 1
37 ];
38 } else {
39 colorHash[key] = [red * alpha, green * alpha, blue * alpha, alpha, 1];
40 }
41 }
42
43 const buffer = Object.keys(colorHash).map(key => {
44 return colorHash[key];
45 }).sort((a, b) => {
46 const
47 countA = a[4],
48 countB = b[4];
49
50 return countA > countB ? -1 : countA === countB ? 0 : 1;
51 });
52
53 const max = buffer[0];
54
55 const redTotal = max[0];
56 const greenTotal = max[1];
57 const blueTotal = max[2];
58
59 const alphaTotal = max[3];
60 const count = max[4];
61
62 return alphaTotal ? [
63 Math.round(redTotal / alphaTotal),
64 Math.round(greenTotal / alphaTotal),
65 Math.round(blueTotal / alphaTotal),
66 Math.round(alphaTotal / count)
67 ] : options.defaultColor;
68}
69
70function simpleAlgorithm(arr, len, options) {
71 let
72 redTotal = 0,
73 greenTotal = 0,
74 blueTotal = 0,
75 alphaTotal = 0,
76 count = 0;
77
78 const ignoredColor = options.ignoredColor;
79
80 for (let i = 0; i < len; i += options.step) {
81 const
82 alpha = arr[i + 3],
83 red = arr[i] * alpha,
84 green = arr[i + 1] * alpha,
85 blue = arr[i + 2] * alpha;
86
87 if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) {
88 continue;
89 }
90
91 redTotal += red;
92 greenTotal += green;
93 blueTotal += blue;
94 alphaTotal += alpha;
95 count++;
96 }
97
98 return alphaTotal ? [
99 Math.round(redTotal / alphaTotal),
100 Math.round(greenTotal / alphaTotal),
101 Math.round(blueTotal / alphaTotal),
102 Math.round(alphaTotal / count)
103 ] : options.defaultColor;
104}
105
106function sqrtAlgorithm(arr, len, options) {
107 let
108 redTotal = 0,
109 greenTotal = 0,
110 blueTotal = 0,
111 alphaTotal = 0,
112 count = 0;
113
114 const ignoredColor = options.ignoredColor;
115
116 for (let i = 0; i < len; i += options.step) {
117 const
118 red = arr[i],
119 green = arr[i + 1],
120 blue = arr[i + 2],
121 alpha = arr[i + 3];
122
123 if (ignoredColor && isIgnoredColor(arr, i, options)) {
124 continue;
125 }
126
127 redTotal += red * red * alpha;
128 greenTotal += green * green * alpha;
129 blueTotal += blue * blue * alpha;
130 alphaTotal += alpha;
131 count++;
132 }
133
134 return alphaTotal ? [
135 Math.round(Math.sqrt(redTotal / alphaTotal)),
136 Math.round(Math.sqrt(greenTotal / alphaTotal)),
137 Math.round(Math.sqrt(blueTotal / alphaTotal)),
138 Math.round(alphaTotal / count)
139 ] : options.defaultColor;
140}
141
142const ERROR_PREFIX = 'FastAverageColor: ';
143
144class FastAverageColor {
145 /**
146 * Get asynchronously the average color from not loaded image.
147 *
148 * @param {HTMLImageElement | string | null} resource
149 * @param {Object} [options]
150 * @param {Array} [options.defaultColor=[0, 0, 0, 0]] [red, green, blue, alpha]
151 * @param {Array} [options.ignoredColor] [red, green, blue, alpha]
152 * @param {string} [options.mode="speed"] "precision" or "speed"
153 * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant"
154 * @param {number} [options.step=1]
155 * @param {number} [options.left=0]
156 * @param {number} [options.top=0]
157 * @param {number} [options.width=width of resource]
158 * @param {number} [options.height=height of resource]
159 * @param {boolean} [options.silent] Disable error output via console.error
160 *
161 * @returns {Promise}
162 */
163 getColorAsync(resource, options) {
164 if (!resource) {
165 return Promise.reject(Error(`${ERROR_PREFIX}call .getColorAsync() without resource.`));
166 } else if (typeof resource === 'string') {
167 return this._bindImageEvents(new Image(resource), options);
168 } else if (resource.complete) {
169 const result = this.getColor(resource, options);
170 return result.error ? Promise.reject(result.error) : Promise.resolve(result);
171 } else {
172 return this._bindImageEvents(resource, options);
173 }
174 }
175
176 /**
177 * Get the average color from images, videos and canvas.
178 *
179 * @param {HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | null} resource
180 * @param {Object} [options]
181 * @param {Array} [options.defaultColor=[0, 0, 0, 0]] [red, green, blue, alpha]
182 * @param {Array} [options.ignoredColor] [red, green, blue, alpha]
183 * @param {string} [options.mode="speed"] "precision" or "speed"
184 * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant"
185 * @param {number} [options.step=1]
186 * @param {number} [options.left=0]
187 * @param {number} [options.top=0]
188 * @param {number} [options.width=width of resource]
189 * @param {number} [options.height=height of resource]
190 * @param {boolean} [options.silent] Disable error output via console.error
191 *
192 * @returns {Object}
193 */
194 getColor(resource, options) {
195 options = options || {};
196
197 const defaultColor = this._getDefaultColor(options);
198
199 let value = defaultColor;
200 if (!resource) {
201 this._outputError(options, 'call .getColor(null) without resource.');
202
203 return this._prepareResult(defaultColor);
204 }
205
206 const
207 originalSize = this._getOriginalSize(resource),
208 size = this._prepareSizeAndPosition(originalSize, options);
209
210 if (!size.srcWidth || !size.srcHeight || !size.destWidth || !size.destHeight) {
211 this._outputError(options, `incorrect sizes for resource "${resource.src}".`);
212
213 return this._prepareResult(defaultColor);
214 }
215
216 if (!this._ctx) {
217 this._canvas = this._makeCanvas();
218 this._ctx = this._canvas.getContext && this._canvas.getContext('2d');
219
220 if (!this._ctx) {
221 this._outputError(options, 'Canvas Context 2D is not supported in this browser.');
222
223 return this._prepareResult(defaultColor);
224 }
225 }
226
227 this._canvas.width = size.destWidth;
228 this._canvas.height = size.destHeight;
229
230 try {
231 this._ctx.clearRect(0, 0, size.destWidth, size.destHeight);
232 this._ctx.drawImage(
233 resource,
234 size.srcLeft, size.srcTop,
235 size.srcWidth, size.srcHeight,
236 0, 0,
237 size.destWidth, size.destHeight
238 );
239
240 const bitmapData = this._ctx.getImageData(0, 0, size.destWidth, size.destHeight).data;
241 value = this.getColorFromArray4(bitmapData, options);
242 } catch (e) {
243 this._outputError(options, `security error (CORS) for resource ${resource.src}.\nDetails: https://developer.mozilla.org/en/docs/Web/HTML/CORS_enabled_image`, e);
244 }
245
246 return this._prepareResult(value);
247 }
248
249 /**
250 * Get the average color from a array when 1 pixel is 4 bytes.
251 *
252 * @param {Array|Uint8Array} arr
253 * @param {Object} [options]
254 * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant"
255 * @param {Array} [options.defaultColor=[0, 0, 0, 0]] [red, green, blue, alpha]
256 * @param {Array} [options.ignoredColor] [red, green, blue, alpha]
257 * @param {number} [options.step=1]
258 *
259 * @returns {Array} [red (0-255), green (0-255), blue (0-255), alpha (0-255)]
260 */
261 getColorFromArray4(arr, options) {
262 options = options || {};
263
264 const
265 bytesPerPixel = 4,
266 arrLength = arr.length,
267 defaultColor = this._getDefaultColor(options);
268
269 if (arrLength < bytesPerPixel) {
270 return defaultColor;
271 }
272
273 const
274 len = arrLength - arrLength % bytesPerPixel,
275 step = (options.step || 1) * bytesPerPixel;
276
277 let algorithm;
278
279 switch (options.algorithm || 'sqrt') {
280 case 'simple':
281 algorithm = simpleAlgorithm;
282 break;
283 case 'sqrt':
284 algorithm = sqrtAlgorithm;
285 break;
286 case 'dominant':
287 algorithm = dominantAlgorithm;
288 break;
289 default:
290 throw Error(`${ERROR_PREFIX}${options.algorithm} is unknown algorithm.`);
291 }
292
293 return algorithm(arr, len, {
294 defaultColor,
295 ignoredColor: options.ignoredColor,
296 step
297 });
298 }
299
300 /**
301 * Destroy the instance.
302 */
303 destroy() {
304 delete this._canvas;
305 delete this._ctx;
306 }
307
308 _getDefaultColor(options) {
309 return this._getOption(options, 'defaultColor', [0, 0, 0, 0]);
310 }
311
312 _getOption(options, name, defaultValue) {
313 return typeof options[name] === 'undefined' ? defaultValue : options[name];
314 }
315
316 _prepareSizeAndPosition(originalSize, options) {
317 let
318 srcLeft = this._getOption(options, 'left', 0),
319 srcTop = this._getOption(options, 'top', 0),
320 srcWidth = this._getOption(options, 'width', originalSize.width),
321 srcHeight = this._getOption(options, 'height', originalSize.height),
322 destWidth = srcWidth,
323 destHeight = srcHeight;
324
325 if (options.mode === 'precision') {
326 return {
327 srcLeft,
328 srcTop,
329 srcWidth,
330 srcHeight,
331 destWidth,
332 destHeight
333 };
334 }
335
336 const
337 maxSize = 100,
338 minSize = 10;
339
340 let factor;
341
342 if (srcWidth > srcHeight) {
343 factor = srcWidth / srcHeight;
344 destWidth = maxSize;
345 destHeight = Math.round(destWidth / factor);
346 } else {
347 factor = srcHeight / srcWidth;
348 destHeight = maxSize;
349 destWidth = Math.round(destHeight / factor);
350 }
351
352 if (
353 destWidth > srcWidth || destHeight > srcHeight ||
354 destWidth < minSize || destHeight < minSize
355 ) {
356 destWidth = srcWidth;
357 destHeight = srcHeight;
358 }
359
360 return {
361 srcLeft,
362 srcTop,
363 srcWidth,
364 srcHeight,
365 destWidth,
366 destHeight
367 };
368 }
369
370 _bindImageEvents(resource, options) {
371 return new Promise((resolve, reject) => {
372 const onload = () => {
373 unbindEvents();
374
375 const result = this.getColor(resource, options);
376
377 if (result.error) {
378 reject(result.error);
379 } else {
380 resolve(result);
381 }
382 },
383 onerror = () => {
384 unbindEvents();
385
386 reject(Error(`${ERROR_PREFIX}Error loading image ${resource.src}.`));
387 },
388 onabort = () => {
389 unbindEvents();
390
391 reject(Error(`${ERROR_PREFIX}Image "${resource.src}" loading aborted.`));
392 },
393 unbindEvents = () => {
394 resource.removeEventListener('load', onload);
395 resource.removeEventListener('error', onerror);
396 resource.removeEventListener('abort', onabort);
397 };
398
399 resource.addEventListener('load', onload);
400 resource.addEventListener('error', onerror);
401 resource.addEventListener('abort', onabort);
402 });
403 }
404
405 _prepareResult(value) {
406 const
407 rgb = value.slice(0, 3),
408 rgba = [].concat(rgb, value[3] / 255),
409 isDark = this._isDark(value);
410
411 return {
412 value,
413 rgb: 'rgb(' + rgb.join(',') + ')',
414 rgba: 'rgba(' + rgba.join(',') + ')',
415 hex: this._arrayToHex(rgb),
416 hexa: this._arrayToHex(value),
417 isDark,
418 isLight: !isDark
419 };
420 }
421
422 _getOriginalSize(resource) {
423 if (resource instanceof HTMLImageElement) {
424 return {
425 width: resource.naturalWidth,
426 height: resource.naturalHeight
427 };
428 }
429
430 if (resource instanceof HTMLVideoElement) {
431 return {
432 width: resource.videoWidth,
433 height: resource.videoHeight
434 };
435 }
436
437 return {
438 width: resource.width,
439 height: resource.height
440 };
441 }
442
443 _toHex(num) {
444 let str = num.toString(16);
445 return str.length === 1 ? '0' + str : str;
446 }
447
448 _arrayToHex(arr) {
449 return '#' + arr.map(this._toHex).join('');
450 }
451
452 _isDark(color) {
453 // http://www.w3.org/TR/AERT#color-contrast
454 const result = (color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000;
455
456 return result < 128;
457 }
458
459 _makeCanvas() {
460 return typeof window === 'undefined' ?
461 new OffscreenCanvas(1, 1) :
462 document.createElement('canvas');
463 }
464
465 _outputError(options, error, details) {
466 if (!options.silent) {
467 console.error(`${ERROR_PREFIX}${error}`);
468
469 if (details) {
470 console.error(details);
471 }
472 }
473 }
474}
475
476export default FastAverageColor;