Algorithms
Every dithering algorithm in @ditherkit/core — what they do, when to use each, and what the output looks like.
@ditherkit/core ships seven dithering algorithms. They all take a
Uint8ClampedArray of RGBA pixels and a Color[] palette, and they
all mutate the buffer in place. Each one has different visual
characteristics and different trade-offs.







Grayscale is optional, and depends on your palette. For 2-colour palettes (BW-style) calling
grayscale(pixels)before dithering gives the cleanest result — the output is luminance-only anyway, so collapsing the channels first produces the best error signal. For 3+ colour palettes you must not grayscale first — the algorithms do nearest-colour matching in full RGB space, and flattening the input would collapse every palette down to brightness matching (which is why the Game Boy palette accidentally worked for a while and CGA / PICO-8 didn't). The<DitheredImage />and<DitheredImageSSR />wrappers auto-decide; if you're calling the core functions directly, make the call yourself.
Six of the seven are error-diffusion algorithms — they pick the nearest palette colour for each pixel, then spread the quantisation error across neighbouring pixels using a weighted kernel. They differ only in the shape and size of that kernel. The seventh, Bayer, is an ordered dither: it nudges each pixel by a fixed per-position threshold before quantising, which makes it deterministic, parallelisable, and visually distinct.
Chunky pixels with pixelSize
Every algorithm below can be combined with the pixelSize prop on
<DitheredImage /> and <DitheredImageSSR />. It downsamples the
image before dithering and nearest-neighbour-upscales the result, so
the dither pattern reads as chunky pixels at the display resolution.
pixelSize={1} is off; higher values give you the Game Boy /
fantasy-console look.
The Color type
All seven algorithms take a palette — an array of Color values:
import { type Color } from '@ditherkit/core'
const blackAndWhite: Color[] = [
{ r: 0, g: 0, b: 0 },
{ r: 255, g: 255, b: 255 },
]Palettes can have as many colors as you like. Two-color palettes give the classic 1-bit look; four colors give Game Boy or CGA looks; sixteen colors can approximate photographs recognisably. See Palettes for the built-in presets.
Floyd-Steinberg
floydSteinbergDither(
pixels: Uint8ClampedArray,
width: number,
height: number,
palette: Color[]
): void
Floyd-Steinberg dithering on the sample image, BW palette — the default for photographs.
The classic error-diffusion algorithm (Floyd & Steinberg, 1976). For each pixel, picks the nearest palette color, calculates the quantization error, and spreads that error across four neighbouring pixels with these weights:
X 7/16
3/16 5/16 1/16When to use it. The default for photographs. Floyd-Steinberg is the best balance of visual fidelity and speed for continuous-tone images — portraits, landscapes, anything with smooth gradients. It produces a characteristic directional noise pattern but preserves detail better than most alternatives.
When not to use it. If you want higher contrast or a harder visual look, try Atkinson. For even smoother gradients at the cost of sharpness, reach for Jarvis-Judice-Ninke or Stucki. For the crosshatched "printer" look, use Bayer.
Example
import {
grayscale,
floydSteinbergDither,
type Color,
} from '@ditherkit/core'
const palette: Color[] = [
{ r: 0, g: 0, b: 0 },
{ r: 255, g: 255, b: 255 },
]
grayscale(pixels)
floydSteinbergDither(pixels, width, height, palette)Notes
- Mutates in place. The original pixel values are overwritten. Clone the buffer if you need to keep the original.
- Direction-aware. Errors diffuse forward (right and down), so results depend on traversal order. This is standard Floyd-Steinberg; don't try to parallelise it.
- No alpha handling. The algorithm operates on RGB and leaves alpha untouched.
Atkinson
atkinsonDither(
pixels: Uint8ClampedArray,
width: number,
height: number,
palette: Color[]
): void
Atkinson dithering — harder contrast, the classic early-Macintosh look.
A variation on Floyd-Steinberg that only diffuses six-eighths of the quantization error (vs all of it), with this pattern:
X 1/8 1/8
1/8 1/8 1/8
1/8The "lost" 2/8 of error means the image looks higher-contrast — shadows go darker, highlights go brighter, and midtones cluster near the palette poles.
When to use it. Atkinson is the Bill Atkinson algorithm from the original Macintosh, and it has that look — harder edges, crunchier midtones, a retro feel. Ideal for:
- Black-and-white photographs where you want vintage Mac vibes
- High-contrast source images where gentle diffusion would be too soft
- Logos and illustrations being reduced to a small palette
When not to use it. If you're trying to preserve subtle tonal detail, use Floyd-Steinberg instead. Atkinson deliberately throws away some tonal information.
Example
import {
grayscale,
atkinsonDither,
type Color,
} from '@ditherkit/core'
const macPalette: Color[] = [
{ r: 0, g: 0, b: 0 },
{ r: 255, g: 255, b: 255 },
]
grayscale(pixels)
atkinsonDither(pixels, width, height, macPalette)Jarvis-Judice-Ninke
jarvisJudiceNinkeDither(
pixels: Uint8ClampedArray,
width: number,
height: number,
palette: Color[]
): void
Jarvis-Judice-Ninke — a wider 12-neighbour kernel, smoother gradients than Floyd-Steinberg at the cost of some sharpness.
A larger error-diffusion kernel (Jarvis, Judice & Ninke, 1976) that spreads the error across 12 neighbours instead of Floyd-Steinberg's four:
X 7/48 5/48
3/48 5/48 7/48 5/48 3/48
1/48 3/48 5/48 3/48 1/48The wider footprint produces smoother gradients and less directional noise than Floyd-Steinberg, at the cost of some sharpness and about 3× the compute.
When to use it. Large photographs with subtle gradients (sky, skin, fog) where Floyd-Steinberg's banding shows through. Also good for posters and print work where you'll never need real-time updates.
When not to use it. Small images where the wider kernel can't spread error far enough before hitting an edge. Real-time scenarios where the ~3× cost versus Floyd-Steinberg matters.
Example
import {
grayscale,
jarvisJudiceNinkeDither,
palettes,
} from '@ditherkit/core'
grayscale(pixels)
jarvisJudiceNinkeDither(pixels, width, height, palettes.bw)Stucki
stuckiDither(
pixels: Uint8ClampedArray,
width: number,
height: number,
palette: Color[]
): void
Stucki on the BW palette — error diffusion with crisp midtones.

The same Stucki algorithm rendered through palettes.pico8 (16 colours). No grayscale() call — colour palettes need the source hues to survive into the algorithm.
Stucki (1981) uses the same 12-neighbour footprint as JJN but with weights tuned to preserve more midtone detail:
X 8/42 4/42
2/42 4/42 8/42 4/42 2/42
1/42 2/42 4/42 2/42 1/42Often the best-looking error diffusion on portraits and anything where skin tones matter — the weight concentration keeps edges crisper than JJN without going as contrast-heavy as Atkinson.
When to use it. Portraits, product photography, anything where you'd have reached for Floyd-Steinberg but want a slightly softer noise pattern. Stucki is my default when I'm not sure what to pick.
When not to use it. Hot performance paths — Stucki is the most expensive algorithm in the set after JJN. Use Burkes if you want roughly the same look at twice the speed.
Example
import { grayscale, stuckiDither, palettes } from '@ditherkit/core'
// BW: grayscale-first is optimal because the dither dimension is
// purely luminance.
grayscale(pixels)
stuckiDither(pixels, width, height, palettes.bw)For a colour palette, skip the grayscale call — the algorithm does nearest-colour matching in RGB space and flattening the input would collapse every palette entry to brightness-only matching:
import { stuckiDither, palettes } from '@ditherkit/core'
// No grayscale() — preserve the source colours so Stucki can match
// hue, not just luminance.
stuckiDither(pixels, width, height, palettes.pico8)Burkes
burkesDither(
pixels: Uint8ClampedArray,
width: number,
height: number,
palette: Color[]
): void
Burkes — Stucki's look at about twice the speed (top two rows of the kernel only).
Burkes (1988) dropped the bottom row of Stucki's kernel, keeping just the top two rows:
X 8/32 4/32
2/32 4/32 8/32 4/32 2/32Running roughly twice as fast as Stucki with output that's visually very similar — some extra contrast at the cost of slightly rougher gradients.
When to use it. When Stucki looks right but the budget doesn't stretch to the full kernel. Good in worker pipelines where you're processing many images in a batch.
When not to use it. If you can afford Stucki, use Stucki — the quality difference is small but real on detailed images.
Example
import { grayscale, burkesDither, palettes } from '@ditherkit/core'
grayscale(pixels)
burkesDither(pixels, width, height, palettes.bw)Bayer
bayerDither(
pixels: Uint8ClampedArray,
width: number,
height: number,
palette: Color[],
strength?: number
): void
Bayer on BW — the classic crosshatch pattern. Tileable, deterministic, GPU-friendly.

Bayer through palettes.gameboy — the four-colour LCD look Bayer was built for.
Ordered dithering. Unlike the six error-diffusion algorithms above, Bayer doesn't look at its neighbours at all — it nudges each pixel by a fixed threshold taken from a tiled 8×8 matrix, then quantises to the palette. The nudge amplitude auto-scales to the palette size, so it gives clean results on any palette without tuning.
Because it's local and deterministic, Bayer is:
- Parallelisable. Every pixel is independent — you can run it on the GPU, in a shader, or split across workers.
- Tileable. The 8×8 pattern repeats perfectly, so you can dither a game texture or a website background without seams.
- Stable. Two pixels with the same value and position always get the same output. No noise, no directional artefacts.
When to use it. Game sprites, tileable backgrounds, anything that needs the look of an old printer or a Game Boy game. Also the right pick when you want the dither pattern to be part of the aesthetic rather than something that disappears into the image.
When not to use it. Photographs where you want the dither to be invisible — the crosshatched pattern is never subtle. Reach for Floyd-Steinberg or Stucki instead.
Example
import { bayerDither, palettes } from '@ditherkit/core'
// No grayscale for multi-colour palettes — Bayer picks the nearest
// palette entry in full RGB space so the source colours need to
// survive into the algorithm.
bayerDither(pixels, width, height, palettes.gameboy)The optional fifth parameter tunes the nudge amplitude. The default
(255 / palette.length) is sized to cover exactly one quantisation
step, which is what you usually want — drop it for a softer pattern,
raise it for a harsher one.
Threshold
thresholdDither(
pixels: Uint8ClampedArray,
threshold: number,
palette: Color[]
): void
Threshold — every pixel becomes either the first or second palette colour depending on whether its grayscale value is above or below the threshold. No diffusion, no neighbour reads, no noise.
No error diffusion, no ordered pattern. Every pixel becomes either
the first or second color in the palette depending on whether its
grayscale value is above or below threshold (0–255).
When to use it.
- Pure 1-bit conversions where you want hard edges
- Stamp, stencil, or newspaper-halftone aesthetics
- When you want the fastest possible algorithm (it's O(pixels) with one comparison per pixel and no neighbour reads)
- When the source already has enough contrast to work without diffusion
When not to use it. Photographs with smooth gradients will band severely — use any of the six dithering algorithms above for those.
Example
import { grayscale, thresholdDither, palettes } from '@ditherkit/core'
grayscale(pixels)
thresholdDither(pixels, 128, palettes.bw)Notes
- Only uses the first two palette entries. Extra colors are ignored — the algorithm is inherently binary.
- No
width/heightparameters. Threshold doesn't touch neighbours, so it doesn't need to know the image dimensions. - Threshold tuning matters.
128is middle grey; raise it to emphasise highlights, lower it to emphasise shadows.
Picking an algorithm
A quick decision tree:
- Photograph, default case → Floyd-Steinberg
- Photograph, want a retro Mac vibe → Atkinson
- Portrait where skin matters → Stucki
- Same, but faster → Burkes
- Very smooth gradients, don't care about speed → Jarvis-Judice-Ninke
- Game sprite, tileable background, explicit retro look → Bayer
- 1-bit stamp / stencil / pure hard cut → Threshold