UNPKG

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