Animating Halftone: Dot Patterns on GIFs Without Flicker

EFFECT EXPLAINERDESIGNER

Animated halftone GIFs shimmer because the dot pattern is recomputed from scratch every frame. Here's why an error-diffusion halftone flickers, why a fixed…

01/ ARTICLE

What causes flicker in an animated halftone GIF?

Flicker in an animated halftone GIF isn't the source moving -- it's the dot pattern getting recomputed from scratch on every frame. ezgif's halftone tool shows the mechanism plainly: it applies your chosen pattern to every frame of an animated GIF, APNG or WebP and keeps the original timing, but there's no frame-to-frame consistency control anywhere in the settings. Each frame gets its own independent pass.

That's harmless if the algorithm always produces the same output for the same input pixel. It's not harmless with error diffusion, which spreads each pixel's rounding error into its neighbors. Two frames can be 99% identical and still come out of an error-diffusion halftone looking noticeably different, because the diffusion path depends on decisions that compound across the whole frame, well beyond the handful of pixels that actually changed.

Halftone methodFrame-to-frame stabilityVisible artifact on a loopGIF compressionBest use
Ordered / Bayer (fixed matrix)Identical output for identical input pixels, every frameNone -- dot grid holds still10-25% smaller under LZW (turbodither.com)Loops, static-camera shots, anything held on screen
Error diffusion (Floyd-Steinberg, Atkinson, Stucki, Sierra Lite)Recomputed per frame from each pixel's local rounding errorShimmer / "swarming" dots, even on unchanged regionsLarger, noisierSingle stills only -- avoid on anything animated

From here: why that gap exists, why deliberately varying the pattern -- the obvious counter-move -- actually makes things worse on a GIF, and which settings hold a halftone dot grid still across a loop.

Why does ordered (Bayer) halftone stay stable across frames?

Ordered dithering -- usually a Bayer matrix -- skips error diffusion entirely and compares every pixel against a fixed, repeating threshold grid instead. The comparison only depends on the pixel's own value and its position in that fixed matrix, so identical input always produces identical output. Frame number simply doesn't enter the math. That's the whole reason a Bayer halftone doesn't shimmer where error diffusion does (turbodither.com).

Matrix size only changes what that stability looks like, never whether it holds. A denser 4x4 grid resolves finer tonal detail, but it reads busier once things move; go with 8x8 and you get a coarser, calmer dot that holds up better against a lot of source motion. Picking a matrix size for something other than animation? The 4x4 vs 8x8 Bayer matrix breakdown covers the full threshold-level tradeoffs.

It's the same fixed-matrix-vs-diffusion logic already covered for straight dithering in motion, in Dithering in Motion: Keeping the Pattern Stable Across Video Frames. That piece deals with dither families generally -- Floyd-Steinberg, ordered, blue noise -- applied to video. Halftone is its own case: a dot or rosette grid rather than a per-pixel dither pattern. But the cause of the instability, and the fix, don't change.

Why doesn't cycling the halftone pattern fix flicker on a GIF?

There's a real, published technique that does the opposite of holding the pattern fixed. US Patent 7187474 describes deliberately rotating or swapping in a different ordered-dither matrix on consecutive frames -- leaning on the eye's temporal integration to blend the sequence into a smoother apparent tone, instead of reading one fixed dot pattern as static texture.

The patent names its own limit directly: "temporal halftoning may produce no visual artifacts when implemented on a display with a refreshing rate higher than human perception can sense; however, if a display's refresh rate is not high enough, the flickering effect will be very noisy." That's the flicker-fusion threshold: roughly the point where separate frames stop registering as separate events and start blending into one.

A GIF loop runs at a handful of frames per second -- nowhere near that threshold. Try the cycling-pattern technique on a GIF and you don't get the smooth perceptual average it's built for. You get exactly the frame-to-frame pattern change it's supposed to hide, just deliberate instead of accidental. It's a genuine fix. It's just borrowed from the wrong display context.

What settings keep an animated halftone GIF stable?

