ncp / extract-colors.js
atoopub's picture
Upload 6 files
9fa1f67 verified
Raw History Blame Contribute Delete
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 }