UNPKG

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