Recolor Images With Neovim Palettes
This is probably a symptom of something undiagnosed.
It's such a minor thing, but I can't stand it when my terminal colors don't match my Neovim colorscheme. Maybe this is the sort of thing that doesn't bother you at all. If so, good. It shouldn't bother you. But it does bother me.
So, whenever I choose a new Neovim colorscheme, I have to configure my terminal with the same palette. And, luckily, these days more colorschemes include a Ghostty palette for me to copy over.
Okay. So, my terminal and Neovim colors have to match. But... I've taken it a step further. This is probably Catppuccin's fault. When that theme became popular, there were Catppuccin wallpapers everywhere, and I couldn't help but notice how good they looked. So, slowly, I started always wanting my wallpaper to match my Neovim colorscheme. But this was a challenge. Most colorschemes do not provide a bunch matching wallpapers.
Let me show you what I mean
Look at this first image.

None of the colors match. Neovim, the terminal, and the wallpaper are all using different palettes. If this doesn't bother you, great. That's healthy. It drives me crazy.
Ok, now look at this image.

Here, all the colors match. The wallpaper, the terminal, Neovim... they feel like they were designed together.
A tool
I decided to build myself a tool that would take care of this problem for me: Millanc. Millanc is a single JavaScript file that converts images so that they adhere to a Neovim colorscheme.
A few key things about Millanc:
- It runs entirely in the browser.
- It can be dropped into any webpage that provides a host
#millanc
div. - It uses web workers so that the image processing run quickly across multiple cpu cores.
- It's vanilla, through-and-through. No dependencies. No build steps. Just the code you see.
How it works
Converting an image to a Neovim colorscheme palette is pretty simple. You iterate through the image one pixel at a time. For each pixel, you ask "what color from the Neovim colorscheme palette is this pixel's color closest to?". Then, you convert the pixel to that color and move on to the next pixel.
But how do you know if two colors are similar or not? My first implementation was naive and used the distance between two hex codes.
Say you have #A4FF16
and #1199B0
. You can find the difference as follows:
-
Convert hex to RGB decimal:
Split each hex color into its red, green, and blue components and convert them from hexadecimal to decimal:
- #A4FF16 → R: 164, G: 255, B: 22
- #1199B0 → R: 17, G: 153, B: 176
-
Compute the distance between the two colors:
Treat each RGB component as a point in 3D space and use the distance formula:
√((17-164)² + (153-255)² + (176-22)²) ≈ 230.2
But there's an issue with comparing RGB. The distance between two colors doesn't
feel
right to the human eye. So, instead we can use a different color space called
OKLCH
in which the distance between two points feels like the correct color difference to the human eye. You can read more about the difference between RBG and OKLCH here.
There are a few additional complexities.
If an image is 2500
by 1500
, that's 3,750,000
pixels. If you could convert 20,000
pixels per second, that image would still take three minutes. Too slow. So, the conversion has to be quick. The hot path has to do as little as possible.
Also, it's important to do things in parallel. Web workers allow you to chunk pieces of work so that they can be run off the main thread. This isn't just concurrent, it's actually parallel. The image conversion can be 7 times faster if it runs on 7 cores.
I'm not a graphics person
While Millanc works, it's not perfect. Some image conversions look extremely posterized. Sometimes the conversion doesn't feel like it belongs to the color palette.
But I'm glad I put in some time to make this tool.
Let me know if you use it.