I’m a designer. I don’t write shaders. Or a minimum of, I didn’t.
However I stored seeing these dithered pictures in all places—that crunchy, pixelated texture that feels each outdated and new. And I wished to make my very own. Not by working pictures by some filter, however in real-time, on 3D fashions, with controls I might tweak.
My first experiment was really for Lummi, the place I used v0 to prototype an results instrument. It was hacky and restricted, nevertheless it labored effectively sufficient that I bought hooked.
So I began constructing Efecto. What began as a fast experiment stored increasing as I examine totally different algorithms and bought inquisitive about how they labored.
I couldn’t have finished any of this with out the work others have shared. Shadertoy was the place I discovered by studying different individuals’s code. The Ebook of Shaders by Patricio Gonzalez Vivo taught me the basics. And libraries like postprocessing and React Three Fiber gave me one thing to construct on.
That is what I discovered alongside the best way.

Beginning with dithering
Dithering is a method that creates the phantasm of extra colours than you even have. When you solely have black and white pixels, you’ll be able to’t present grey. However if you happen to organize black and white pixels in a sample, your mind blends them collectively and perceives grey.
The approach comes from newspapers. Earlier than digital something, printers had to determine easy methods to reproduce pictures utilizing solely black ink on white paper. Their resolution was halftones: tiny dots of various sizes that trick your eye into seeing steady shades.

The digital model of this began in 1976 with a paper by Robert Floyd and Louis Steinberg. Their perception: if you spherical a pixel to the closest out there colour, you get an “error” (the distinction between what you wished and what you bought). As an alternative of throwing that error away, you’ll be able to unfold it to neighboring pixels. This creates natural patterns as an alternative of harsh bands.
Right here’s the fundamental concept in code:
// For every pixel...
const [r, g, b] = getPixel(x, y)
// Discover the closest colour in our palette
const [qR, qG, qB] = findNearestColor(r, g, b, palette)
// Calculate the error
const errR = r - qR
const errG = g - qG
const errB = b - qB
// Unfold that error to neighbors (Floyd-Steinberg weights)
addError(x + 1, y, errR * 7/16, errG * 7/16, errB * 7/16)
addError(x - 1, y + 1, errR * 3/16, errG * 3/16, errB * 3/16)
addError(x, y + 1, errR * 5/16, errG * 5/16, errB * 5/16)
addError(x + 1, y + 1, errR * 1/16, errG * 1/16, errB * 1/16)
The weights (7/16, 3/16, 5/16, 1/16) add as much as 1, so that you’re redistributing 100% of the error. The uneven distribution prevents seen diagonal patterns.
Strive dithering with the unique Floyd-Steinberg error diffusion algorithm from 1976.
Different algorithms
As soon as I bought Floyd-Steinberg working, I wished to strive others. Every algorithm distributes error in another way, which creates totally different textures:
Atkinson (1984) was created by Invoice Atkinson for the unique Macintosh, which might solely show black or white. His trick: solely distribute 75% of the error. This creates increased distinction pictures with a barely “crunchy” high quality.
const atkinson = {
kernel: [
[1, 0, 1], // proper
[2, 0, 1], // two proper
[-1, 1, 1], // bottom-left
[0, 1, 1], // backside
[1, 1, 1], // bottom-right
[0, 2, 1], // two under
],
divisor: 8, // 6 neighbors × 1 = 6, however divisor is 8
}
Discover how solely 6/8 of the error will get distributed. That “misplaced” 25% is what offers Atkinson its distinctive look.
Strive dithering with the Invoice Atkinson’s algorithm from the unique Macintosh.
Jarvis-Judice-Ninke spreads error to 12 neighbors throughout 3 rows. It’s slower however produces smoother gradients:
Strive the Jarvis-Judice-Ninke 12-neighbor algorithm for ultra-smooth gradients.
I ended up implementing 8 totally different algorithms. Every has its personal character. Which one appears finest is dependent upon the picture.
Including colour
Two-color dithering (black and white) is traditional, however multi-color palettes open up extra choices. Efecto contains 31 preset palettes organized into classes: traditional terminal colours, heat tones, cool tones, neon/synthwave, earth tones, and monochrome. You may as well create customized palettes with 2-6 colours.
The Sport Boy had 4 shades of inexperienced. That’s it. However artists made memorable video games inside these constraints. The restricted palette compelled creativity.
Strive the traditional Sport Boy 4-color palette from 1989.
The palette you select fully adjustments the temper. Heat palettes really feel nostalgic, neon feels cyberpunk, monochrome seems like outdated print.

