Spaces:
Running
Running
Download extract-colors.js from node-user1/ncp: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/node-user1/ncp/resolve/main/extract-colors.js
- Command line
-
hf download hf://spaces/node-user1/ncp/extract-colors.js
-
curl -L -o extract-colors.js https://huggingface.co/spaces/node-user1/ncp/resolve/main/extract-colors.js
11.4 kB
| 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 } |