Dither
Ordered dithering: quantizes to a few tones per channel, but offsets each pixel's rounding by a threshold pattern first. The error becomes texture instead of a visible band — how 8-bit hardware faked gradients.
noise picks where the threshold comes from, and it is the difference between the two looks:
'bayer'(default) — the classic recursive ordered matrix. Its lattice is the look: a visible crosshatch at every density, which reads as retro and is also what can beat against the pixel grid at somescales.'blue'— a 64x64 void-and-cluster blue-noise field. Every threshold level stays homogeneously distributed, so the same tones are reproduced with no lattice and no moire. Reach for this when the dither should read as grain rather than as pattern.

Usage
import { Effects } from 'motion-script';
// Default: 1-bit per channel through a 4×4 matrix
<Image src={'./photo.jpg'} effects={Effects.dither()} />
// Scalar shorthand sets the level count
<Image src={'./photo.jpg'} effects={Effects.dither(4)} />
// Chunky low-res look
<Image src={'./photo.jpg'} effects={Effects.dither({ levels: 2, matrix: 8, scale: 4 })} />
// Same tones, no visible grid
<Image src={'./photo.jpg'} effects={Effects.dither({ levels: 2, scale: 3, noise: 'blue' })} />
// Game Boy: one channel, four tones
<Image src={'./photo.jpg'} effects={Effects.dither({ levels: 4, monochrome: true, scale: 3 })} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'dither' | – | Effect identifier |
levels | number | 2 | Output tones per channel (minimum 2). 2 is pure 1-bit-per-channel |
noise | 'bayer' | 'blue' | 'bayer' | Threshold source. 'blue' removes the visible lattice |
matrix | 2 | 4 | 8 | 4 | Bayer matrix size. Ignored when noise is 'blue' |
scale | number | 1 | Pattern cell size in pixels. Above 1 gives chunky, low-res dither |
monochrome | boolean | false | Dither luminance to grey levels instead of per channel |
mode | 'foreground' | 'backdrop' | 'foreground' | 'backdrop' dithers the content beneath the node |
Bigger matrices carry more distinct threshold levels, so the pattern is finer and the apparent tonal range wider: 2 is a blunt checkerboard, 8 the smooth classic ordered dither. noise: 'blue' is finer still and has no repeat structure to see at all.
Animating
levels and scale interpolate; matrix, monochrome and noise snap at the midpoint (two threshold fields have no meaningful blend).
import { createScene, createRef, Image, Effects, easeInOut } from 'motion-script';
export default createScene(function* (stage) {
const photo = createRef<Image>();
stage.add(<Image ref={photo} src={'./photo.jpg'} effects={Effects.dither({ levels: 64, matrix: 8 })} />);
// Degrade from near-continuous tone down to 1-bit.
yield* photo().to({ effects: Effects.dither({ levels: 2, matrix: 8 }) }, 1.2, easeInOut('quad'));
});
Stacking with other effects
// Game Boy
<Image
src={'./photo.jpg'}
effects={Effects.dither({ levels: 4, monochrome: true, scale: 3 })
.duotone({ shadows: '#0f380f', highlights: '#9bbc0f' })}
/>
Pair it with pixelate first if you want the dither cells aligned to visible pixel blocks rather than to the device grid.
See also
posterizequantizes with no threshold pattern, producing flat bands.halftonealso trades tone for pattern, but as printed dot area rather than as per-pixel rounding.