A free, browser-based print-effects workshop built with p5.js.
Load an image, animated GIF, or short video and run it through halftone screens, dithering, risograph separations, ASCII, glitch effects and more, all with live controls. You can stack two effects and export stills, separations, vectors, processed GIFs, or silent WebM video.
There's no backend, no framework, no build step. Just static files. Everything runs in your browser and your media never leaves your machine.
| # | Effect | What it does |
|---|---|---|
| 00 | None | Pass-through. Shows the source after pre-adjust with no screen applied. Selecting this in slot B clears the second effect. |
| 01 | Halftone | Classic rotated dot screen where the dot radius maps to darkness. Supports circle, square, line, and star dots in a single mono ink or full CMYK separation with proper screen angles and multiply blending. |
| 02 | Dither | Ordered dithering (2x2, 4x4, 8x8 Bayer) and Floyd-Steinberg error diffusion with optional serpentine scan. Multiple palettes (1-bit, duotone, 3/4-tone, CMYK), chunky pixel scaling, and a modulation field. |
| 03 | ASCII | Converts the image to a grid of characters from an editable brightness ramp. Mono or source-coloured, rendered in a real monospace font. There's an optional selectable-text overlay so you can copy the output. |
| 04 | Risograph | Simulates 2 or 3 spot-colour separations using luminance bands or RGB channels. Each layer gets its own halftone or grain screen, with misregistration offset and multiply compositing. |
| 05 | Xerox | High-contrast photocopy look. Cranks up contrast, applies a soft blur, adds a noisy threshold, and sprinkles in toner dropouts and dust. |
| 06 | Analog | A MiniDV camera signal on a CRT. Resamples onto a virtual raster of scanlines, applies DV 4:1:1 chroma bleed, highlight clip and edge enhancement (8×8 DCT blocking at high Signal), then draws brightness-dependent beams through an aperture-grille or slot mask, with beam defocus (Blur) and halation. |
| 07 | Glitch | Static, seeded datamosh. Random-height band shifts, block smears, and channel tears. Works horizontal or vertical. |
| 08 | RGB Shift | Chromatic aberration in linear or radial (lens fringing) mode. Amount/angle (or radial strength) control with edge bias. |
| 09 | Pixel Sort | Sorts pixel runs by brightness, hue, or saturation. Several interval modes: threshold (Asendorf-style), edge detection, seeded random, or full row/column. |
| 10 | Emboss | Raised-relief look mapping luminance gradients along a light angle around mid-grey. Supports grey, colour, and tinted modes. |
| 11 | Tiles | Cuts the frame into a dynamic grid of multi-spanning rectangular tiles with partial shuffle and rotation. |
Analog recipes: the defaults are a clean broadcast monitor. For a consumer TV, set Mask to Slot, Lines 280, Bloom 0.45, Blur 0.45, Signal 0.5. For a PVM close-up, use Grille, Lines 480, Mask strength 0.55, Scanlines 0.8, Bloom 0.15, Blur 0.05. Fine phosphor and scanline structure only shows when the screen has the pixels for it, so it is clearer in 2× PNG exports and at low Lines.
Effects 07 to 09 are the glitch family. Glitch and Pixel Sort (random intervals) take a seed, so the same seed always gives you the same result. Hit Rnd (randomize) or press R to reroll the active effect's settings.
Gradient Map, Tone, and Hue / Sat are colour modules: set them as the Colour pre-stage under Pre-adjust to recolour the source before the effect runs (e.g. gradient-map into a halftone).
It's a static site, so any file server works. From this folder:
# Node (same as `npm start`, which serves on port 8000)
npx serve .
# or Python 3
python3 -m http.server 8000Then open the URL it prints (usually http://localhost:8000). Opening index.html directly via file:// won't work because the app uses ES modules, which browsers only load over HTTP.
Tested on current Chrome, Firefox, and Safari.
npm testRuns the Node test suite in tests/*.test.mjs: media helpers, animated-export guards, the filmstrip wheel, and a headless-Chromium pass that renders every effect across its modes, checks the pre-adjust curves and grain, and enforces a 50 ms warm-frame budget per effect. It needs Playwright's Chromium (npx playwright install chromium).
The deterministic animated-media smoke pages can be run against all supported browser engines:
npm ci
npx playwright install --with-deps chromium firefox webkit
npm run test:browserThe command runs both tests/browser-smoke.html and
tests/app-integration-smoke.html in Chromium (Chrome), Firefox, and WebKit.
WebKit is the Safari-equivalent target used by Linux CI, where Apple Safari
cannot run. Each target checks disposal 2 and disposal 3 forward and backward
seeks, transparent-pixel compositing, and canvas cleanup. Engine-specific
assertion or page errors identify the browser and smoke page in the failure
output. The same matrix runs automatically in GitHub Actions.
- Load an image, animated GIF, or short video (or try the Demo plate). You can also drag and drop onto the stage.
- Image: up to 20 MB and 8192 px per side (larger images are downscaled to 2560 px on load).
- GIF: up to 12 MB, 1600 px per side, 240 frames, 30 seconds, and a 48 MB decoded-patch budget.
- Video: MP4, WebM, MOV, or OGV up to 60 MB, 1920 px per side, and 60 seconds.
- Animated playback runs at a constant 15 processed frames per second (or the media's original frame rate if 15 fps or lower) to keep motion smooth and the browser responsive.
- Pre-adjust brightness, contrast, gamma, grain, and invert. Optionally pick a Colour pre-stage (Gradient Map, Tone, or Hue/Sat) that gets applied to the working buffer before the effect runs.
- Pick Effect A from the filmstrip at the bottom and tweak its parameters. The preview updates live.
- Want to stack a second effect? Click B in the filmstrip toggle, then pick any effect. B runs on A's rendered output. Picking 00 None in slot B clears the stack.
- Export (button or press
E) opens the overlay. Every export is free, full-resolution:- PNG at 2x device resolution
- Colour separation ZIP with one plate per ink (for Halftone CMYK, RGB Shift, and Risograph)
- SVG with true vectors (for Halftone, ASCII, Dither, and Risograph). Not available for raster effects or while two effects are stacked.
- Animated GIF for GIF sources, encoded in a worker at a constant 15 fps (or original if ≤ 15 fps) and capped at 720 px.
- Silent WebM for video sources, recorded from the processed canvas at a constant 15 fps (or original if ≤ 15 fps). Source audio is never captured. Like most browser recordings, the file has no duration in its header, so some players show an unknown length.
PNG, SVG, and separation exports capture the animated source's current frame when that export is compatible with the selected effect.
Keyboard shortcuts: R randomize the active effect, E open Export, Esc close it, Space play or pause animated media.
index.html markup + p5 CDN + Google Fonts + GoatCounter analytics
style.css five-colour token system, no rounded corners, Swiss styling
src/
main.js p5 instance mode, app state, all wiring
input.js image loading, fitting, pre-adjust, working buffer + sampling helpers
media.js bounded GIF/video loading, playback, seeking, and disposal
controls.js auto-generates UI controls from each effect's param schema
export.js PNG (2x) + SVG + colour-separation ZIP export
animated-export.js WebM recording + worker-backed GIF export orchestration
gif-worker.js per-frame GIF palette and encoding work
zip.js minimal STORE-only ZIP writer for separation plates
effects/
index.js effect registry (array of modules, in UI order)
stack.js rendering pipeline: pre-adjust → colour pre-stage → A → [B]
_shared.js colour/SVG helpers, re-exports the PRNG
none.js halftone.js dither.js ascii.js riso.js xerox.js analog.js
glitch.js rgb-shift.js pixel-sort.js emboss.js tiles.js
gradient-map.js tone.js hue-sat.js
controls/
curve-editor.js gradient-editor.js
color/
curve.js ramps.js
ui/
filmstrip-wheel.js mouse-wheel horizontal scrolling for the effect strip
util/
prng.js mulberry32 seeded PRNG
scratch.js pooled scratch canvases + zero-readback pixel buffers
tests/ Node test suite and browser smoke pages (see Tests)
DESIGN.md design system (tokens, rules, components); mirrored in .impeccable/design.json
Every effect is a self-describing module with an id, name, params array (which drives the control panel automatically), and render() / optional renderSVG() methods. The pipeline in stack.js caches each stage by signature and only recomputes when inputs actually change.
Your images, GIFs, and videos never leave your machine. There is no backend and no upload. The page loads p5.js from cdnjs and its fonts from Google Fonts, and the GIF decoding and encoding libraries are loaded on demand from jsDelivr; all processing stays local. The site loads GoatCounter, a lightweight, cookieless analytics script that only records anonymous page-view counts. No personal data, no cross-site tracking, no fingerprinting.
If you find Halftone Tools useful, consider buying me a coffee ☕️
Personal Use Only. See LICENSE.md for the full terms.
You're free to use and modify Halftone Tools for personal, non-commercial purposes. Commercial use and redistribution (of the original or modified versions) are not permitted. Credit is required: Halftone Tools by Sefa Tolaman.