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
JavaScript
/*! 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;
})();