# Dither prompt — generic

Paste everything below the line into a coding agent, along with any video clip or
still image. It works for any subject and any colour scheme — the choices that
depend on your material are called out inline as decisions to make, not values to
copy.

If you just want it to run without thinking, the defaults in each block are safe.

---

Build a full-viewport animated hero that renders a source video (or still image)
as a live ordered-dither dot field on an HTML canvas. Do not use WebGL; canvas 2D
is fast enough and far simpler to reason about. I don't need the full website; just the hero. I will design the website later.

## 0. Check the source first

This technique only works on subjects made of **many small discrete elements** — a
flock, a crowd, a field of crops, falling particles, repeated architecture, a
plume of smoke. A solid object (one face, one animal, one smooth mass) turns into
an illegible grey blob, because the dot field has no internal structure to latch
onto. If your source is a solid object, change the source rather than fighting the
settings.

If the source is video, scrub it and confirm the camera is locked off. Nothing
downstream can undo a moving camera, and a drifting subject makes the loop
unusable. If it drifts, regenerate the clip.

Note whether your subject is **dark on a light background** or **light on a dark
background**. This decides the `invert` flag in step 2 and is the most common
thing to get backwards.

## 1. Extract and clean the frames

For video:

```
ffmpeg -i input.mp4 -vf "fps=11,scale=1400:-2" %03d.png
```

Extract at ~11fps, not 24. The dither is a coarse grid, so extra frames buy
nothing visible and cost load time.

**If the source has a border or frame baked into it** (AI image models often add
a painted parchment edge), crop it off before scaling, or it will dither into a
hard rectangle sitting in the middle of your page:

```
ffmpeg -i input.mp4 -vf "fps=11,crop=iw*0.84:ih*0.84:iw*0.08:ih*0.08,scale=1400:-2" %03d.png
```

**If the background carries noise or grain**, snap it flat: walk every pixel and
force anything within a threshold of the background tone to exactly that tone.
Otherwise codec noise renders as a haze of stray dots across the whole panel. Skip
this if your background is already clean.

A still image works too — just feed the same frame repeatedly and skip step 3's
crossfade.

## 2. The dither engine

Per frame:

1. Draw the source into an offscreen buffer, downscaled so one cell covers each
   `pixelSize` block of the output. All subsequent work happens on this small
   buffer, which is what makes it cheap.
2. Convert to luma with Rec. 709 weights: `0.2126 R + 0.7152 G + 0.0722 B`.
3. Auto-level against the 2nd and 98th percentiles of the luma histogram — then
   **pin those levels for the entire sequence**. Recomputing per frame makes the
   dot density visibly pulse. This is the single easiest thing to get wrong.
4. Apply brightness and contrast.
5. **Invert if your subject is dark on a light background**, so the dots land on
   the subject rather than the empty background. Invert the other way if your
   subject is light on dark. This is about the *source*, not about whether your
   page has a dark or light colour scheme — those are independent.
6. Optionally clamp anything below a small floor (~0.02 normalised) to zero. This
   kills residual background texture. Raise it if you still see haze; drop it to 0
   if it is eating real detail.
7. Quantise with an 8×8 Bayer ordered-dither matrix into N tone levels. Build the
   8×8 by recursing a 4×4 into each quadrant. Bayer gives the characteristic
   regular grid; error-diffusion (Floyd–Steinberg) gives a looser organic scatter
   if you prefer that.
8. Draw each cell as a filled circle whose radius scales with its tone level.
   Bucket cells by quantised colour before drawing, so you set `fillStyle` a few
   dozen times per frame instead of once per dot. Squares also work and read more
   graphic; dots read softer.

### Parameters — the ones that actually matter

```
pixelSize   6–14   cell size in output pixels. Bigger = coarser, more graphic.
spacing     0.3–0.5  fraction of each cell left as gap
dotScale    0.7–0.9  dot radius as a fraction of the remaining cell
levels      2–6    tone steps. 2 = hard 1-bit. 6 = smooth gradation.
contrast    20–35
brightness  −10–+5
```