Palette and contrast. Keep the color palette to 4-16 colors. A narrower palette is simply easier to hold stable across frames than a broad one. Raise contrast before the halftone step, too -- stronger tonal separation matters because flicker shows up first in the mid-tones, and a harder light/dark split leaves the algorithm less room to waver on a borderline pixel (turbodither.com).

One global palette, not one per frame. Encoding through ffmpeg? paletteuse supports dither=bayer:bayer_scale=N (N from 0 to 5, a fixed matrix, stable frame to frame) against dither=sierra2_4a (ffmpeg's default error-diffusion mode -- it'll shimmer for the same reason Floyd-Steinberg does). The dither flag only solves half the problem, though. You still need a single palettegen pass, computed once and reused across every frame via paletteuse, for color stability on top of it. A fresh palette generated per frame is a second flicker source entirely on its own, and a stable dither matrix won't touch it (blog.pkh.me).

A stable sampling grid, if you're rendering dots procedurally. Building a code-based halftone -- shader or canvas -- keep the underlying sampling grid fixed across frames and animate only a continuous displacement value on top of it. Don't regenerate or shift the grid itself. And use fwidth()-based antialiasing rather than a fixed pixel threshold: a fixed threshold produces edge jitter the moment the frame's scale or motion changes (blog.maximeheckel.com).

Working from Photoshop's Save for Web dialog instead of ffmpeg? Dithering for the Web: GIF Export Settings That Avoid Banding covers that diffusion-vs-pattern export toggle and the banding side of this same tradeoff.

Test every change in motion, never on a paused frame. A halftone that looks sharper as a still can easily be the one that shimmers once the loop starts running.

Frequently Asked Questions

Why does my animated halftone GIF flicker even though the source barely moves?

Because the halftone/dither algorithm is recomputing its pattern from scratch on every frame. Error-diffusion methods spread quantization error outward from each pixel, so a near-identical frame still produces a slightly different dot pattern -- that per-frame recomputation is what you see as shimmer, not real motion in the source.

Does ordered (Bayer) halftone completely eliminate GIF flicker?

It eliminates the algorithm-level cause: a fixed threshold matrix means identical input pixels always produce identical output regardless of frame. It does not fix a second, independent cause -- a color palette recalculated per frame in your GIF encoder. You need both a fixed matrix and a single reused palette.

Can I fix flicker by varying the halftone pattern every frame instead of holding it fixed?

No -- that's the opposite of what works for a GIF. Cycling the mask relies on the eye's flicker-fusion threshold blending fast-changing frames into a smooth average, which only holds above the refresh rate the eye can resolve. A GIF loop plays at a handful of frames per second, well below that threshold, so a cycling pattern reads as visible noise instead of a stable dot grid.

What palette size and contrast settings keep an animated halftone stable?

Keep the palette to 4-16 colors -- narrower palettes are easier to hold stable across frames than a broad one. Raise contrast before applying the halftone so tonal separation is stronger; the mid-tones are where flicker shows up first. Favor a larger dot/matrix size over fine texture when the source has busy motion.

Does ezgif's halftone tool support animated GIFs without extra settings?

It applies your chosen pattern to every frame and preserves the original timing, but it doesn't expose any frame-to-frame consistency control -- it's running the same pattern-per-frame approach by default, which is why picking a genuinely fixed matrix pattern, not an error-diffusion one, matters more on this tool than the settings panel suggests.

Putting it together

Flicker on an animated halftone GIF comes down to two separate decisions, not one settings slider. There's the dither algorithm -- a fixed matrix holds still, error diffusion recomputes and shimmers -- and there's the palette, where one global palette holds color still and a fresh palette per frame doesn't. Get both right and the loop reads as intentional, not broken.

Try it on your own source: open the halftone effect in kott's studio, with matrix size and palette exposed as real controls instead of one toggle.

02/ OUT

Try it in the studio

Every setting described above is a real control. Open your own image and sweep it.

All journal entries
Animated Halftone GIFs Without Flicker