UNPKG

fast-average-color

Version:

A simple library that calculates the average color of images, videos and canvas in browser environment.

410 lines (351 loc) 12.7 kB
/*! Fast Average Color | © 2019 Denis Seleznev | MIT License | https://github.com/hcodes/fast-average-color/ */ export default class FastAverageColor { /** * Get asynchronously the average color from not loaded image. * * @param {HTMLImageElement} resource * @param {Object|null} [options] * @param {Array} [options.defaultColor=[255, 255, 255, 255]] * @param {*} [options.data] * @param {string} [options.mode="speed"] "precision" or "speed" * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant" * @param {number} [options.step=1] * @param {number} [options.left=0] * @param {number} [options.top=0] * @param {number} [options.width=width of resource] * @param {number} [options.height=height of resource] * * @returns {Promise} */ getColorAsync(resource, options) { if (resource.complete) { const result = this.getColor(resource, options); return result.error ? Promise.reject(result.error) : Promise.resolve(result); } else { return this._bindImageEvents(resource, options); } } /** * Get the average color from images, videos and canvas. * * @param {HTMLImageElement|HTMLVideoElement|HTMLCanvasElement} resource * @param {Object|null} [options] * @param {Array} [options.defaultColor=[255, 255, 255, 255]] * @param {*} [options.data] * @param {string} [options.mode="speed"] "precision" or "speed" * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant" * @param {number} [options.step=1] * @param {number} [options.left=0] * @param {number} [options.top=0] * @param {number} [options.width=width of resource] * @param {number} [options.height=height of resource] * * @returns {Object} */ getColor(resource, options) { options = options || {}; const defaultColor = this._getDefaultColor(options), originalSize = this._getOriginalSize(resource), size = this._prepareSizeAndPosition(originalSize, options); let error = null, value = defaultColor; if (!size.srcWidth || !size.srcHeight || !size.destWidth || !size.destHeight) { return this._prepareResult( defaultColor, new Error('FastAverageColor: Incorrect sizes.') ); } if (!this._ctx) { this._canvas = this._makeCanvas(); this._ctx = this._canvas.getContext && this._canvas.getContext('2d'); if (!this._ctx) { return this._prepareResult( defaultColor, new Error('FastAverageColor: Canvas Context 2D is not supported in this browser.') ); } } this._canvas.width = size.destWidth; this._canvas.height = size.destHeight; try { this._ctx.clearRect(0, 0, size.destWidth, size.destHeight); this._ctx.drawImage( resource, size.srcLeft, size.srcTop, size.srcWidth, size.srcHeight, 0, 0, size.destWidth, size.destHeight ); const bitmapData = this._ctx.getImageData(0, 0, size.destWidth, size.destHeight).data; value = this.getColorFromArray4(bitmapData, options); } catch (e) { // Security error, CORS // https://developer.mozilla.org/en/docs/Web/HTML/CORS_enabled_image error = e; } return this._prepareResult(value, error); } /** * Get the average color from a array when 1 pixel is 4 bytes. * * @param {Array|Uint8Array} arr * @param {Object} [options] * @param {string} [options.algorithm="sqrt"] "simple", "sqrt" or "dominant" * @param {Array} [options.defaultColor=[255, 255, 255, 255]] * @param {number} [options.step=1] * * @returns {Array} [red (0-255), green (0-255), blue (0-255), alpha (0-255)] */ getColorFromArray4(arr, options) { options = options || {}; const bytesPerPixel = 4, arrLength = arr.length; if (arrLength < bytesPerPixel) { return this._getDefaultColor(options); } const len = arrLength - arrLength % bytesPerPixel, preparedStep = (options.step || 1) * bytesPerPixel, algorithm = '_' + (options.algorithm || 'sqrt') + 'Algorithm'; if (typeof this[algorithm] !== 'function') { throw new Error(`FastAverageColor: ${options.algorithm} is unknown algorithm.`); } return this[algorithm](arr, len, preparedStep); } /** * Destroy the instance. */ destroy() { delete this._canvas; delete this._ctx; } _getDefaultColor(options) { return this._getOption(options, 'defaultColor', [255, 255, 255, 255]); } _getOption(options, name, defaultValue) { return typeof options[name] === 'undefined' ? defaultValue : options[name]; } _prepareSizeAndPosition(originalSize, options) { let srcLeft = this._getOption(options, 'left', 0), srcTop = this._getOption(options, 'top', 0), srcWidth = this._getOption(options, 'width', originalSize.width), srcHeight = this._getOption(options, 'height', originalSize.height), destWidth = srcWidth, destHeight = srcHeight; if (options.mode === 'precision') { return { srcLeft, srcTop, srcWidth, srcHeight, destWidth, destHeight }; } const maxSize = 100, minSize = 10; let factor; if (srcWidth > srcHeight) { factor = srcWidth / srcHeight; destWidth = maxSize; destHeight = Math.round(destWidth / factor); } else { factor = srcHeight / srcWidth; destHeight = maxSize; destWidth = Math.round(destHeight / factor); } if ( destWidth > srcWidth || destHeight > srcHeight || destWidth < minSize || destHeight < minSize ) { destWidth = srcWidth; destHeight = srcHeight; } return { srcLeft, srcTop, srcWidth, srcHeight, destWidth, destHeight }; } _simpleAlgorithm(arr, len, preparedStep) { let redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0, count = 0; for (let i = 0; i < len; i += preparedStep) { const alpha = arr[i + 3], red = arr[i] * alpha, green = arr[i + 1] * alpha, blue = arr[i + 2] * alpha; redTotal += red; greenTotal += green; blueTotal += blue; alphaTotal += alpha; count++; } return alphaTotal ? [ Math.round(redTotal / alphaTotal), Math.round(greenTotal / alphaTotal), Math.round(blueTotal / alphaTotal), Math.round(alphaTotal / count) ] : [0, 0, 0, 0]; } _sqrtAlgorithm(arr, len, preparedStep) { let redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0, count = 0; for (let i = 0; i < len; i += preparedStep) { const red = arr[i], green = arr[i + 1], blue = arr[i + 2], alpha = arr[i + 3]; redTotal += red * red * alpha; greenTotal += green * green * alpha; blueTotal += blue * blue * alpha; alphaTotal += alpha; count++; } 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]; } _dominantAlgorithm(arr, len, preparedStep) { const colorHash = {}, divider = 24; for (let i = 0; i < len; i += preparedStep) { let red = arr[i], green = arr[i + 1], blue = arr[i + 2], alpha = arr[i + 3], key = Math.round(red / divider) + ',' + Math.round(green / divider) + ',' + Math.round(blue / divider); if (colorHash[key]) { 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 ]; } else { colorHash[key] = [red * alpha, green * alpha, blue * alpha, alpha, 1]; } } const buffer = Object.keys(colorHash).map(function(key) { return colorHash[key]; }).sort(function(a, b) { const countA = a[4], countB = b[4]; return countA > countB ? -1 : countA === countB ? 0 : 1; }); const [redTotal, greenTotal, blueTotal, alphaTotal, count] = buffer[0]; return alphaTotal ? [ Math.round(redTotal / alphaTotal), Math.round(greenTotal / alphaTotal), Math.round(blueTotal / alphaTotal), Math.round(alphaTotal / count) ] : [0, 0, 0, 0]; } _bindImageEvents(resource, options) { return new Promise((resolve, reject) => { const onload = () => { unbindEvents(); const result = this.getColor(resource, options); if (result.error) { reject(result.error); } else { resolve(result); } }, onerror = () => { unbindEvents(); reject(new Error('Image error')); }, onabort = () => { unbindEvents(); reject(new Error('Image abort')); }, unbindEvents = () => { resource.removeEventListener('load', onload); resource.removeEventListener('error', onerror); resource.removeEventListener('abort', onabort); }; resource.addEventListener('load', onload); resource.addEventListener('error', onerror); resource.addEventListener('abort', onabort); }); } _prepareResult(value, error) { const rgb = value.slice(0, 3), rgba = [].concat(rgb, value[3] / 255), isDark = this._isDark(value); return { error, value, rgb: 'rgb(' + rgb.join(',') + ')', rgba: 'rgba(' + rgba.join(',') + ')', hex: this._arrayToHex(rgb), hexa: this._arrayToHex(value), isDark, isLight: !isDark }; } _getOriginalSize(resource) { if (resource instanceof HTMLImageElement) { return { width: resource.naturalWidth, height: resource.naturalHeight }; } if (resource instanceof HTMLVideoElement) { return { width: resource.videoWidth, height: resource.videoHeight }; } return { width: resource.width, height: resource.height }; } _toHex(num) { let str = num.toString(16); return str.length === 1 ? '0' + str : str; } _arrayToHex(arr) { return '#' + arr.map(this._toHex).join(''); } _isDark(color) { // http://www.w3.org/TR/AERT#color-contrast const result = (color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000; return result < 128; } _makeCanvas() { return typeof window === 'undefined' ? new OffscreenCanvas(1, 1) : document.createElement('canvas'); } }