UNPKG

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