UNPKG

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