color-octree
Advanced tools
Comparing version 1.1.1 to 2.0.0
233
dist/cjs.js
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
function _defineProperty(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
} | ||
function _objectSpread(target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i] != null ? arguments[i] : {}; | ||
var ownKeys = Object.keys(source); | ||
if (typeof Object.getOwnPropertySymbols === 'function') { | ||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { | ||
return Object.getOwnPropertyDescriptor(source, sym).enumerable; | ||
})); | ||
} | ||
ownKeys.forEach(function (key) { | ||
_defineProperty(target, key, source[key]); | ||
}); | ||
} | ||
return target; | ||
} | ||
function _slicedToArray(arr, i) { | ||
@@ -7,6 +43,2 @@ return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); | ||
function _toArray(arr) { | ||
return _arrayWithHoles(arr) || _iterableToArray(arr) || _nonIterableRest(); | ||
} | ||
function _arrayWithHoles(arr) { | ||
@@ -16,6 +48,2 @@ if (Array.isArray(arr)) return arr; | ||
function _iterableToArray(iter) { | ||
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); | ||
} | ||
function _iterableToArrayLimit(arr, i) { | ||
@@ -53,4 +81,2 @@ var _arr = []; | ||
const dist = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2; | ||
const hexToRgb = hex => { | ||
@@ -63,96 +89,131 @@ const h = parseHex(hex); | ||
const hexToOctants = hex => { | ||
const h = parseHex(hex); | ||
const distRgb = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2; | ||
const _Array$from = Array.from({ | ||
length: 3 | ||
}, (_, i) => parseInt(h.slice(2 * i, 2 * i + 2), 16).toString(2).padStart(8, 0)), | ||
_Array$from2 = _slicedToArray(_Array$from, 3), | ||
r = _Array$from2[0], | ||
g = _Array$from2[1], | ||
b = _Array$from2[2]; | ||
const distHex = (a, b) => distRgb(hexToRgb(a), hexToRgb(b)); | ||
return Array.from({ | ||
length: 8 | ||
}, (_, i) => r[i] + g[i] + b[i]); | ||
const rgbToBin = rgb => rgb.map(x => x.toString(2).padStart(8, 0)); | ||
const hexToBin = hex => rgbToBin(hexToRgb(hex)); | ||
const buildOctree = (depth, n = 0) => n > depth ? undefined : { | ||
n, | ||
colors: [], | ||
'000': buildOctree(depth, n + 1), | ||
'001': buildOctree(depth, n + 1), | ||
'010': buildOctree(depth, n + 1), | ||
'011': buildOctree(depth, n + 1), | ||
'100': buildOctree(depth, n + 1), | ||
'101': buildOctree(depth, n + 1), | ||
'110': buildOctree(depth, n + 1), | ||
'111': buildOctree(depth, n + 1) | ||
}; | ||
class ColorOctant { | ||
constructor(path = []) { | ||
this.path = path; | ||
this.colors = []; | ||
let root; | ||
const init = () => { | ||
root = buildOctree(7); // 8 would go down to leaves, but it's too intense for nodejs | ||
}; | ||
const add = cols => { | ||
if (!root) init(); | ||
if (!path.length) { | ||
this.closest = hex => { | ||
const octants = hexToOctants(hex); | ||
let child = this; | ||
if (Array.isArray(cols)) { | ||
return cols.forEach(c => _add(c)); // might want to precmpute rgb to spped up search | ||
} | ||
for (const octant of octants) { | ||
if (!child[octant] || !child[octant].colors.length) break; | ||
child = child[octant]; | ||
} | ||
_add(cols); | ||
}; | ||
if (child.colors.length === 1) return child.colors[0]; | ||
const targ = hexToRgb(hex); | ||
let maxDist = Infinity, | ||
closest; | ||
child.colors.forEach(col => { | ||
const c = hexToRgb(col.hex); | ||
const d = dist(c, targ); | ||
const _add = col => { | ||
let node = root; | ||
const bin = hexToBin(col.hex); | ||
if (d < maxDist) { | ||
maxDist = d; | ||
closest = col; | ||
} | ||
}); | ||
return Object.assign({ | ||
d: maxDist | ||
}, closest); | ||
}; | ||
} | ||
for (let i = 0; i < 7; i++) { | ||
const k = bin[0][i] + bin[1][i] + bin[2][i]; | ||
node = node[k]; | ||
node.colors.push(col); | ||
} | ||
}; | ||
/** | ||
* get neighbors of a given node | ||
* example: neighbors(['0010', '1101', '0101'], '000') will get the 7 neighbors of this node next the firsr corner ('000') | ||
*/ | ||
addAll(colors) { | ||
colors.forEach(({ | ||
hex, | ||
name | ||
}) => this.add({ | ||
hex, | ||
name, | ||
octants: hexToOctants(hex) | ||
})); | ||
const neighbors = ([r, g, b], dir) => { | ||
const rDec = parseInt(r, 2); | ||
const gDec = parseInt(g, 2); | ||
const bDec = parseInt(b, 2); | ||
const nodes = []; | ||
const n = r.length; | ||
for (let dr = dir[0] - 1; dr <= dir[0]; dr++) { | ||
for (let dg = dir[1] - 1; dg <= dir[1]; dg++) { | ||
for (let db = dir[2] - 1; db <= dir[2]; db++) { | ||
const R = rDec + dr, | ||
G = gDec + dg, | ||
B = bDec + db; | ||
if (R >= 0 && R < 2 ** n && G >= 0 && G < 2 ** n && B >= 0 && B < 2 ** n) { | ||
nodes.push([R.toString(2).padStart(n, 0), G.toString(2).padStart(n, 0), B.toString(2).padStart(n, 0)]); | ||
} | ||
} | ||
} | ||
} | ||
add(color) { | ||
if (this.path.length >= 8 || !this.colors.length) { | ||
this.colors.push(color); | ||
return; | ||
} // else we need to go deeper | ||
// get first octant of each color, and put them in corresponding child for this octant | ||
return nodes; | ||
}; | ||
const getNodeFromCoords = ([r, g, b]) => [...r].reduce((node, ri, i) => node[ri + g[i] + b[i]], root); | ||
this.colors.concat(color).forEach((_ref) => { | ||
let hex = _ref.hex, | ||
name = _ref.name, | ||
_ref$octants = _toArray(_ref.octants), | ||
octant = _ref$octants[0], | ||
rest = _ref$octants.slice(1); | ||
const closest = hex => { | ||
const rgb = hexToRgb(hex); | ||
const child = this.child(octant); | ||
child.add({ | ||
hex, | ||
name, | ||
octants: rest | ||
}); | ||
}); | ||
} | ||
const _rgbToBin = rgbToBin(rgb), | ||
_rgbToBin2 = _slicedToArray(_rgbToBin, 3), | ||
r = _rgbToBin2[0], | ||
g = _rgbToBin2[1], | ||
b = _rgbToBin2[2]; // reminder: we don't/can't store level 8 | ||
child(octant) { | ||
if (this[octant]) return this[octant]; | ||
this[octant] = new ColorOctant(this.path.concat(octant)); | ||
return this[octant]; | ||
for (let i = 7; i > 0; i--) { | ||
const coords = [r.slice(0, i), g.slice(0, i), b.slice(0, i)]; // take one resolution higher, since we don't/can't store level 8 | ||
const ns = neighbors(coords, r[i] + g[i] + b[i]); | ||
const colors = ns.reduce((cs, n) => cs.concat(getNodeFromCoords(n).colors), []); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
} // search in all | ||
const colors = ['000', '001', '010', '011', '100', '101', '110', '111'].reduce((cs, c) => cs.concat(root[c].colors), []); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
} | ||
console.log(`Couldn't find any color`); | ||
}; | ||
module.exports = ColorOctant; | ||
const closestIn = (rgb, colors) => { | ||
let minDist = Infinity, | ||
best; | ||
colors.forEach(col => { | ||
const d = distRgb(hexToRgb(col.hex), rgb); | ||
if (d < minDist) { | ||
minDist = d; | ||
best = col; | ||
} | ||
}); | ||
return _objectSpread({}, best, { | ||
d: minDist | ||
}); | ||
}; | ||
exports.parseHex = parseHex; | ||
exports.distHex = distHex; | ||
exports.hexToBin = hexToBin; | ||
exports.init = init; | ||
exports.add = add; | ||
exports.closest = closest; |
226
dist/es.js
@@ -0,1 +1,35 @@ | ||
function _defineProperty(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
} | ||
function _objectSpread(target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i] != null ? arguments[i] : {}; | ||
var ownKeys = Object.keys(source); | ||
if (typeof Object.getOwnPropertySymbols === 'function') { | ||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { | ||
return Object.getOwnPropertyDescriptor(source, sym).enumerable; | ||
})); | ||
} | ||
ownKeys.forEach(function (key) { | ||
_defineProperty(target, key, source[key]); | ||
}); | ||
} | ||
return target; | ||
} | ||
function _slicedToArray(arr, i) { | ||
@@ -5,6 +39,2 @@ return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); | ||
function _toArray(arr) { | ||
return _arrayWithHoles(arr) || _iterableToArray(arr) || _nonIterableRest(); | ||
} | ||
function _arrayWithHoles(arr) { | ||
@@ -14,6 +44,2 @@ if (Array.isArray(arr)) return arr; | ||
function _iterableToArray(iter) { | ||
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); | ||
} | ||
function _iterableToArrayLimit(arr, i) { | ||
@@ -51,4 +77,2 @@ var _arr = []; | ||
const dist = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2; | ||
const hexToRgb = hex => { | ||
@@ -61,96 +85,126 @@ const h = parseHex(hex); | ||
const hexToOctants = hex => { | ||
const h = parseHex(hex); | ||
const distRgb = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2; | ||
const _Array$from = Array.from({ | ||
length: 3 | ||
}, (_, i) => parseInt(h.slice(2 * i, 2 * i + 2), 16).toString(2).padStart(8, 0)), | ||
_Array$from2 = _slicedToArray(_Array$from, 3), | ||
r = _Array$from2[0], | ||
g = _Array$from2[1], | ||
b = _Array$from2[2]; | ||
const distHex = (a, b) => distRgb(hexToRgb(a), hexToRgb(b)); | ||
return Array.from({ | ||
length: 8 | ||
}, (_, i) => r[i] + g[i] + b[i]); | ||
const rgbToBin = rgb => rgb.map(x => x.toString(2).padStart(8, 0)); | ||
const hexToBin = hex => rgbToBin(hexToRgb(hex)); | ||
const buildOctree = (depth, n = 0) => n > depth ? undefined : { | ||
n, | ||
colors: [], | ||
'000': buildOctree(depth, n + 1), | ||
'001': buildOctree(depth, n + 1), | ||
'010': buildOctree(depth, n + 1), | ||
'011': buildOctree(depth, n + 1), | ||
'100': buildOctree(depth, n + 1), | ||
'101': buildOctree(depth, n + 1), | ||
'110': buildOctree(depth, n + 1), | ||
'111': buildOctree(depth, n + 1) | ||
}; | ||
class ColorOctant { | ||
constructor(path = []) { | ||
this.path = path; | ||
this.colors = []; | ||
let root; | ||
const init = () => { | ||
root = buildOctree(7); // 8 would go down to leaves, but it's too intense for nodejs | ||
}; | ||
const add = cols => { | ||
if (!root) init(); | ||
if (!path.length) { | ||
this.closest = hex => { | ||
const octants = hexToOctants(hex); | ||
let child = this; | ||
if (Array.isArray(cols)) { | ||
return cols.forEach(c => _add(c)); // might want to precmpute rgb to spped up search | ||
} | ||
for (const octant of octants) { | ||
if (!child[octant] || !child[octant].colors.length) break; | ||
child = child[octant]; | ||
} | ||
_add(cols); | ||
}; | ||
if (child.colors.length === 1) return child.colors[0]; | ||
const targ = hexToRgb(hex); | ||
let maxDist = Infinity, | ||
closest; | ||
child.colors.forEach(col => { | ||
const c = hexToRgb(col.hex); | ||
const d = dist(c, targ); | ||
const _add = col => { | ||
let node = root; | ||
const bin = hexToBin(col.hex); | ||
if (d < maxDist) { | ||
maxDist = d; | ||
closest = col; | ||
} | ||
}); | ||
return Object.assign({ | ||
d: maxDist | ||
}, closest); | ||
}; | ||
} | ||
for (let i = 0; i < 7; i++) { | ||
const k = bin[0][i] + bin[1][i] + bin[2][i]; | ||
node = node[k]; | ||
node.colors.push(col); | ||
} | ||
}; | ||
/** | ||
* get neighbors of a given node | ||
* example: neighbors(['0010', '1101', '0101'], '000') will get the 7 neighbors of this node next the firsr corner ('000') | ||
*/ | ||
addAll(colors) { | ||
colors.forEach(({ | ||
hex, | ||
name | ||
}) => this.add({ | ||
hex, | ||
name, | ||
octants: hexToOctants(hex) | ||
})); | ||
const neighbors = ([r, g, b], dir) => { | ||
const rDec = parseInt(r, 2); | ||
const gDec = parseInt(g, 2); | ||
const bDec = parseInt(b, 2); | ||
const nodes = []; | ||
const n = r.length; | ||
for (let dr = dir[0] - 1; dr <= dir[0]; dr++) { | ||
for (let dg = dir[1] - 1; dg <= dir[1]; dg++) { | ||
for (let db = dir[2] - 1; db <= dir[2]; db++) { | ||
const R = rDec + dr, | ||
G = gDec + dg, | ||
B = bDec + db; | ||
if (R >= 0 && R < 2 ** n && G >= 0 && G < 2 ** n && B >= 0 && B < 2 ** n) { | ||
nodes.push([R.toString(2).padStart(n, 0), G.toString(2).padStart(n, 0), B.toString(2).padStart(n, 0)]); | ||
} | ||
} | ||
} | ||
} | ||
add(color) { | ||
if (this.path.length >= 8 || !this.colors.length) { | ||
this.colors.push(color); | ||
return; | ||
} // else we need to go deeper | ||
// get first octant of each color, and put them in corresponding child for this octant | ||
return nodes; | ||
}; | ||
const getNodeFromCoords = ([r, g, b]) => [...r].reduce((node, ri, i) => node[ri + g[i] + b[i]], root); | ||
this.colors.concat(color).forEach((_ref) => { | ||
let hex = _ref.hex, | ||
name = _ref.name, | ||
_ref$octants = _toArray(_ref.octants), | ||
octant = _ref$octants[0], | ||
rest = _ref$octants.slice(1); | ||
const closest = hex => { | ||
const rgb = hexToRgb(hex); | ||
const child = this.child(octant); | ||
child.add({ | ||
hex, | ||
name, | ||
octants: rest | ||
}); | ||
}); | ||
} | ||
const _rgbToBin = rgbToBin(rgb), | ||
_rgbToBin2 = _slicedToArray(_rgbToBin, 3), | ||
r = _rgbToBin2[0], | ||
g = _rgbToBin2[1], | ||
b = _rgbToBin2[2]; // reminder: we don't/can't store level 8 | ||
child(octant) { | ||
if (this[octant]) return this[octant]; | ||
this[octant] = new ColorOctant(this.path.concat(octant)); | ||
return this[octant]; | ||
for (let i = 7; i > 0; i--) { | ||
const coords = [r.slice(0, i), g.slice(0, i), b.slice(0, i)]; // take one resolution higher, since we don't/can't store level 8 | ||
const ns = neighbors(coords, r[i] + g[i] + b[i]); | ||
const colors = ns.reduce((cs, n) => cs.concat(getNodeFromCoords(n).colors), []); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
} // search in all | ||
const colors = ['000', '001', '010', '011', '100', '101', '110', '111'].reduce((cs, c) => cs.concat(root[c].colors), []); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
} | ||
console.log(`Couldn't find any color`); | ||
}; | ||
export default ColorOctant; | ||
const closestIn = (rgb, colors) => { | ||
let minDist = Infinity, | ||
best; | ||
colors.forEach(col => { | ||
const d = distRgb(hexToRgb(col.hex), rgb); | ||
if (d < minDist) { | ||
minDist = d; | ||
best = col; | ||
} | ||
}); | ||
return _objectSpread({}, best, { | ||
d: minDist | ||
}); | ||
}; | ||
export { parseHex, distHex, hexToBin, init, add, closest }; |
241
dist/umd.js
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | ||
typeof define === 'function' && define.amd ? define(factory) : | ||
(global.colorutil = factory()); | ||
}(this, (function () { 'use strict'; | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | ||
typeof define === 'function' && define.amd ? define(['exports'], factory) : | ||
(factory((global.colorutil = {}))); | ||
}(this, (function (exports) { 'use strict'; | ||
function _defineProperty(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
} | ||
function _objectSpread(target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i] != null ? arguments[i] : {}; | ||
var ownKeys = Object.keys(source); | ||
if (typeof Object.getOwnPropertySymbols === 'function') { | ||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { | ||
return Object.getOwnPropertyDescriptor(source, sym).enumerable; | ||
})); | ||
} | ||
ownKeys.forEach(function (key) { | ||
_defineProperty(target, key, source[key]); | ||
}); | ||
} | ||
return target; | ||
} | ||
function _slicedToArray(arr, i) { | ||
@@ -11,6 +45,2 @@ return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); | ||
function _toArray(arr) { | ||
return _arrayWithHoles(arr) || _iterableToArray(arr) || _nonIterableRest(); | ||
} | ||
function _arrayWithHoles(arr) { | ||
@@ -20,6 +50,2 @@ if (Array.isArray(arr)) return arr; | ||
function _iterableToArray(iter) { | ||
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); | ||
} | ||
function _iterableToArrayLimit(arr, i) { | ||
@@ -57,4 +83,2 @@ var _arr = []; | ||
const dist = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2; | ||
const hexToRgb = hex => { | ||
@@ -67,98 +91,135 @@ const h = parseHex(hex); | ||
const hexToOctants = hex => { | ||
const h = parseHex(hex); | ||
const distRgb = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2; | ||
const _Array$from = Array.from({ | ||
length: 3 | ||
}, (_, i) => parseInt(h.slice(2 * i, 2 * i + 2), 16).toString(2).padStart(8, 0)), | ||
_Array$from2 = _slicedToArray(_Array$from, 3), | ||
r = _Array$from2[0], | ||
g = _Array$from2[1], | ||
b = _Array$from2[2]; | ||
const distHex = (a, b) => distRgb(hexToRgb(a), hexToRgb(b)); | ||
return Array.from({ | ||
length: 8 | ||
}, (_, i) => r[i] + g[i] + b[i]); | ||
const rgbToBin = rgb => rgb.map(x => x.toString(2).padStart(8, 0)); | ||
const hexToBin = hex => rgbToBin(hexToRgb(hex)); | ||
const buildOctree = (depth, n = 0) => n > depth ? undefined : { | ||
n, | ||
colors: [], | ||
'000': buildOctree(depth, n + 1), | ||
'001': buildOctree(depth, n + 1), | ||
'010': buildOctree(depth, n + 1), | ||
'011': buildOctree(depth, n + 1), | ||
'100': buildOctree(depth, n + 1), | ||
'101': buildOctree(depth, n + 1), | ||
'110': buildOctree(depth, n + 1), | ||
'111': buildOctree(depth, n + 1) | ||
}; | ||
class ColorOctant { | ||
constructor(path = []) { | ||
this.path = path; | ||
this.colors = []; | ||
let root; | ||
const init = () => { | ||
root = buildOctree(7); // 8 would go down to leaves, but it's too intense for nodejs | ||
}; | ||
const add = cols => { | ||
if (!root) init(); | ||
if (!path.length) { | ||
this.closest = hex => { | ||
const octants = hexToOctants(hex); | ||
let child = this; | ||
if (Array.isArray(cols)) { | ||
return cols.forEach(c => _add(c)); // might want to precmpute rgb to spped up search | ||
} | ||
for (const octant of octants) { | ||
if (!child[octant] || !child[octant].colors.length) break; | ||
child = child[octant]; | ||
} | ||
_add(cols); | ||
}; | ||
if (child.colors.length === 1) return child.colors[0]; | ||
const targ = hexToRgb(hex); | ||
let maxDist = Infinity, | ||
closest; | ||
child.colors.forEach(col => { | ||
const c = hexToRgb(col.hex); | ||
const d = dist(c, targ); | ||
const _add = col => { | ||
let node = root; | ||
const bin = hexToBin(col.hex); | ||
if (d < maxDist) { | ||
maxDist = d; | ||
closest = col; | ||
} | ||
}); | ||
return Object.assign({ | ||
d: maxDist | ||
}, closest); | ||
}; | ||
} | ||
for (let i = 0; i < 7; i++) { | ||
const k = bin[0][i] + bin[1][i] + bin[2][i]; | ||
node = node[k]; | ||
node.colors.push(col); | ||
} | ||
}; | ||
/** | ||
* get neighbors of a given node | ||
* example: neighbors(['0010', '1101', '0101'], '000') will get the 7 neighbors of this node next the firsr corner ('000') | ||
*/ | ||
addAll(colors) { | ||
colors.forEach(({ | ||
hex, | ||
name | ||
}) => this.add({ | ||
hex, | ||
name, | ||
octants: hexToOctants(hex) | ||
})); | ||
const neighbors = ([r, g, b], dir) => { | ||
const rDec = parseInt(r, 2); | ||
const gDec = parseInt(g, 2); | ||
const bDec = parseInt(b, 2); | ||
const nodes = []; | ||
const n = r.length; | ||
for (let dr = dir[0] - 1; dr <= dir[0]; dr++) { | ||
for (let dg = dir[1] - 1; dg <= dir[1]; dg++) { | ||
for (let db = dir[2] - 1; db <= dir[2]; db++) { | ||
const R = rDec + dr, | ||
G = gDec + dg, | ||
B = bDec + db; | ||
if (R >= 0 && R < 2 ** n && G >= 0 && G < 2 ** n && B >= 0 && B < 2 ** n) { | ||
nodes.push([R.toString(2).padStart(n, 0), G.toString(2).padStart(n, 0), B.toString(2).padStart(n, 0)]); | ||
} | ||
} | ||
} | ||
} | ||
add(color) { | ||
if (this.path.length >= 8 || !this.colors.length) { | ||
this.colors.push(color); | ||
return; | ||
} // else we need to go deeper | ||
// get first octant of each color, and put them in corresponding child for this octant | ||
return nodes; | ||
}; | ||
const getNodeFromCoords = ([r, g, b]) => [...r].reduce((node, ri, i) => node[ri + g[i] + b[i]], root); | ||
this.colors.concat(color).forEach((_ref) => { | ||
let hex = _ref.hex, | ||
name = _ref.name, | ||
_ref$octants = _toArray(_ref.octants), | ||
octant = _ref$octants[0], | ||
rest = _ref$octants.slice(1); | ||
const closest = hex => { | ||
const rgb = hexToRgb(hex); | ||
const child = this.child(octant); | ||
child.add({ | ||
hex, | ||
name, | ||
octants: rest | ||
}); | ||
}); | ||
} | ||
const _rgbToBin = rgbToBin(rgb), | ||
_rgbToBin2 = _slicedToArray(_rgbToBin, 3), | ||
r = _rgbToBin2[0], | ||
g = _rgbToBin2[1], | ||
b = _rgbToBin2[2]; // reminder: we don't/can't store level 8 | ||
child(octant) { | ||
if (this[octant]) return this[octant]; | ||
this[octant] = new ColorOctant(this.path.concat(octant)); | ||
return this[octant]; | ||
for (let i = 7; i > 0; i--) { | ||
const coords = [r.slice(0, i), g.slice(0, i), b.slice(0, i)]; // take one resolution higher, since we don't/can't store level 8 | ||
const ns = neighbors(coords, r[i] + g[i] + b[i]); | ||
const colors = ns.reduce((cs, n) => cs.concat(getNodeFromCoords(n).colors), []); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
} // search in all | ||
const colors = ['000', '001', '010', '011', '100', '101', '110', '111'].reduce((cs, c) => cs.concat(root[c].colors), []); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
} | ||
console.log(`Couldn't find any color`); | ||
}; | ||
return ColorOctant; | ||
const closestIn = (rgb, colors) => { | ||
let minDist = Infinity, | ||
best; | ||
colors.forEach(col => { | ||
const d = distRgb(hexToRgb(col.hex), rgb); | ||
if (d < minDist) { | ||
minDist = d; | ||
best = col; | ||
} | ||
}); | ||
return _objectSpread({}, best, { | ||
d: minDist | ||
}); | ||
}; | ||
exports.parseHex = parseHex; | ||
exports.distHex = distHex; | ||
exports.hexToBin = hexToBin; | ||
exports.init = init; | ||
exports.add = add; | ||
exports.closest = closest; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
}))); |
161
index.js
@@ -1,72 +0,123 @@ | ||
const parseHex = hex => hex.length < 6 ? hex[hex.length-3].repeat(2)+hex[hex.length-2].repeat(2)+hex[hex.length-1].repeat(2) : hex.slice(-6); | ||
export const parseHex = hex => | ||
hex.length < 6 | ||
? hex[hex.length - 3].repeat(2) + hex[hex.length - 2].repeat(2) + hex[hex.length - 1].repeat(2) | ||
: hex.slice(-6); | ||
const dist = (c1, c2) => (c1[0]-c2[0])**2 + (c1[1]-c2[1])**2 + (c1[2]-c2[2])**2; | ||
const hexToRgb = hex => { | ||
const h = parseHex(hex); | ||
return Array.from({length:3}, (_,i)=>parseInt(h.slice(2*i, 2*i+2), 16)); | ||
} | ||
return Array.from({ length: 3 }, (_, i) => parseInt(h.slice(2 * i, 2 * i + 2), 16)); | ||
}; | ||
const distRgb = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2; | ||
const hexToOctants = hex => { | ||
const h = parseHex(hex); | ||
const [r,g,b] = Array.from({length:3}, (_,i)=>parseInt(h.slice(2*i, 2*i+2), 16).toString(2).padStart(8,0)) | ||
return Array.from({length:8}, (_,i) => r[i]+g[i]+b[i]); | ||
} | ||
export const distHex = (a, b) => distRgb(hexToRgb(a), hexToRgb(b)); | ||
const rgbToBin = rgb => rgb.map(x => x.toString(2).padStart(8, 0)); | ||
export default class ColorOctant { | ||
constructor(path = []) { | ||
this.path = path; | ||
this.colors = []; | ||
export const hexToBin = hex => rgbToBin(hexToRgb(hex)); | ||
if (!path.length) { | ||
this.closest = hex => { | ||
const octants = hexToOctants(hex); | ||
let child = this; | ||
for (const octant of octants) { | ||
if (!child[octant] || !child[octant].colors.length) break; | ||
child = child[octant] | ||
} | ||
const buildOctree = (depth, n = 0) => | ||
n > depth | ||
? undefined | ||
: { | ||
n, | ||
colors: [], | ||
'000': buildOctree(depth, n + 1), | ||
'001': buildOctree(depth, n + 1), | ||
'010': buildOctree(depth, n + 1), | ||
'011': buildOctree(depth, n + 1), | ||
'100': buildOctree(depth, n + 1), | ||
'101': buildOctree(depth, n + 1), | ||
'110': buildOctree(depth, n + 1), | ||
'111': buildOctree(depth, n + 1), | ||
}; | ||
if (child.colors.length === 1) return child.colors[0]; | ||
const targ = hexToRgb(hex) | ||
let maxDist = Infinity, closest; | ||
child.colors.forEach(col => { | ||
const c = hexToRgb(col.hex); | ||
const d = dist(c, targ); | ||
if (d < maxDist) { | ||
maxDist = d; | ||
closest = col; | ||
} | ||
}); | ||
return Object.assign({d: maxDist}, closest); | ||
} | ||
} | ||
let root; | ||
export const init = () => { | ||
root = buildOctree(7); // 8 would go down to leaves, but it's too intense for nodejs | ||
}; | ||
export const add = cols => { | ||
if (!root) init(); | ||
if (Array.isArray(cols)) { | ||
return cols.forEach(c => _add(c)); // might want to precmpute rgb to spped up search | ||
} | ||
_add(cols); | ||
}; | ||
addAll(colors) { | ||
colors.forEach(({hex, name}) => this.add({hex, name, octants: hexToOctants(hex)})); | ||
const _add = col => { | ||
let node = root; | ||
const bin = hexToBin(col.hex); | ||
for (let i = 0; i < 7; i++) { | ||
const k = bin[0][i] + bin[1][i] + bin[2][i]; | ||
node = node[k]; | ||
node.colors.push(col); | ||
} | ||
}; | ||
add(color) { | ||
if (this.path.length >= 8 || !this.colors.length) { | ||
this.colors.push(color); | ||
return; | ||
/** | ||
* get neighbors of a given node | ||
* example: neighbors(['0010', '1101', '0101'], '000') will get the 7 neighbors of this node next the firsr corner ('000') | ||
*/ | ||
const neighbors = ([r, g, b], dir) => { | ||
const rDec = parseInt(r, 2); | ||
const gDec = parseInt(g, 2); | ||
const bDec = parseInt(b, 2); | ||
const nodes = []; | ||
const n = r.length; | ||
for (let dr = dir[0] - 1; dr <= dir[0]; dr++) { | ||
for (let dg = dir[1] - 1; dg <= dir[1]; dg++) { | ||
for (let db = dir[2] - 1; db <= dir[2]; db++) { | ||
const R = rDec + dr, | ||
G = gDec + dg, | ||
B = bDec + db; | ||
if (R >= 0 && R < 2 ** n && G >= 0 && G < 2 ** n && B >= 0 && B < 2 ** n) { | ||
nodes.push([ | ||
R.toString(2).padStart(n, 0), | ||
G.toString(2).padStart(n, 0), | ||
B.toString(2).padStart(n, 0), | ||
]); | ||
} | ||
} | ||
} | ||
// else we need to go deeper | ||
// get first octant of each color, and put them in corresponding child for this octant | ||
this.colors.concat(color).forEach(({hex, name, octants: [octant, ...rest]}) => { | ||
const child = this.child(octant); | ||
child.add({hex, name, octants: rest}); | ||
}); | ||
} | ||
return nodes; | ||
}; | ||
child(octant) { | ||
if (this[octant]) return this[octant]; | ||
this[octant] = new ColorOctant(this.path.concat(octant)); | ||
return this[octant]; | ||
const getNodeFromCoords = ([r, g, b]) => [...r].reduce((node, ri, i) => node[ri + g[i] + b[i]], root); | ||
export const closest = hex => { | ||
const rgb = hexToRgb(hex); | ||
const [r, g, b] = rgbToBin(rgb); | ||
// reminder: we don't/can't store level 8 | ||
for (let i = 7; i > 0; i--) { | ||
const coords = [r.slice(0, i), g.slice(0, i), b.slice(0, i)]; // take one resolution higher, since we don't/can't store level 8 | ||
const ns = neighbors(coords, r[i] + g[i] + b[i]); | ||
const colors = ns.reduce((cs, n) => cs.concat(getNodeFromCoords(n).colors), []); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
} | ||
} | ||
// search in all | ||
const colors = ['000', '001', '010', '011', '100', '101', '110', '111'].reduce( | ||
(cs, c) => cs.concat(root[c].colors), | ||
[], | ||
); | ||
if (colors.length) { | ||
return closestIn(rgb, colors); | ||
} | ||
console.log(`Couldn't find any color`); | ||
}; | ||
const closestIn = (rgb, colors) => { | ||
let minDist = Infinity, | ||
best; | ||
colors.forEach(col => { | ||
const d = distRgb(hexToRgb(col.hex), rgb); | ||
if (d < minDist) { | ||
minDist = d; | ||
best = col; | ||
} | ||
}); | ||
return { ...best, d: minDist }; | ||
}; |
{ | ||
"name": "color-octree", | ||
"version": "1.1.1", | ||
"version": "2.0.0", | ||
"description": "Get closest hex color", | ||
@@ -5,0 +5,0 @@ "main": "dist/cjs.js", |
## Find efficiently the closest hex color | ||
```js | ||
import colorNames from 'color-names'; | ||
import ColorTree from 'color-octree'; | ||
import colorNames from 'color-names'; | ||
import { add, closest } from 'color-octree'; | ||
// we expect an array of {hex, name} | ||
const entries = Object.entries(colorNames).map(([hex, name]) => ({hex, name})); | ||
const colors = Object.entries(colorNames).map(([hex, name]) => ({ hex, name })); | ||
const tree = new ColorTree(); | ||
tree.addAll(entries); | ||
console.log(tree.closest('5544df')) | ||
add(colors); | ||
console.log(closest('5544df')); | ||
``` | ||
[live example](https://repl.it/@caub/closest-color) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
22962
8
655
15
1