/**
* 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)
}