1 | import dominantAlgorithm from './algorithm/dominant';
|
2 | import simpleAlgorithm from './algorithm/simple';
|
3 | import sqrtAlgorithm from './algorithm/sqrt';
|
4 |
|
5 | const ERROR_PREFIX = 'FastAverageColor: ';
|
6 |
|
7 | export default class FastAverageColor {
|
8 | |
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | getColorAsync(resource, options) {
|
26 | if (!resource) {
|
27 | return Promise.reject(Error('Call .getColorAsync(null) without resource.'));
|
28 | } else if (resource.complete) {
|
29 | const result = this.getColor(resource, options);
|
30 | return result.error ? Promise.reject(result.error) : Promise.resolve(result);
|
31 | } else {
|
32 | return this._bindImageEvents(resource, options);
|
33 | }
|
34 | }
|
35 |
|
36 | |
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | getColor(resource, options) {
|
54 | options = options || {};
|
55 |
|
56 | const defaultColor = this._getDefaultColor(options);
|
57 |
|
58 | let value = defaultColor;
|
59 | if (!resource) {
|
60 | this._outputError(options, 'call .getColor(null) without resource.');
|
61 |
|
62 | return this._prepareResult(defaultColor);
|
63 | }
|
64 |
|
65 | const
|
66 | originalSize = this._getOriginalSize(resource),
|
67 | size = this._prepareSizeAndPosition(originalSize, options);
|
68 |
|
69 | if (!size.srcWidth || !size.srcHeight || !size.destWidth || !size.destHeight) {
|
70 | this._outputError(options, `incorrect sizes for resource "${resource.src}".`);
|
71 |
|
72 | return this._prepareResult(defaultColor);
|
73 | }
|
74 |
|
75 | if (!this._ctx) {
|
76 | this._canvas = this._makeCanvas();
|
77 | this._ctx = this._canvas.getContext && this._canvas.getContext('2d');
|
78 |
|
79 | if (!this._ctx) {
|
80 | this._outputError(options, 'Canvas Context 2D is not supported in this browser.');
|
81 |
|
82 | return this._prepareResult(defaultColor);
|
83 | }
|
84 | }
|
85 |
|
86 | this._canvas.width = size.destWidth;
|
87 | this._canvas.height = size.destHeight;
|
88 |
|
89 | try {
|
90 | this._ctx.clearRect(0, 0, size.destWidth, size.destHeight);
|
91 | this._ctx.drawImage(
|
92 | resource,
|
93 | size.srcLeft, size.srcTop,
|
94 | size.srcWidth, size.srcHeight,
|
95 | 0, 0,
|
96 | size.destWidth, size.destHeight
|
97 | );
|
98 |
|
99 | const bitmapData = this._ctx.getImageData(0, 0, size.destWidth, size.destHeight).data;
|
100 | value = this.getColorFromArray4(bitmapData, options);
|
101 | } catch (e) {
|
102 | this._outputError(options, `security error (CORS) for resource ${resource.src}.\nDetails: https://developer.mozilla.org/en/docs/Web/HTML/CORS_enabled_image`, e);
|
103 | }
|
104 |
|
105 | return this._prepareResult(value);
|
106 | }
|
107 |
|
108 | |
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | getColorFromArray4(arr, options) {
|
120 | options = options || {};
|
121 |
|
122 | const
|
123 | bytesPerPixel = 4,
|
124 | arrLength = arr.length;
|
125 |
|
126 | if (arrLength < bytesPerPixel) {
|
127 | return this._getDefaultColor(options);
|
128 | }
|
129 |
|
130 | const
|
131 | len = arrLength - arrLength % bytesPerPixel,
|
132 | preparedStep = (options.step || 1) * bytesPerPixel;
|
133 |
|
134 | let algorithm;
|
135 |
|
136 | switch (options.algorithm || 'sqrt') {
|
137 | case 'simple':
|
138 | algorithm = simpleAlgorithm;
|
139 | break;
|
140 | case 'sqrt':
|
141 | algorithm = sqrtAlgorithm;
|
142 | break;
|
143 | case 'dominant':
|
144 | algorithm = dominantAlgorithm;
|
145 | break;
|
146 | default:
|
147 | throw new Error(`${ERROR_PREFIX}${options.algorithm} is unknown algorithm.`);
|
148 | }
|
149 |
|
150 | return algorithm(arr, len, preparedStep);
|
151 | }
|
152 |
|
153 | |
154 |
|
155 |
|
156 | destroy() {
|
157 | delete this._canvas;
|
158 | delete this._ctx;
|
159 | }
|
160 |
|
161 | _getDefaultColor(options) {
|
162 | return this._getOption(options, 'defaultColor', [255, 255, 255, 255]);
|
163 | }
|
164 |
|
165 | _getOption(options, name, defaultValue) {
|
166 | return typeof options[name] === 'undefined' ? defaultValue : options[name];
|
167 | }
|
168 |
|
169 | _prepareSizeAndPosition(originalSize, options) {
|
170 | let
|
171 | srcLeft = this._getOption(options, 'left', 0),
|
172 | srcTop = this._getOption(options, 'top', 0),
|
173 | srcWidth = this._getOption(options, 'width', originalSize.width),
|
174 | srcHeight = this._getOption(options, 'height', originalSize.height),
|
175 | destWidth = srcWidth,
|
176 | destHeight = srcHeight;
|
177 |
|
178 | if (options.mode === 'precision') {
|
179 | return {
|
180 | srcLeft,
|
181 | srcTop,
|
182 | srcWidth,
|
183 | srcHeight,
|
184 | destWidth,
|
185 | destHeight
|
186 | };
|
187 | }
|
188 |
|
189 | const
|
190 | maxSize = 100,
|
191 | minSize = 10;
|
192 |
|
193 | let factor;
|
194 |
|
195 | if (srcWidth > srcHeight) {
|
196 | factor = srcWidth / srcHeight;
|
197 | destWidth = maxSize;
|
198 | destHeight = Math.round(destWidth / factor);
|
199 | } else {
|
200 | factor = srcHeight / srcWidth;
|
201 | destHeight = maxSize;
|
202 | destWidth = Math.round(destHeight / factor);
|
203 | }
|
204 |
|
205 | if (
|
206 | destWidth > srcWidth || destHeight > srcHeight ||
|
207 | destWidth < minSize || destHeight < minSize
|
208 | ) {
|
209 | destWidth = srcWidth;
|
210 | destHeight = srcHeight;
|
211 | }
|
212 |
|
213 | return {
|
214 | srcLeft,
|
215 | srcTop,
|
216 | srcWidth,
|
217 | srcHeight,
|
218 | destWidth,
|
219 | destHeight
|
220 | };
|
221 | }
|
222 |
|
223 | _bindImageEvents(resource, options) {
|
224 | return new Promise((resolve, reject) => {
|
225 | const onload = () => {
|
226 | unbindEvents();
|
227 |
|
228 | const result = this.getColor(resource, options);
|
229 |
|
230 | if (result.error) {
|
231 | reject(result.error);
|
232 | } else {
|
233 | resolve(result);
|
234 | }
|
235 |
|
236 | },
|
237 | onerror = () => {
|
238 | unbindEvents();
|
239 |
|
240 | reject(new Error(`${ERROR_PREFIX}Error loading image ${resource.src}.`));
|
241 | },
|
242 | onabort = () => {
|
243 | unbindEvents();
|
244 |
|
245 | reject(new Error(`${ERROR_PREFIX}Image "${resource.src}" loading aborted.`));
|
246 | },
|
247 | unbindEvents = () => {
|
248 | resource.removeEventListener('load', onload);
|
249 | resource.removeEventListener('error', onerror);
|
250 | resource.removeEventListener('abort', onabort);
|
251 | };
|
252 |
|
253 | resource.addEventListener('load', onload);
|
254 | resource.addEventListener('error', onerror);
|
255 | resource.addEventListener('abort', onabort);
|
256 | });
|
257 | }
|
258 |
|
259 | _prepareResult(value) {
|
260 | const
|
261 | rgb = value.slice(0, 3),
|
262 | rgba = [].concat(rgb, value[3] / 255),
|
263 | isDark = this._isDark(value);
|
264 |
|
265 | return {
|
266 | value,
|
267 | rgb: 'rgb(' + rgb.join(',') + ')',
|
268 | rgba: 'rgba(' + rgba.join(',') + ')',
|
269 | hex: this._arrayToHex(rgb),
|
270 | hexa: this._arrayToHex(value),
|
271 | isDark,
|
272 | isLight: !isDark
|
273 | };
|
274 | }
|
275 |
|
276 | _getOriginalSize(resource) {
|
277 | if (resource instanceof HTMLImageElement) {
|
278 | return {
|
279 | width: resource.naturalWidth,
|
280 | height: resource.naturalHeight
|
281 | };
|
282 | }
|
283 |
|
284 | if (resource instanceof HTMLVideoElement) {
|
285 | return {
|
286 | width: resource.videoWidth,
|
287 | height: resource.videoHeight
|
288 | };
|
289 | }
|
290 |
|
291 | return {
|
292 | width: resource.width,
|
293 | height: resource.height
|
294 | };
|
295 | }
|
296 |
|
297 | _toHex(num) {
|
298 | let str = num.toString(16);
|
299 | return str.length === 1 ? '0' + str : str;
|
300 | }
|
301 |
|
302 | _arrayToHex(arr) {
|
303 | return '#' + arr.map(this._toHex).join('');
|
304 | }
|
305 |
|
306 | _isDark(color) {
|
307 |
|
308 | const result = (color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000;
|
309 |
|
310 | return result < 128;
|
311 | }
|
312 |
|
313 | _makeCanvas() {
|
314 | return typeof window === 'undefined' ?
|
315 | new OffscreenCanvas(1, 1) :
|
316 | document.createElement('canvas');
|
317 | }
|
318 |
|
319 | _outputError(options, error, details) {
|
320 | if (!options.silent) {
|
321 |
|
322 | console.error(`${ERROR_PREFIX}${error}`);
|
323 |
|
324 | if (details) {
|
325 |
|
326 | console.error(details);
|
327 | }
|
328 | }
|
329 | }
|
330 | }
|