halftone-fx

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.

Quick start

  1. Open the playground.
  2. Pick a clip from Source (the list mirrors this repo's webgl/assets/ folder) and tune the sliders until it looks right.
  3. Click πŸ“‹ Export iframe β€” the finished embed code is copied to your clipboard.
  4. Paste it into your site builder's embed/HTML element. Done.

Embedding in STUDIO (studio.design)

  1. In the STUDIO editor, open the θΏ½εŠ γƒ‘γƒγƒ« (Add panel) on the left and add an εŸ‹γ‚θΎΌγΏγƒœγƒƒγ‚―γ‚Ή (Embed box) to your page.
  2. Select the box, open the settings panel on the right, and paste the code from Export iframe into the εŸ‹γ‚θΎΌγΏγ‚³γƒΌγƒ‰ (embed code) field.
  3. Size and position the box like any element β€” for a hero background, make it fill the section and layer your text boxes on top.
  4. The editor canvas may show a placeholder instead of the animation β€” that's normal. Check プレビγƒ₯γƒΌ (preview), then ε…¬ι–‹ (publish).
Keep the embed box to the single <iframe> tag. A standalone iframe gets STUDIO's clean "iframe type" embed; any extra HTML around it switches STUDIO to a restricted sandbox mode.
Backgrounds: the exported code includes 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.

URL parameters

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
ParamValuesMeaning
srcURLImage or video. Relative (./assets/…) or any CORS-accessible absolute URL. Add type=video for video URLs without a file extension.
grid3–50Halftone cell size in px. Smaller = finer dots. Default 12.
dothexDot color, without # (e.g. 1a1a2e). Default black.
bghex / transparentBackground color. Default white.
brightness-100…100Default 0.
contrast-100…100Default 0.
gamma0.1–3Default 1.
dithernone bayer4 bayer8 grain noiseSee dither modes. Default none.
multicolorflagRainbow dots colored by brightness.

Adding your own image / video

Assets live in the repo, so they're versioned and served with correct CORS headers. To add one:

  1. Open webgl/assets on GitHub β†’ Add file β†’ Upload files β†’ commit.
  2. Wait ~1 minute for GitHub Pages to redeploy.
  3. Reload the playground β€” the file appears in the Source dropdown automatically (the list is read live from the repo).
Keep files small β€” it's free. The halftone destroys fine detail, so a 640Γ—360 heavily-compressed clip looks identical to 1080p. The dots are drawn by the shader and stay sharp regardless. Recommended encode:
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).

Dither modes

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:

ModeLook
noneSmooth tonal halftone β€” dot size carries the tone. Best default.
bayer4Coarse retro print / CRT crosshatch.
bayer8Finer retro pattern, more tonal steps.
grainOrganic, editorial β€” blue-noise-like, no visible pattern.
noiseAnimated 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.

Performance

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.

FAQ

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.