I’m a designer. I don’t write shaders. Or a minimum of, I didn’t.
However I saved seeing these dithered pictures all over the place—that crunchy, pixelated texture that feels each previous 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 software. It was hacky and restricted, nevertheless it labored effectively sufficient that I received hooked.
So I began constructing Efecto. What began as a fast experiment saved increasing as I examine totally different algorithms and received 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 realized by studying different folks’s code. The E book 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. In the event you solely have black and white pixels, you possibly can’t present grey. However for those who prepare black and white pixels in a sample, your mind blends them collectively and perceives grey.
The method comes from newspapers. Earlier than digital something, printers had to determine the right way to reproduce images utilizing solely black ink on white paper. Their answer 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: while you spherical a pixel to the closest obtainable colour, you get an “error” (the distinction between what you wished and what you bought). As an alternative of throwing that error away, you possibly can unfold it to neighboring pixels. This creates natural patterns as a substitute of harsh bands.
Right here’s the essential thought 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.
Attempt dithering with the unique Floyd-Steinberg error diffusion algorithm from 1976.
Different algorithms
As soon as I received Floyd-Steinberg working, I wished to attempt 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 larger 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 beneath
],
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 provides Atkinson its distinctive look.
Attempt 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:
Attempt 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 greatest 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 can even 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.
Attempt 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 looks like previous 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 gentle, 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 dimension 1-10) that processes the picture in chunks somewhat than particular person pixels. Larger values offer you that chunky, low-res look. The error diffusion nonetheless works, nevertheless it spreads between block facilities as a substitute of particular person pixels.
Attempt 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 conserving 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 received inquisitive about ASCII artwork. Similar fundamental thought (characterize brightness with patterns) however utilizing textual content characters as a substitute of pixel preparations.

The problem: shaders don’t have fonts. You’ll be able to’t simply name drawText(). The whole lot must be math.
The answer is to attract characters procedurally on a 5×7 pixel grid. Every character turns into a operate that returns 1 (stuffed) 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: heart + arms + diagonals
bool heart = (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 (heart || vert || horiz || diag1 || diag2) ? 1.0 : 0.0;
The shader divides the display right into a grid of cells. For every cell, it:
- Samples the colour on the cell heart
- Calculates brightness
- Picks a personality primarily based on that brightness
Darker areas get denser characters (@, #, 8), lighter areas get sparser ones (., :, area).
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 types. Every makes use of a distinct character set and association:

CRT results
Each dithering and ASCII evoke early computing, so I added some submit results to finish the look:
Scanlines are horizontal darkish bands that simulate CRT phosphor rows.
Display screen curvature mimics the curved glass of previous 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. Basic math, convincing impact.
Chromatic aberration barely separates RGB channels, like low-cost optics.
Vignette darkens the sides, drawing focus to the middle.
Mixed with a inexperienced phosphor or amber palette, the entire thing looks like an previous 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 knowledge in reminiscence. WebGPU handles texture administration and the bloom impact (which is GPU-accelerated). When WebGPU isn’t obtainable (like in Firefox), there’s a Canvas 2D fallback.
ASCII runs as a WebGL shader. Not like dithering, every cell is impartial, so it might 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. Complicated shaders with a number of post-processing can drop body charges considerably, particularly on older {hardware}. It is a tradeoff between visible complexity and efficiency.
Attempt it
Listed here are some beginning factors:






What I realized
Historic algorithms maintain up. Floyd-Steinberg from 1976 remains to be probably the greatest. The unique papers are value studying.
Constraints drive creativity. Working inside technical limitations forces totally different options. Shaders can’t use fonts, so characters should 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 choices compound.
If you wish to dig deeper:
Papers:
Studying sources:
Libraries I constructed on:
And for those who construct one thing with these strategies, I’d like to see it.