**Airiness is cell size against dot fill, not dot count.** Big cells with small
dots leave background visible between every dot and read as delicate and
atmospheric. Small cells with full-size dots pack into a solid mass. If it looks
heavy and flat, raise `pixelSize` and lower `dotScale` together — do not just add
more dots.

### Colour — pick one of three approaches

- **Sampled.** Take each dot's colour from the source pixel. Preserves the
  original artwork's palette. Use when the source colour *is* the appeal.
- **Duotone.** Ignore source colour entirely; draw every dot in one flat ink.
  Boldest and most graphic. Use for poster-like or risograph looks.
- **Blended.** Sample, then mix toward a scheme colour by a `colorMix` factor.
  High mix (0.6–1.0) keeps the artwork's own palette; low mix (0.1–0.3) forces it
  into your brand colour while retaining some internal variation.

Blended at a low mix is usually the right default for a branded page: the subject
keeps its structure but the page keeps its identity.

## 3. Animation and looping

Play frames at the rate you extracted (11fps).

**Most natural motifs have no true loop point.** Turbulent subjects — flocks,
smoke, falling particles, wheeling light — never return to a previous state, so
cutting from the last frame back to the first always visibly jumps. Do not spend
time hunting for a seam that does not exist.

Instead, **crossfade the wrap**: over the final ~8 frames, blend the opening
frames back in with rising alpha, so the sequence dissolves into its own start.
Guard the blend index against running past the end of the array when the sequence
is short — with a single still there is no tail to blend at all.

(If your subject *is* genuinely cyclic — a walk cycle, a rotation — then a real
loop point exists. Score every candidate window by how closely its last frame
matches its first, *relative to the average step between adjacent frames*. A seam
below ~1.0× the average step is invisible. Scoring the raw seam alone finds loops
that close but still jump.)

## 4. Pointer interaction

Displace the **sampling coordinates** of each cell, not the drawn output. The warp
then costs nothing extra per frame, because it happens while reading the
already-downscaled buffer.

Give it a wide radius (~60% of the canvas) and a soft eased falloff, so it reads
as a field effect rather than a cursor-shaped bubble tracking the mouse.

Match the motion to the subject:

- **Directional lean** (cells lean away horizontally, lift slightly) — for wind
  crossing a field, falling particles, anything with a natural direction.
- **Rotational twist** about the cursor — for radial subjects: a rose window, a
  vortex, a spiral.
- **Gentle scatter** — for swarms and crowds.

**Avoid radial ripple.** On a natural motif it reads as a lens artefact sitting on
top of the image rather than something happening to the subject itself. It is the
most obvious choice and almost always the wrong one.

## 5. Page composition

Full-bleed canvas behind everything. Headline and CTA over it.

**Legibility over a dot field** is the main design problem, and there are three
ways to solve it — pick per design:

1. **Gradient scrim.** A directional gradient over the artwork, heaviest on the
   side carrying the type. Cheapest and most flexible.
2. **Blend mode.** On light grounds, set the type to `mix-blend-mode: multiply` so
   dots show through the letterforms instead of punching a hole in the field. This
   looks excellent but only works on light backgrounds — on dark grounds skip it
   and rely on colour contrast.
3. **Solid backing panel.** For centred copy over a dense dither, put a solid
   panel behind the text block. Necessary when the dither is heavy (low `levels`,
   high `dotScale`); unnecessary when it is airy.

Set the canvas background to your scheme's ground colour so the "empty" cells are
your background, not white.

## 6. Sanity checks before you ship

- Does the dot density stay constant across the whole loop? If it pulses, the
  auto-levels are not pinned.
- Is there a visible jump at the wrap? If so, lengthen the crossfade.
- Can you read the headline at every point in the animation, not just the frame
  you happened to screenshot?
- Does the subject still read as itself at your chosen `pixelSize`? Coarser is
  more striking but there is a point where the subject dissolves.
- Does it still look right on a narrow viewport, where the canvas is much smaller
  relative to `pixelSize`? Small canvases usually need a finer grid.
