Beacon color finder

node v14.20.1
version: 1.1.2
endpointsharetweet
/** * Extermely simple beacon-color solver using a greedy algorithm. * * Start with closest color to the wanted, add colors that are closest to * optimal. Stop when we can't do any better. * * Mathematicians among you will know this is not optimal. But I'm hoping * it will be good enough. (Huh, it does fail more on restricted colors. * Use a serious search algo? I don't want to spend all my time on this!) * * To use it, skip to the second cell. Or turn some knobs below. You are * free to change anything: this is licensed under WTFPL v2. */ // This provides some color magic. const oklab = require("@butterwell/oklab") const COLORS = { black: "#1D1D21", red: "#B02E26", green: "#5E7C16", brown: "#835432", blue: "#3C44AA", purple: "#8932B8", cyan: "#169C9C", light_gray: "#9D9D97", gray: "#474F52", pink: "#F38BAA", lime: "#80C71F", yellow: "#FED83D", light_blue: "#3AB3DA", magenta: "#C74EBD", orange: "#F9801D", white: "#F9FFFE", } /** To customize what colors to search for, change this to something like: * available = [ 'black', 'white', 'red', 'blue', 'green' ] */ let available = Object.keys(COLORS) /** * @param {string} s * @returns {number[]} */ function parse_hex(s) { console.assert(s.length === 7) const elements = [s.substring(1, 3), s.substring(3, 5), s.substring(5, 7)] return elements.map((e) => Number.parseInt(e, 16)) } let available_parsed = Object.fromEntries( available.map((k) => [k, parse_hex(COLORS[k])]) ) /** * @param {number[]} c * @returns {string} */ function write_hex(c) { console.assert(c.length === 3) return ( "#" + c .map((e) => e | 0) .map((e) => e.toString(16).padStart(2, "0")) .join("") ) } /** * Find minimum given conversion. * @template T * @param {Iterator<T>} iter * @param {function(T): number} num * @returns {T} */ function min(iter, num) { let min_val = Infinity let min_obj = undefined for (const obj of iter) { const val = num(obj) if (val < min_val) { min_val = val min_obj = obj } } return min_obj } /** * @param {number[]} a * @returns {oklab.rgb} */ function color_to_oklab_rgb(a) { return { r: a[0], g: a[1], b: a[2] } } /** * Oklab + HyAB color distance. * * @param {number[]} a * @param {number[]} b * @returns {number} */ function cdiff(a, b) { const c1 = oklab.toOklab(color_to_oklab_rgb(a)) const c2 = oklab.toOklab(color_to_oklab_rgb(b)) return ( 100 * (Math.abs(c1.L - c2.L) + Math.sqrt((c1.a - c2.a) ** 2 + (c1.b - c2.b) ** 2)) ) } /** * Given table, find closest color in it in the dumb way. * * @returns {string} */ function find_closest_to(c) { return min(available, function (colorname) { const e = available_parsed[colorname] return cdiff(e, c) }) } /** * Blend two colors by averaging -- according to beacon rules. * @param {number[]} a * @param {number[]} b * @returns {number[]} */ function blend(a, b) { return a.map((_, i) => (a[i] + b[i]) / 2) } /** * Find one that * @param {number[]} current * @param {number[]} target * @returns {string} */ function blend_closest_to(current, target) { return min(available, function (colorname) { const e = available_parsed[colorname] return cdiff(blend(e, current), target) }) } /** * @typedef {Object} BlendStep * @property {string} color_name * @property {string} blend_result_color * @property {number} step_error */ /** * @param {string | number[]} c * @returns {BlendStep[]} */ function solve_color(c) { if (typeof c === "string") c = parse_hex(c) console.log(write_hex(c)) /** @type {BlendStep[]} */ let result_table = [] // Initialize. /** @type {string} */ let last_color_addition = find_closest_to(c) /** @type {number[]} */ let last_color = available_parsed[last_color_addition] /** @type {[number, number]} */ let error = [Infinity, cdiff(c, last_color)] result_table.push([last_color_addition, COLORS[last_color_addition], error[1]]) // Iterate. while (error[1] < error[0]) { let new_color_addition = blend_closest_to(last_color, c) let new_color = blend(last_color, available_parsed[new_color_addition]) error = [error[1], cdiff(c, new_color)] result_table.push([new_color_addition, write_hex(new_color), error[1]]) last_color = new_color } return result_table.slice(0, -1) }
// Hi there! Replace the #66CCFF with any color you want. Then press the green "refresh". // To find a color, Google "color picker" and there's a built-in one. solve_color("#66CCFF") // The first output is the color you specified. // The second line contains the set of colors that correspond to your input. // The text describes the color you should apply to the beacon; // the color shows a preview of what color it would be; // and the number is how far it is from the color you specified.
// More colors? Sure! (you may need to click-expand the results) console.log(solve_color('#663399')) console.log(solve_color('#808080')) console.log(solve_color('#ffaa00'))
// What can we get from the traditional base colors? available = [ 'black', 'white', 'red', 'blue', 'green' ] available_parsed = Object.fromEntries( available.map((k) => [k, parse_hex(COLORS[k])]) ) console.log(solve_color('#663399'))
Loading…

no comments

    sign in to comment