UNPKG

10.5 kBJavaScriptView Raw
1import dominantAlgorithm from './algorithm/dominant';
2import simpleAlgorithm from './algorithm/simple';
3import sqrtAlgorithm from './algorithm/sqrt';
4
5const ERROR_PREFIX = 'FastAverageColor: ';
6
7export default class FastAverageColor {
8 /**
9 * Get asynchronously the average color from not loaded image.
10 *
11 * @param {HTMLImageElement | null} resource
12 * @param {Object} [options]
13 * @param {Array} [options.defaultColor=[255, 255, 255, 255]]
14 * @param {string} [options.mode="speed"] "precision" or "speed"
15 * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant"
16 * @param {number} [options.step=1]
17 * @param {number} [options.left=0]
18 * @param {number} [options.top=0]
19 * @param {number} [options.width=width of resource]
20 * @param {number} [options.height=height of resource]
21 * @param {boolean} [options.silent] Disable error output via console.error
22 *
23 * @returns {Promise}
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 * Get the average color from images, videos and canvas.
38 *
39 * @param {HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | null} resource
40 * @param {Object} [options]
41 * @param {Array} [options.defaultColor=[255, 255, 255, 255]]
42 * @param {string} [options.mode="speed"] "precision" or "speed"
43 * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant"
44 * @param {number} [options.step=1]
45 * @param {number} [options.left=0]
46 * @param {number} [options.top=0]
47 * @param {number} [options.width=width of resource]
48 * @param {number} [options.height=height of resource]
49 * @param {boolean} [options.silent] Disable error output via console.error
50 *
51 * @returns {Object}
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 * Get the average color from a array when 1 pixel is 4 bytes.
110 *
111 * @param {Array|Uint8Array} arr
112 * @param {Object} [options]
113 * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant"
114 * @param {Array} [options.defaultColor=[255, 255, 255, 255]]
115 * @param {number} [options.step=1]
116 *
117 * @returns {Array} [red (0-255), green (0-255), blue (0-255), alpha (0-255)]
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 * Destroy the instance.
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 // http://www.w3.org/TR/AERT#color-contrast
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 // eslint-disable-next-line no-console
322 console.error(`${ERROR_PREFIX}${error}`);
323
324 if (details) {
325 // eslint-disable-next-line no-console
326 console.error(details);
327 }
328 }
329 }
330}