UNPKG

fast-average-color

Version:

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

534 lines (523 loc) 20.4 kB
/*! Fast Average Color | © 2026 Denis Seleznev | MIT License | https://github.com/fast-average-color/fast-average-color */ (function () { 'use strict'; function toHex(num) { var str = num.toString(16); return str.length === 1 ? '0' + str : str; } function arrayToHex(arr) { return '#' + arr.map(toHex).join(''); } function isDark(color) { // http://www.w3.org/TR/AERT#color-contrast var result = (color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000; return result < 128; } function prepareIgnoredColor(color) { if (!color) { return []; } return isRGBArray(color) ? color : [color]; } function isRGBArray(value) { return Array.isArray(value[0]); } function isIgnoredColor(data, index, ignoredColor) { for (var i = 0; i < ignoredColor.length; i++) { if (isIgnoredColorAsNumbers(data, index, ignoredColor[i])) { return true; } } return false; } function isIgnoredColorAsNumbers(data, index, ignoredColor) { switch (ignoredColor.length) { case 3: // [red, green, blue] if (isIgnoredRGBColor(data, index, ignoredColor)) { return true; } break; case 4: // [red, green, blue, alpha] if (isIgnoredRGBAColor(data, index, ignoredColor)) { return true; } break; case 5: // [red, green, blue, alpha, threshold] if (isIgnoredRGBAColorWithThreshold(data, index, ignoredColor)) { return true; } break; default: return false; } } function isIgnoredRGBColor(data, index, ignoredColor) { // Ignore if the pixel are transparent. if (data[index + 3] !== 255) { return true; } if (data[index] === ignoredColor[0] && data[index + 1] === ignoredColor[1] && data[index + 2] === ignoredColor[2]) { return true; } return false; } function isIgnoredRGBAColor(data, index, ignoredColor) { if (data[index + 3] && ignoredColor[3]) { return data[index] === ignoredColor[0] && data[index + 1] === ignoredColor[1] && data[index + 2] === ignoredColor[2] && data[index + 3] === ignoredColor[3]; } // Ignore rgb components if the pixel are fully transparent. return data[index + 3] === ignoredColor[3]; } function inRange(colorComponent, ignoredColorComponent, value) { return colorComponent >= (ignoredColorComponent - value) && colorComponent <= (ignoredColorComponent + value); } function isIgnoredRGBAColorWithThreshold(data, index, ignoredColor) { var redIgnored = ignoredColor[0]; var greenIgnored = ignoredColor[1]; var blueIgnored = ignoredColor[2]; var alphaIgnored = ignoredColor[3]; var threshold = ignoredColor[4]; var alphaData = data[index + 3]; var alphaInRange = inRange(alphaData, alphaIgnored, threshold); if (!alphaIgnored) { return alphaInRange; } if (!alphaData && alphaInRange) { return true; } if (inRange(data[index], redIgnored, threshold) && inRange(data[index + 1], greenIgnored, threshold) && inRange(data[index + 2], blueIgnored, threshold) && alphaInRange) { return true; } return false; } var DEFAULT_DOMINANT_DIVIDER = 24; function dominantAlgorithm(arr, len, options) { var colorHash = {}; var divider = options.dominantDivider || DEFAULT_DOMINANT_DIVIDER; var ignoredColor = options.ignoredColor; var step = options.step; var max = [0, 0, 0, 0, 0]; for (var i = 0; i < len; i += step) { var red = arr[i]; var green = arr[i + 1]; var blue = arr[i + 2]; var alpha = arr[i + 3]; if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) { continue; } var 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]; } if (max[4] < colorHash[key][4]) { max = colorHash[key]; } } var redTotal = max[0]; var greenTotal = max[1]; var blueTotal = max[2]; var alphaTotal = max[3]; var count = max[4]; return alphaTotal ? [ Math.round(redTotal / alphaTotal), Math.round(greenTotal / alphaTotal), Math.round(blueTotal / alphaTotal), Math.round(alphaTotal / count) ] : options.defaultColor; } function simpleAlgorithm(arr, len, options) { var redTotal = 0; var greenTotal = 0; var blueTotal = 0; var alphaTotal = 0; var count = 0; var ignoredColor = options.ignoredColor; var step = options.step; for (var i = 0; i < len; i += step) { var alpha = arr[i + 3]; var red = arr[i] * alpha; var green = arr[i + 1] * alpha; var blue = arr[i + 2] * alpha; if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) { continue; } 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) ] : options.defaultColor; } function sqrtAlgorithm(arr, len, options) { var redTotal = 0; var greenTotal = 0; var blueTotal = 0; var alphaTotal = 0; var count = 0; var ignoredColor = options.ignoredColor; var step = options.step; for (var i = 0; i < len; i += step) { var red = arr[i]; var green = arr[i + 1]; var blue = arr[i + 2]; var alpha = arr[i + 3]; if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) { continue; } 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) ] : options.defaultColor; } function getDefaultColor(options) { return getOption(options, 'defaultColor', [0, 0, 0, 0]); } function getOption(options, name, defaultValue) { return (options[name] === undefined ? defaultValue : options[name]); } var MIN_SIZE = 10; var MAX_SIZE = 100; function isSvg(filename) { return filename.search(/\.svg(\?|$)/i) !== -1; } function getOriginalSize(resource) { if (isInstanceOfHTMLImageElement(resource)) { var width = resource.naturalWidth; var height = resource.naturalHeight; // For SVG images with only viewBox attribute if (!resource.naturalWidth && isSvg(resource.src)) { width = height = MAX_SIZE; } return { width: width, height: height, }; } if (isInstanceOfHTMLVideoElement(resource)) { return { width: resource.videoWidth, height: resource.videoHeight }; } if (isInstanceOfVideoFrame(resource)) { return { width: resource.codedWidth, height: resource.codedHeight, }; } return { width: resource.width, height: resource.height }; } function getSrc(resource) { if (isInstanceOfHTMLCanvasElement(resource)) { return 'canvas'; } if (isInstanceOfOffscreenCanvas(resource)) { return 'offscreencanvas'; } if (isInstanceOfVideoFrame(resource)) { return 'videoframe'; } if (isInstanceOfImageBitmap(resource)) { return 'imagebitmap'; } return resource.src; } function isInstanceOfHTMLImageElement(resource) { return typeof HTMLImageElement !== 'undefined' && resource instanceof HTMLImageElement; } var hasOffscreenCanvas = typeof OffscreenCanvas !== 'undefined'; function isInstanceOfOffscreenCanvas(resource) { return hasOffscreenCanvas && resource instanceof OffscreenCanvas; } function isInstanceOfHTMLVideoElement(resource) { return typeof HTMLVideoElement !== 'undefined' && resource instanceof HTMLVideoElement; } function isInstanceOfVideoFrame(resource) { return typeof VideoFrame !== 'undefined' && resource instanceof VideoFrame; } function isInstanceOfHTMLCanvasElement(resource) { return typeof HTMLCanvasElement !== 'undefined' && resource instanceof HTMLCanvasElement; } function isInstanceOfImageBitmap(resource) { return typeof ImageBitmap !== 'undefined' && resource instanceof ImageBitmap; } function prepareSizeAndPosition(originalSize, options) { var srcLeft = getOption(options, 'left', 0); var srcTop = getOption(options, 'top', 0); var srcWidth = getOption(options, 'width', originalSize.width); var srcHeight = getOption(options, 'height', originalSize.height); var destWidth = srcWidth; var destHeight = srcHeight; if (options.mode === 'precision') { return { srcLeft: srcLeft, srcTop: srcTop, srcWidth: srcWidth, srcHeight: srcHeight, destWidth: destWidth, destHeight: destHeight }; } var factor; if (srcWidth > srcHeight) { factor = srcWidth / srcHeight; destWidth = MAX_SIZE; destHeight = Math.round(destWidth / factor); } else { factor = srcHeight / srcWidth; destHeight = MAX_SIZE; destWidth = Math.round(destHeight / factor); } if (destWidth > srcWidth || destHeight > srcHeight || destWidth < MIN_SIZE || destHeight < MIN_SIZE) { destWidth = srcWidth; destHeight = srcHeight; } return { srcLeft: srcLeft, srcTop: srcTop, srcWidth: srcWidth, srcHeight: srcHeight, destWidth: destWidth, destHeight: destHeight }; } var isWebWorkers = typeof window === 'undefined'; function makeCanvas() { if (isWebWorkers) { return hasOffscreenCanvas ? new OffscreenCanvas(1, 1) : null; } return document.createElement('canvas'); } var ERROR_PREFIX = 'FastAverageColor: '; function getError(message) { return Error(ERROR_PREFIX + message); } function outputError(error, silent) { if (!silent) { console.error(error); } } var FastAverageColor = /** @class */ (function () { function FastAverageColor() { this.canvas = null; this.ctx = null; } FastAverageColor.prototype.getColorAsync = function (resource, options) { if (!resource) { return Promise.reject(getError('call .getColorAsync() without resource')); } if (typeof resource === 'string') { // Web workers if (typeof Image === 'undefined') { return Promise.reject(getError('resource as string is not supported in this environment')); } var img = new Image(); img.crossOrigin = options && options.crossOrigin || ''; var promise = this.bindImageEvents(img, options); img.src = resource; return promise; } else if (isInstanceOfHTMLImageElement(resource) && !resource.complete) { return this.bindImageEvents(resource, options); } else { var result = this.getColor(resource, options); return result.error ? Promise.reject(result.error) : Promise.resolve(result); } }; /** * Get the average color from images, videos and canvas. */ FastAverageColor.prototype.getColor = function (resource, options) { options = options || {}; var defaultColor = getDefaultColor(options); if (!resource) { var error = getError('call .getColor() without resource'); outputError(error, options.silent); return this.prepareResult(defaultColor, error); } var originalSize = getOriginalSize(resource); var size = prepareSizeAndPosition(originalSize, options); if (!size.srcWidth || !size.srcHeight || !size.destWidth || !size.destHeight) { var error = getError("incorrect sizes for resource \"".concat(getSrc(resource), "\"")); outputError(error, options.silent); return this.prepareResult(defaultColor, error); } if (!this.canvas) { this.canvas = makeCanvas(); if (!this.canvas) { var error = getError('OffscreenCanvas is not supported in this browser'); outputError(error, options.silent); return this.prepareResult(defaultColor, error); } } if (!this.ctx) { this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); if (!this.ctx) { var error = getError('Canvas Context 2D is not supported in this browser'); outputError(error, options.silent); return this.prepareResult(defaultColor, error); } this.ctx.imageSmoothingEnabled = false; } 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); var bitmapData = this.ctx.getImageData(0, 0, size.destWidth, size.destHeight).data; return this.prepareResult(this.getColorFromArray4(bitmapData, options)); } catch (originalError) { var error = getError("security error (CORS) for resource ".concat(getSrc(resource), ".\nDetails: https://developer.mozilla.org/en/docs/Web/HTML/CORS_enabled_image")); outputError(error, options.silent); if (!options.silent) { console.error(originalError); } return this.prepareResult(defaultColor, error); } }; /** * Get the average color from a array when 1 pixel is 4 bytes. */ FastAverageColor.prototype.getColorFromArray4 = function (arr, options) { options = options || {}; var bytesPerPixel = 4; var arrLength = arr.length; var defaultColor = getDefaultColor(options); if (arrLength < bytesPerPixel) { return defaultColor; } var len = arrLength - arrLength % bytesPerPixel; var step = (options.step || 1) * bytesPerPixel; var algorithm; switch (options.algorithm || 'sqrt') { case 'simple': algorithm = simpleAlgorithm; break; case 'sqrt': algorithm = sqrtAlgorithm; break; case 'dominant': algorithm = dominantAlgorithm; break; default: throw getError("".concat(options.algorithm, " is unknown algorithm")); } return algorithm(arr, len, { defaultColor: defaultColor, ignoredColor: prepareIgnoredColor(options.ignoredColor), step: step, dominantDivider: options.dominantDivider, }); }; /** * Get color data from value ([r, g, b, a]). */ FastAverageColor.prototype.prepareResult = function (value, error) { var rgb = value.slice(0, 3); var rgba = [value[0], value[1], value[2], value[3] / 255]; var isDarkColor = isDark(value); return { value: [value[0], value[1], value[2], value[3]], rgb: 'rgb(' + rgb.join(',') + ')', rgba: 'rgba(' + rgba.join(',') + ')', hex: arrayToHex(rgb), hexa: arrayToHex(value), isDark: isDarkColor, isLight: !isDarkColor, error: error, }; }; /** * Destroy the instance. */ FastAverageColor.prototype.destroy = function () { if (this.canvas) { this.canvas.width = 1; this.canvas.height = 1; this.canvas = null; } this.ctx = null; }; FastAverageColor.prototype.bindImageEvents = function (resource, options) { var _this = this; return new Promise(function (resolve, reject) { var unbindEvents = function () { resource.removeEventListener('load', onload); resource.removeEventListener('error', onerror); resource.removeEventListener('abort', onabort); }; var onload = function () { unbindEvents(); var result = _this.getColor(resource, options); if (result.error) { reject(result.error); } else { resolve(result); } }; var onerror = function () { unbindEvents(); reject(getError("Error loading image \"".concat(resource.src, "\""))); }; var onabort = function () { unbindEvents(); reject(getError("Image \"".concat(resource.src, "\" loading aborted"))); }; resource.addEventListener('load', onload); resource.addEventListener('error', onerror); resource.addEventListener('abort', onabort); if (resource.complete) { onload(); } }); }; return FastAverageColor; }()); var global = typeof window !== 'undefined' ? window : self; global.FastAverageColor = FastAverageColor; })();