1 |
|
2 | function 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 |
|
57 | function 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 |
|
87 | function 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 |
|
117 | const ERROR_PREFIX = 'FastAverageColor: ';
|
118 |
|
119 | class FastAverageColor {
|
120 | |
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
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 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
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 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
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 |
|
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 |
|
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 |
|
434 | console.error(`${ERROR_PREFIX}${error}`);
|
435 |
|
436 | if (details) {
|
437 |
|
438 | console.error(details);
|
439 | }
|
440 | }
|
441 | }
|
442 | }
|
443 |
|
444 | export default FastAverageColor;
|