Efecto maps colours utilizing luminance. First, calculate the brightness of every pixel:
const luminance = 0.299 * r + 0.587 * g + 0.114 * b
Then map that brightness to a palette index. Palettes are ordered from darkish to mild, so a darkish pixel picks colours from the beginning of the palette, shiny pixels from the top:
const index = Math.flooring(luminance * palette.size)
const colour = palette[Math.min(index, palette.length - 1)]
This implies palette order issues. Flip the colours round and also you get an inverted picture.
There’s additionally a pixelation management (block measurement 1-10) that processes the picture in chunks slightly than particular person pixels. Greater values offer you that chunky, low-res look. The error diffusion nonetheless works, nevertheless it spreads between block facilities as an alternative of particular person pixels.
Strive the Synthwave palette with pink, purple, and cyan gradients.
The bloom trick
I wished to simulate how CRT screens appeared, and bloom turned out to be the important thing. Dithering creates high-contrast pixel patterns. Bloom makes shiny pixels glow into darkish ones, softening the cruel edges whereas retaining the dithered texture.
Apply a inexperienced monochrome look with a CRT-style glow and dithering with bloom.
Then I wished ASCII
After getting dithering to work, I bought inquisitive about ASCII artwork. Identical primary concept (signify brightness with patterns) however utilizing textual content characters as an alternative of pixel preparations.

The problem: shaders don’t have fonts. You’ll be able to’t simply name drawText(). All the things must be math.
The answer is to attract characters procedurally on a 5×7 pixel grid. Every character turns into a perform that returns 1 (crammed) or 0 (empty) for any place:
// A colon: two dots vertically centered
if (grid.x == 2.0 && (grid.y == 2.0 || grid.y == 4.0)) {
return 1.0;
}
return 0.0;
// An asterisk: middle + arms + diagonals
bool middle = (grid.x == 2.0 && grid.y == 3.0);
bool vert = (grid.x == 2.0 && (grid.y >= 2.0 && grid.y <= 4.0));
bool horiz = (grid.y == 3.0 && (grid.x >= 1.0 && grid.x <= 3.0));
bool diag1 = ((grid.x == 1.0 && grid.y == 2.0) || (grid.x == 3.0 && grid.y == 4.0));
bool diag2 = ((grid.x == 1.0 && grid.y == 4.0) || (grid.x == 3.0 && grid.y == 2.0));
return (middle || vert || horiz || diag1 || diag2) ? 1.0 : 0.0;
The shader divides the display screen right into a grid of cells. For every cell, it:
- Samples the colour on the cell middle
- Calculates brightness
- Picks a personality based mostly on that brightness
Darker areas get denser characters (@, #, 8), lighter areas get sparser ones (., :, house).
float brightness = dot(cellColor.rgb, vec3(0.299, 0.587, 0.114));
These numbers (0.299, 0.587, 0.114) come from how human eyes understand colour. We’re most delicate to inexperienced, then crimson, then blue. This provides perceptually correct grayscale.

Efecto has 8 totally different ASCII kinds. Every makes use of a distinct character set and association:

CRT results
Each dithering and ASCII evoke early computing, so I added some publish results to finish the look:
Scanlines are horizontal darkish bands that simulate CRT phosphor rows.
Display curvature mimics the curved glass of outdated screens:
vec2 centered = uv * 2.0 - 1.0;
float dist = dot(centered, centered);
centered *= 1.0 + curvature * dist;
uv = centered * 0.5 + 0.5;
This pushes pixels outward from the middle, extra on the edges. Simple arithmetic, convincing impact.
Chromatic aberration barely separates RGB channels, like low-cost optics.
Vignette darkens the perimeters, drawing focus to the middle.
Mixed with a inexperienced phosphor or amber palette, the entire thing seems like an outdated terminal.

How Efecto is constructed
Dithering runs on the CPU. Error diffusion is inherently sequential since every pixel is dependent upon beforehand processed pixels. The precise dithering algorithm runs in JavaScript, processing pixel information in reminiscence. WebGPU handles texture administration and the bloom impact (which is GPU-accelerated). When WebGPU isn’t out there (like in Firefox), there’s a Canvas 2D fallback.
ASCII runs as a WebGL shader. Not like dithering, every cell is unbiased, so it might probably run totally on the GPU. The shader is constructed with Three.js and the postprocessing library. Characters are generated procedurally in GLSL, not from bitmap fonts.
Some results are heavy. Advanced shaders with a number of post-processing can drop body charges considerably, particularly on older {hardware}. This can be a tradeoff between visible complexity and efficiency.
Strive it
Listed below are some beginning factors:






What I discovered
Historic algorithms maintain up. Floyd-Steinberg from 1976 continues to be top-of-the-line. The unique papers are value studying.
Constraints drive creativity. Working inside technical limitations forces totally different options. Shaders can’t use fonts, so characters must be drawn as math. Error diffusion can’t parallelize simply, so it runs on the CPU whereas bloom runs on the GPU.
The small print matter. These luminance weights (0.299, 0.587, 0.114) exist as a result of somebody studied how human imaginative and prescient works. The uneven error distribution in Floyd-Steinberg exists as a result of somebody seen diagonal artifacts. These small selections compound.
If you wish to dig deeper:
Papers:
Studying sources:
Libraries I constructed on:
And if you happen to construct one thing with these strategies, I’d like to see it.









