UNPKG

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