Live GPU halftone dithering for websites β help & embedding guide
halftone-fx renders an image or video as animated halftone dots, live in the browser, on the GPU. Instead of exporting a heavy video file, you embed a tiny program: it stays razor-sharp on any screen, costs ~0.1 ms of CPU per frame, and a whole animated hero background weighs a few hundred kilobytes.
webgl/assets/ folder) and tune the sliders until it looks right.<iframe> tag.
A standalone iframe gets STUDIO's clean "iframe type" embed; any extra HTML around it
switches STUDIO to a restricted sandbox mode.
pointer-events:none
so clicks and scrolling pass through to your page. Remove it if the animation should be
interactive. Set the bg param to your page's background color so the
iframe edges are invisible.
The embed page is configured entirely by its URL β edit params and reload to restyle, no code changes ever:
https://playbox-dev.github.io/halftone-dithering-fx/webgl/embed.html?src=./assets/horse.mp4&grid=9&dither=grain
| Param | Values | Meaning |
|---|---|---|
src | URL | Image or video. Relative (./assets/β¦) or any CORS-accessible absolute URL. Add type=video for video URLs without a file extension. |
grid | 3β50 | Halftone cell size in px. Smaller = finer dots. Default 12. |
dot | hex | Dot color, without # (e.g. 1a1a2e). Default black. |
bg | hex / transparent | Background color. Default white. |
brightness | -100β¦100 | Default 0. |
contrast | -100β¦100 | Default 0. |
gamma | 0.1β3 | Default 1. |
dither | none bayer4 bayer8 grain noise | See dither modes. Default none. |
multicolor | flag | Rainbow dots colored by brightness. |
Assets live in the repo, so they're versioned and served with correct CORS headers. To add one:
ffmpeg -i input.mp4 -vf scale=640:360 -c:v libx264 -pix_fmt yuv420p \
-crf 30 -preset veryslow -movflags +faststart -an output.mp4
The two bundled clips are 97 KB (spiral) and 373 KB (horse).
For animation, temporal stability is what matters β a pattern that changes
every frame reads as shimmer. All modes are stable except noise,
which flickers on purpose:
| Mode | Look |
|---|---|
none | Smooth tonal halftone β dot size carries the tone. Best default. |
bayer4 | Coarse retro print / CRT crosshatch. |
bayer8 | Finer retro pattern, more tonal steps. |
grain | Organic, editorial β blue-noise-like, no visible pattern. |
noise | Animated film grain, re-seeded ~30 fps. |
Error-diffusion dithers (FloydβSteinberg, Stuckiβ¦) are deliberately absent here: they're sequential (can't run on a GPU shader) and they shimmer on video. They live in the classic export tool for still images.
Measured head-to-head against the CPU original (same video, 1920Γ1080, Apple M2 Pro):
CPU version 51.7 ms/frame (~19 fps max, one core pinned) vs
WebGL ~0.07 ms/frame β about 700Γ faster, using
under 1% of a 60 fps frame budget. Reproduce it: open
bench.html, wait a few seconds, and read
window.__results in the console.
The animation doesn't show inside the STUDIO editor.
Expected β embeds render in preview and on the published site, not on the editor canvas.
bg=transparent looks white when I open the embed URL directly.
Expected β a standalone browser tab has nothing behind the page, so you see the
browser's own default backdrop. The transparency is real: put the iframe on a page
(or in STUDIO) and the page shows through between the dots. Both bg and
background work as the param name.
The video doesn't autoplay.
Keep allow="autoplay" on the iframe. Playback is muted + inline, which all
browsers permit.
Colors in the URL don't work.
Drop the # β write dot=ff3300, not dot=#ff3300.
Can I use a clip hosted elsewhere (not in this repo)?
Yes, if the host sends CORS headers (Access-Control-Allow-Origin).
GitHub Pages does; most random hosts don't. When in doubt, commit it to
webgl/assets/.
Does it work on phones?
Yes β WebGL2 is supported by all current browsers including iOS Safari, and the effect
is far lighter than a CSS blur. The component also pauses itself when scrolled offscreen.