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 some scales.
  • '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.

Dither effect demo

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

PropTypeDefaultDescription
type'dither'Effect identifier
levelsnumber2Output tones per channel (minimum 2). 2 is pure 1-bit-per-channel
noise'bayer' | 'blue''bayer'Threshold source. 'blue' removes the visible lattice
matrix2 | 4 | 84Bayer matrix size. Ignored when noise is 'blue'
scalenumber1Pattern cell size in pixels. Above 1 gives chunky, low-res dither
monochromebooleanfalseDither 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

  • posterize quantizes with no threshold pattern, producing flat bands.
  • halftone also trades tone for pattern, but as printed dot area rather than as per-pixel rounding.