Live GPU square halftone for websites
mimono halftone uses the stable <halftone-fx> component
to render an image or video as animated square marks,
live in the browser, on the GPU. Download the result as an image or animation, or embed
the tiny live 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). Frame follows its original ratio
by default; choose Square crop only when you want a centered 1:1 cut.
Choose a Look preset, then tune any slider until it looks right.| Format | Output | Best for |
|---|---|---|
| PNG | Full preview resolution and selected frame ratio; preserves alpha when Transparent background is on | Still artwork and layouts |
| MP4 | Up to 30 fps and 1080 px on the longest edge; 3, 6, or 12 seconds | Social, slides, and broadly compatible video |
| WebM | Available when the browser supports it; same lengths and size limit | A browser-friendly video fallback |
| GIF | 10 fps; up to 480 px on the longest edge; 3 or 6 seconds; loops forever | Messages, docs, and quick previews |
Animation export records the live preview from its current pose and is silent. Keep the tab visible until capture finishes; the Download button becomes Cancel export while it works.
MP4 encoding is provided by the browser. If the current browser cannot create a real MP4,
the option is marked unavailable and WebM is offered when possible. The app never puts
WebM data inside a file named .mp4.
<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. New exports include the source's numeric
aspect-ratio; choosing Square crop emits 1/1 instead. For a
custom crop, use the same ratio in both the URL's aspect parameter and the
iframe style.
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&aspect=source&grid=80&shape=square&size=42&threshold=50
| 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. |
aspect | source / square / ratio | Output frame. New exports use the source ratio by default; ratios such as 4/5 are also accepted. Omitted on legacy embeds means square. |
grid | 1β320 in the playground | Halftone cell size in px. Smaller = more detail; larger = fewer marks. Default 80. |
shape | square / circle | Mark geometry. Default square; circle preserves the legacy look. |
size | 0β100 | Maximum mark size within each cell. Lower values create more space between squares. Default 42. |
threshold | 0β100 | Hides marks smaller than this percentage. Raise it to remove small peripheral squares. Default 50. |
dot | hex | Mark color, without # (e.g. ff3110). Default mimono Play Red. |
bg | hex / transparent | Background color. Default transparent. |
brightness | -100β¦100 | Raises or lowers source luminance. Default 12 for a lighter, sparser result. |
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 marks colored by brightness. |
invert | flag | Flip foreground and background: marks are drawn on the light areas of the source and the dark areas stay empty. Tone controls keep their meaning. |
motion | pulse radial sweep interference scan | A symmetric mark-size field. Omit it for no motion. |
amount | 0β100 | How strongly the field resizes marks. Default 35. |
speed | 0β100 | Motion rate. Zero freezes the current field. Default 35. |
phase | radians | Starting pose. Added automatically by the iframe exporter so a frozen field round-trips exactly. |
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 β square 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.
Motion changes mark size only. It does not change the source tone, grid, color, or threshold. Every field uses normalized mirrored coordinates, so it keeps the same geometry at sparse and detailed grid settings and keeps the field itself vertically and horizontally symmetric.
| Mode | Movement |
|---|---|
pulse | All marks expand and contract together. |
radial | Concentric circular fronts move through the source. |
sweep | A square front travels from the centre to the edge. |
interference | Two perpendicular waves form a shifting lattice. |
scan | A thin mirrored cross travels outwards and back. |
Amount sets the resize depth and Speed sets the rate.
The operating system's reduced-motion setting disables the field and shows the untouched
look. Previously exported overdrive, flux, and seed
params remain accepted for compatibility.
embed.html?src=./assets/horse.mp4&aspect=source&grid=3&dither=grain&motion=radial&amount=35&speed=35
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 marks. 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/.
Why did animation export stop when I switched tabs?
Browsers pause visual animation in hidden tabs. Keep this tab visible while recording so
the saved timing matches the preview.
Does it work on phones?
The live preview works on current phones and is far lighter than a CSS blur. Available
video formats depend on the browser, and longer GIFs may be limited by device memory.
The component also pauses itself when scrolled offscreen.