My first game was Pokémon Red on the original Game Boy: 160 by 144 pixels, four shades of green. That was all there was, and it still held an entire world. That is where my love of gaming started, and it eventually led me into my job.
The look stuck with me. It is the reason this site looks the way it does.
For the site I wanted my portrait as a sprite, a small pixel graphic like the characters in old games. I could have used an existing tool. But I wanted to understand what actually happens between photo and sprite, and I enjoy pixel art anyway. The result is Spriteworks: a small pixel art converter that runs entirely in the browser, with no upload and no server.
Why shrinking alone is not enough
The obvious approach is to scale the image down to 48 pixels and back up again. The result looks like a blurry photo with jagged edges, not like a sprite.
A sprite has two properties: few pixels and few colours. A downscaled photo only has the first. It still contains thousands of shades with soft transitions, and those give it away.
So Spriteworks works in three steps: shrink the image (downscaling), adjust contrast and colours, then reduce the colours to a small palette.
Downscaling with a box average
A photo with a million pixels has to become a few thousand. The question is which colour each of the new, large pixels gets.
The simplest method is called nearest neighbour: for each new pixel, exactly one pixel is taken from the original and all the others are dropped. That is fast, but a matter of luck. With fine structures like hair or fabric, chance decides whether a light or a dark pixel gets picked. Move the slider one notch and the image looks different.
Spriteworks uses a box average instead: all pixels of the original that fall onto one new pixel are mixed into an average colour. Transparent areas count less, otherwise cut-out subjects would get a dark fringe.
Four colours have to do
A palette is a fixed list of allowed colours. The Game Boy had four, other old systems sixteen. Spriteworks ships the palettes of the Game Boy, NES, PICO-8, C64 and CGA and replaces every colour in the image with the most similar one from the list.
"Similar" is the interesting part. The simplest way is to treat the red, green and blue values like coordinates and measure the distance between two colours. But that matches poorly what we see: the eye reacts to green far more than to blue. There are colour models like CIELAB that are built around human vision, but converting every single pixel is expensive.
So I use redmean, a cheap approximation: green always counts most, and how much red and blue count depends on how red the colour is.
const redMean = (r + p[0]) * 0.5;
const dr = r - p[0];
const dg = g - p[1];
const db = b - p[2];
const distance =
(2 + redMean / 256) * dr * dr +
4 * dg * dg +
(2 + (255 - redMean) / 256) * db * db;If you do not want a fixed palette, Spriteworks builds one from the image itself. The method is called median cut: all colours of the image go into one bucket, which is split again and again at the point where the colours are furthest apart. Each half ends up as one palette colour. That way rare but striking shades do not get lost.
Dithering: gradients from four colours
With four colours, a sky turns into stripes, because there is nothing between the shades.
Dithering is the trick against that: when a colour is missing, you place two existing ones next to each other in alternation, like a chessboard of light and dark pixels. From a little distance the eye blends them into a shade in between. Newspapers have always printed photos this way, with nothing but black dots.
![]()
Spriteworks offers two methods. Bayer lays a fixed 4×4 pattern over the image. That looks regular and very much like old hardware.
Floyd–Steinberg works with error diffusion. An example: a pixel should be mid-green, but the palette only has light and dark. It becomes light, which makes it a little too light. That error is passed on to the neighbours that have not been processed yet: they are calculated slightly darker so that the average works out again. The four numbers in the code are the shares each neighbour receives.
const spread = [
[dir, 0, 7 / 16],
[-dir, 1, 3 / 16],
[0, 1, 5 / 16],
[dir, 1, 1 / 16],
];Two details mattered more than I expected. Every other row runs backwards, otherwise the error always drifts right and produces diagonal streaks. And the error needs decimal places.
Pixel art is not low resolution. It is low resolution on purpose.
Try it yourself
Spriteworks lives at crondung.com/spriteworks. Drop an image in, play with the sliders, save the PNG. The image never leaves your browser.
What gives me the best results:
- A resolution between 32 and 64 pixels; below that a face becomes unreadable
- A subject with a clear outline against a calm background
- PICO-8 for portraits, Game Boy for anything that should look like childhood
- Floyd–Steinberg at around 70 percent; full strength gets too noisy
- Raise contrast and saturation slightly before the palette kicks in
The portrait at the top of the home page was made this way. The PIXELATE button next to it opens the tool with my photo loaded.