Find closest match for a given color?

Hi, I just had this idea pop into my mind but I'm not sure how I could approach this problem, and before I go spend a few hours, days... or weeks researching this I wanted to ask around for some pointers. Probably some tool already exists as well but I don't know how to even search for it, although I'm still interested in learning how to build it myself as practice. So, the goal is: provided a valid CSS color, determine the closest it gets to other two color values. For example, say I have this value #eebebe, and I want to find out which of these other two values it approaches the most; #e5c890 or #ef9f76?
10 Replies
Joao
Joao•2y ago
That's the general idea, later it can be extended to include other color formats (rgb, hsl, ...) and multiple options to compare to.
MarkBoots
MarkBoots•2y ago
I dont have an immediate answer yet, but you also have to question "closest to what?" Just the hex value itself (as a number), a specific rgb-channel, hue or saturation or lightness?
Jochem
Jochem•2y ago
I'd recommend starting with HSL, cause it most closely mimics our experience of colors. But yeah, what Mark said, you need to specify a distance function, there's not one simple answer to "closest"
WillsterJohnson
WillsterJohnson•2y ago
I think I would use distance in 3D space, but do it with the HSL values rather than RGB. If we let out x axis be saturation, y be lightness, and z be hue, we can just plug the numbers in and be done with it. But this approach will result in a hue of 359 and a hue of 0 being maximally distant, whereas realistically they're right next to eachother. To solve this we could mess around with 3D polar coordinates or fancy math but a simple solution is to just have it max out at 180 and anything above counts backwards (eg 185 -> 175, 200 -> 160, etc);
function hueToAxis(hue: number) {
const ensuredHue = ensureHue(hue); // eg 365 -> 5, -10 -> 350, etc
if (ensuredHue <= 180) return ensuredHue;
const difference = ensuredHue - 180;
return 180 - difference;
}
function hueToAxis(hue: number) {
const ensuredHue = ensureHue(hue); // eg 365 -> 5, -10 -> 350, etc
if (ensuredHue <= 180) return ensuredHue;
const difference = ensuredHue - 180;
return 180 - difference;
}
There's probably a simple math expression which will do this but I'm not the guy to figure it out. You could also have this scale from 0-180 to 0-100 for consistent boundary sizes on all three dimensions, but that's down to what works best in experiments/tests. Then it's a case of using 3D distance formula
type Coords = Record<"x"|"y"|"x", number>;

// d = ((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2)^1/2
function distance(p1: Coords, p2: Coords) {
const xDiff = (p2.x - p1.x) ** 2;
const yDiff = (p2.y - p1.y) ** 2;
const zDiff = (p2.z - p1.z) ** 2;
const sum = xDiff + yDiff + zDiff;
return sum ** 0.5;
}

function whichIsCloser(source: HSL, c1: HSL, c2: HSL) {
const coordsSource = { x: source.sat, y: source.lit, z: hueToAxis(source.hue) };
// coordsC1 & coordsC2 the same

const toC1 = distance(coordsSource, coordsC1);
const toC2 = distance(coordsSource, coordsC2);

if (toC1 < toC2) return c1;
if (toC2 < toC1) return c2;
// they're equal, now what?
return /* ??? */
}
type Coords = Record<"x"|"y"|"x", number>;

// d = ((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2)^1/2
function distance(p1: Coords, p2: Coords) {
const xDiff = (p2.x - p1.x) ** 2;
const yDiff = (p2.y - p1.y) ** 2;
const zDiff = (p2.z - p1.z) ** 2;
const sum = xDiff + yDiff + zDiff;
return sum ** 0.5;
}

function whichIsCloser(source: HSL, c1: HSL, c2: HSL) {
const coordsSource = { x: source.sat, y: source.lit, z: hueToAxis(source.hue) };
// coordsC1 & coordsC2 the same

const toC1 = distance(coordsSource, coordsC1);
const toC2 = distance(coordsSource, coordsC2);

if (toC1 < toC2) return c1;
if (toC2 < toC1) return c2;
// they're equal, now what?
return /* ??? */
}
I'd recommend experimenting with various different color spaces to see which works best for you. Maybe using RGB as XYZ works best - maybe CMYK (probably not but you get the idea)
Joao
Joao•2y ago
@markboots. @jochemm Right, I thought about how to define closest but couldn't really come up with something. Based on a few examples I remember from the sass documentation, I would probably start with things like lightness, etc. But I'm absolutely clueless about it. This looks great! I'll have to take some time later to take a look at it more properly Thanks for the suggestions, any ideas are more than welcome Also, to make things even more challenging, a scss option is preferred Though I'm not sure if this is even possible 😄
WillsterJohnson
WillsterJohnson•2y ago
if you know the colors at build time you can do it in SASS, but if not I don't think it's possible (unless there's CSS magic it compiles to that can do it?) And if you know the colors at build time... well, compute it ahead of time and then use the result instead of a calculation
Joao
Joao•2y ago
Mmm well, one thing where I thought this could be useful is when selecting themes on a website. For instance being able to select not only light/dark themes but also accent colors. So I guess there's a case for JavaScript right there, but I would also like to have that as a sass function. The question is how to calculate this
Kevin Powell
Kevin Powell•2y ago
how to calculate it is a very complex question that no one seems to be able to agree on, lol. If you look at the contrast checkers online right now, they use a formula created by WCAG (I think they made it anyway, don't quote me on that) that basically no one actually likes. CSS is looking to add color-contrast() where you can give it a base color, and then a list of other colors, and it'll take the highest contrast one. It's hit a roadblock because of all the arguing over what algorithm to use to calculate the highest contrast.
Joao
Joao•2y ago
I see, so this is an even more fundamental issue with colors? maybe I should just drop it... which is why I wanted to ask first 😂
WillsterJohnson
WillsterJohnson•2y ago
what algorithm to use
Let the vendors decide the default, but require that several be available and selected by an optional first argument to color-contrast(). easy!