class Color { constructor(red, green, blue, hex = red << 16 | green << 8 | blue) { this._count = 1 this.__saturation = -1 this.__hue = -1 this.__lightness = -1 this.__intensity = -1 this._red = red this._green = green this._blue = blue this._hex = hex } static distance(colorA, colorB) { return (Math.abs(colorB._red - colorA._red) + Math.abs(colorB._green - colorA._green) + Math.abs(colorB._blue - colorA._blue)) / (3 * 255) } updateHSL() { const red = this._red / 255 const green = this._green / 255 const blue = this._blue / 255 const max = Math.max(red, green, blue) const min = Math.min(red, green, blue) this.__lightness = (max + min) / 2 if (max === min) { this.__hue = 0 this.__saturation = 0 this.__intensity = 0 } else { const distance2 = max - min this.__saturation = this.__lightness > 0.5 ? distance2 / (2 - max - min) : distance2 / (max + min) this.__intensity = this.__saturation * ((0.5 - Math.abs(0.5 - this.__lightness)) * 2) switch (max) { case red: this.__hue = ((green - blue) / distance2 + (green < blue ? 6 : 0)) / 6 break case green: this.__hue = ((blue - red) / distance2 + 2) / 6 break case blue: this.__hue = ((red - green) / distance2 + 4) / 6 break } } } get _hue() { if (this.__hue === -1) { this.updateHSL() } return this.__hue } get _saturation() { if (this.__saturation === -1) { this.updateHSL() } return this.__saturation } get _lightness() { if (this.__lightness === -1) { this.updateHSL() } return this.__lightness } get _intensity() { if (this.__intensity === -1) { this.updateHSL() } return this.__intensity } } const distance = (a, b) => Math.abs(a - b) const hueDistance = (a, b) => Math.min(distance(a, b), distance((a + 0.5) % 1, (b + 0.5) % 1)) class AverageGroup { constructor() { this.colors = [] this._average = null } addColor(color) { this.colors.push(color) this._average = null } isSamePalette(color, hue, saturation, lightness) { for (let i = 0; i < this.colors.length; i++) { const currentColor = this.colors[i] const isSame = hueDistance(currentColor._hue, color._hue) < hue && distance(currentColor._saturation, color._saturation) < saturation && distance(currentColor._lightness, color._lightness) < lightness if (!isSame) { return false } } return true } get average() { if (!this._average) { const { r, g, b } = this.colors.reduce((total2, color) => { total2.r += color._red total2.g += color._green total2.b += color._blue return total2 }, { r: 0, g: 0, b: 0 }) const total = this.colors.reduce((_count, color) => _count + color._count, 0) this._average = new Color( Math.round(r / this.colors.length), Math.round(g / this.colors.length), Math.round(b / this.colors.length) ) this._average._count = total } return this._average } } class AverageManager { constructor(hue, saturation, lightness) { this._groups = [] this._hue = hue this._saturation = saturation this._lightness = lightness } addColor(color) { const samePalette = this._groups.find((averageGroup) => averageGroup.isSamePalette(color, this._hue, this._saturation, this._lightness)) if (samePalette) { samePalette.addColor(color) } else { const averageGroup = new AverageGroup() averageGroup.addColor(color) this._groups.push(averageGroup) } } getGroups() { return this._groups.map((averageGroup) => averageGroup.average) } } var sortColors = (list, _pixels, _hueDistance, _saturationDistance, _lightnessDistance) => { const averageManager = new AverageManager(_hueDistance, _saturationDistance, _lightnessDistance) list.forEach((color) => averageManager.addColor(color)) const sorted = averageManager.getGroups() sorted.sort((a, b) => { const bPower = (b._intensity + 0.1) * (0.9 - b._count / _pixels) const aPower = (a._intensity + 0.1) * (0.9 - a._count / _pixels) return bPower - aPower }) return sorted } const createFinalColor = (color, pixels) => { return { hex: `#${'0'.repeat(6 - color._hex.toString(16).length)}${color._hex.toString(16)}`, red: color._red, green: color._green, blue: color._blue, area: color._count / pixels, hue: color._hue, saturation: color._saturation, lightness: color._lightness, intensity: color._intensity } } const testUint = (label, val, min = 0, max = Number.MAX_SAFE_INTEGER) => { if (!Number.isInteger(val) || val < min || val > max) { throw new Error(`${label} is invalid (${val})`) } return val } const testNumber = (label, val, min = 0, max = Number.MAX_VALUE) => { if (Number(val) !== val || val < min || val > max) { throw new Error(`${label} is invalid (${val})`) } return val } const testFunction = (label, val) => { if (!val || {}.toString.call(val) !== '[object Function]') { throw new Error(`${label} is invalid (${val})`) } return val } var cleanInputs = (options) => { var _a, _b, _c, _d, _e, _f, _g, _h return [ testUint('pixels', (_a = options.pixels) != null ? _a : 1e4, 1), testNumber('distance', (_b = options.distance) != null ? _b : 0.12, 0, 1), testUint('splitPower', (_c = options.splitPower) != null ? _c : 10, 2, 15), testFunction('colorValidator', (_d = options.colorValidator) != null ? _d : (_red, _green, _blue, _alpha) => (_alpha != null ? _alpha : 255) > 250), testNumber('hueDistance', (_e = options.hueDistance) != null ? _e : 0.08333333333333333, 0, 1), testNumber('saturationDistance', (_f = options.saturationDistance) != null ? _f : 0.2, 0, 1), testNumber('lightnessDistance', (_g = options.lightnessDistance) != null ? _g : 0.2, 0, 1), (_h = options.crossOrigin) != null ? _h : null ] } class BudGroup { constructor() { this._count = 1 this._children = {} } addColor(_hex, _red, _green, _blue) { if (this._children[_hex]) { this._children[_hex]._count++ } else { this._children[_hex] = new Color(_red, _green, _blue, _hex) } return this._children[_hex] } getList() { return Object.keys(this._children).map((key) => this._children[key]) } getMaxWeight(_count) { if (this._maxWeight === void 0) { const list = this.getList().map((child) => child._count / _count) list.sort((a, b) => b - a) this._maxWeight = list[0] || 0 } return this._maxWeight } getMaxWeightColor(_count) { const list = this.getList() list.sort((a, b) => { return b._count / _count - a._count / _count }) return list[0] } getMaxCountColor() { const list = this.getList() const biggest = list.reduce((a, b) => a._count >= b._count ? a : b) return biggest } } class RootGroup { constructor() { this._count = 1 this._children = {} } addRootGroup(key) { if (this._children[key]) { this._children[key]._count++ } else { this._children[key] = new RootGroup() } return this._children[key] } getList() { return Object.keys(this._children).map((key) => this._children[key]) } addBudGroup(key) { if (this._children[key]) { this._children[key]._count++ } else { this._children[key] = new BudGroup() } return this._children[key] } getMaxWeight(_count) { if (this._maxWeight === void 0) { const list = this.getList().map((child) => child._count / _count) list.sort((a, b) => b - a) this._maxWeight = list[0] || 0 } return this._maxWeight } getMaxWeightColor(_count) { const list = this.getList() list.sort((a, b) => { return b._count / _count - a._count / _count }) return list[0].getMaxWeightColor(_count) } getMaxCountColor() { const list = this.getList() const biggest = list.reduce((a, b) => a.getMaxCountColor()._count >= b.getMaxCountColor()._count ? a : b) return biggest.getMaxCountColor() } getColors(_distance, _count) { const list = this.getList().map((child) => { const { _count: _count2 } = child const color = child.getMaxCountColor() color._count = _count2 return color }) list.sort((a, b) => b._count / _count - a._count / _count) const newList = [] list.forEach((color) => { const near = newList.find((col) => Color.distance(col, color) < _distance) if (!near) { newList.push(color) } else { near._count += color._count } }) return newList } } var extractor = ({ data, width, height }, _pixels, _distance, _splitPower, _colorValidator) => { const rootGroup = new RootGroup() const reducer = width && height ? Math.floor(width * height / _pixels) || 1 : 1 for (let i = 0; i < data.length; i += 4 * reducer) { const r = data[i] const g = data[i + 1] const b = data[i + 2] const a = data[i + 3] if (_colorValidator(r, g, b, a)) { const real = r << 16 | g << 8 | b const medium = (r >> 4 & 15) << 2 | (g >> 4 & 15) << 1 | b >> 4 & 15 const small = Math.round(r * (_splitPower - 1) / 255) * (_splitPower * _splitPower) + Math.round(g * (_splitPower - 1) / 255) * _splitPower + Math.round(b * (_splitPower - 1) / 255) const smallGroup = rootGroup.addRootGroup(small) const mediumGroup = smallGroup.addBudGroup(medium) mediumGroup.addColor(real, r, g, b) } } return rootGroup.getColors(_distance, _pixels) } const sortFinalColors = (_colors, _pixels, _hueDistance, _saturationDistance, _lightnessDistance) => { const list = sortColors(_colors, _pixels, _hueDistance, _saturationDistance, _lightnessDistance) return list.map((color) => createFinalColor(color, _pixels)) } const extractColorsFromImageData = (imageData, options = {}) => { const [_pixels, _distance, _splitPower, _colorValidator, _hueDistance, _saturationDistance, _lightnessDistance] = cleanInputs(options) const colors = extractor(imageData, _pixels, _distance, _splitPower, _colorValidator) const px = imageData.width && imageData.height ? Math.min(imageData.width * imageData.height, _pixels) : _pixels return sortFinalColors(colors, px, _hueDistance, _saturationDistance, _lightnessDistance) } const extractColors = (imageData, options) => { if (imageData.data) { return new Promise((resolve) => { resolve(extractColorsFromImageData(imageData, options)) }) } throw new Error('Send imageData to extractColors') } const extractColorsFromImage = () => { throw new Error('Can not use extractColorsFromImage for Node.js') } const extractColorsFromSrc = () => { throw new Error('Can not use extractColorsFromSrc for Node.js') } export { extractColors as default, extractColors, extractColorsFromImage, extractColorsFromImageData, extractColorsFromSrc }