A glitch-art workbench that runs entirely in the browser: stackable effects with seeded, reproducible randomness, a live Canvas preview debounced to 120ms, 12 presets, four export formats, and an animated GIF encoder in pure JavaScript. Nothing you load ever leaves the tab.
Glitch tools hand you a dice roll. I wanted dials.
Most glitch generators work like a slot machine. You mash a randomize button until something looks great, and the moment you touch anything else, that look is gone. The chaos is the appeal; losing it isn't.
GlitchFilter treats every artifact as a parameter. RGB channels shift by the pixel. Scanlines have spacing, thickness, and opacity. The block corruption that reads as random is driven by a seeded linear congruential generator, so seed 412 produces the same damage today, tomorrow, and on someone else's laptop.
That one decision (randomness as a parameter, not an event) carries the rest of the tool. It's what makes presets possible, share links possible, and a ten-frame animated GIF possible, where each frame is a controlled variation instead of a reroll.
Field note · the whole trick, two lines
// glitch-blocks: a seeded LCG stands in for Math.random()
next = seed *1664525+1013904223// gifExport: frame n re-runs the stack, nudged
frameSeed = seed + frameIndex *13
02The effects system
Every effect is one file that describes itself
Effects live in src/effects, one file each, in a plugin-style registry keyed by id and grouped into five categories: distortion, vintage, overlay, noise, and color. An effect exports a single object: id, label, category, icon, default params, a controls array, and an apply function that takes ImageData and returns ImageData.
The controls array is the interesting part. Each entry declares a slider, select, or toggle with its min, max, step, and unit, and the controls panel renders itself from that metadata. Adding an effect means writing one file (every effect so far fits in under 70 lines) and the UI shows up on its own.
Effects stack. The pipeline is an ordered array of instances, each apply feeding the next, so scanlines can run over corrupted blocks, or twice over themselves: the Laser Grid preset stacks two scanline instances. Instances reorder by drag and drop and toggle on and off individually.
Data specimen · one effect, as registered
{id:'glitch-blocks',label:'Glitch Blocks',category:'distortion',defaultParams:{/* one value per control */},controls:[{param:'count',type:'slider',min:1,max:60},{param:'maxShift',type:'slider',max:200,unit:'px'},{param:'colorGlitch',type:'toggle'},{param:'seed',type:'slider',min:0,max:999}],apply(imageData, params){/* pixels in, pixels out */}}
The roster
RGB Shift
distortion
Six per-channel offsets (red, green, and blue on both axes, -100 to 100px), resampled from a pristine copy. Radial mode scales the shift by distance from center.
Scanlines
overlay
Spacing 2-20px, thickness 1-4px, 0-100% opacity, horizontal or vertical. Darkens rows by multiplying the channels down.
Glitch Blocks
distortion
1 to 60 displaced slabs with height and probability controls, shifts up to 200px, an optional XOR pass over the red channel, and a 0-999 seed. Fully reproducible.
Noise / Grain
noise
0-100% amount with chunky per-cell grain from 1 to 8px, in luminance or full color.
VHS / Tape
vintage · benched
Noise, color bleed, jitter, chroma shift, warp, and opacity. Built, then pulled from the registry over color dominance issues. More in section 07.
03The render pipeline
One pristine buffer, cloned every render
The renderer is Canvas 2D and raw ImageData bytes. No WebGL, no workers, no shader mystery: just typed arrays and arithmetic you can read.
Pristine ImageDataisCloned to a fresh Uint8ClampedArrayruns throughEnabled effects, in orderthenputImageData · full resolution
A 120ms debounce does the heavy lifting
Slider drags collapse into a single render at the end of the debounce window, with a spinner while it waits. The preview feels live without re-running the whole stack for every tick of the drag. That spinner is doing real work: telling the user the wait is theirs, not the app's, which is the whole argument in micro-interactions that earn their keep.
Zoom never touches a pixel
10% to 400% zoom is a CSS transform on the stage, so it costs nothing. Fit mode re-fits through a ResizeObserver when the window resizes; Original is an exact 100%.
Before / After / Split
A second canvas holds the untouched source underneath the output. The compare modes cross-fade the two with opacity, including a half-and-half split.
Survey noteThe source buffer is never mutated. Every look, no matter how destroyed, is one state change away from the original.
04The GIF encoder
An animated GIF, encoded in the tab
Export needs no server and no upload. gifenc plus 71 lines of glue in gifExport.js turn a static effect stack into a looping animation.
STA 01
Downscale first
The source runs through two offscreen canvases until its longest side is 640px or less. GIF is a heavy format; the cap keeps exports shareable instead of enormous.
STA 02
Re-run the stack, ten times
Each of the 10 frames re-runs the entire effect pipeline. Any effect with a seed gets seed + frameIndex * 13, so glitch blocks jump deterministically from frame to frame, while noise varies on its own. The animation is the same controlled chaos, ten different ways.
STA 03
Quantize per frame
Every frame gets its own 256-color palette through gifenc's quantize and applyPalette.
STA 04
Yield between frames
The encoder hands control back to the UI after every frame and reports progress, so the export button reads "Frame 3/10" instead of the tab freezing.
STA 05
Deliver and clean up
Frames encode at a 100ms delay (10fps, infinite loop), land in a Blob, download as glitchfilter-export.gif, and the object URL is revoked behind them.
05Privacy
Private by architecture, not by policy
There is no upload. A dropped file goes URL.createObjectURL to an Image to an offscreen canvas to getImageData, and lives in React state from then on. The src tree contains zero network calls: no fetch, no XMLHttpRequest, no axios, no beacons, no sockets.
The share feature is the proof. A share link compresses the effect stack (index-based effect ids, only the params that differ from defaults, select values as option indexes) into a base64 #preset= hash. It carries your settings, never your pixels. Paste a link or a bare code into the Import box and the exact look reproduces on any machine.
The only outbound touches in the whole app: navigator.clipboard.writeText when you copy a share link, and localStorage for the theme.
Receipt · network calls in src/
$ grep -rE "fetch\(|XMLHttpRequest|axios|sendBeacon|WebSocket" src/
exit 1 · no matches
06The workbench
Details that make it feel like a tool
The shell is three panes: the effect stack on the left, the canvas in the middle, the auto-generated controls on the right. Around it, the parts a tool owes its user:
12 presets
Signal Lost, Retrowave, Data Rot, TV Snow: each preset is a saved stack of two or three fully parameterized effect instances. Nothing magic, which is the point.
Randomize, with guardrails
Picks 3 to 6 effects and rolls every slider within its declared range, honoring per-control randomMax caps so "random" stays usable instead of turning the canvas to soup.
Undo that respects you
Up to 20 history snapshots on Ctrl+Z, Ctrl+Y, and Ctrl+Shift+Z. A creative tool without undo is a toy.
Full-resolution exports
PNG, JPEG, and WebP at the source's native size from a split button in the header, plus the animated GIF.
Drop anything
Drag and drop or click to load JPG, PNG, WebP, or GIF, decoded straight to ImageData. Dark and light themes persist to localStorage.
One reducer, no sprawl
All state lives in a single React Context with useReducer: 16 action types in 233 lines. No Redux, no router, no state library.
07Margin of error
Honest limits
GlitchFilter is at v0.2.0, and the sheet marks its margins honestly.
The VHS effect is benched
Six parameters of tape damage, built and working, then hidden from the registry over color dominance issues (tracked as FEATURES.md #143). Shipping four good effects beats shipping five with one that muddies every image.
Main thread, on purpose
Every render walks the full-resolution byte array on the main thread: no WebGL, no workers. The 120ms debounce keeps it feeling live, and the 640px GIF cap keeps exports quick. If stacks grow much deeper, a worker is the natural escape hatch.
Live on GitHub Pages
The GitHub Actions workflow builds and publishes to GitHub Pages behind the custom domain. That last box is checked: the site is live at glitchfilter.com (opens in a new tab).
Stack
08 · Field kit · built with
React 18
Vite 5
JavaScript + JSX
Canvas 2D
gifenc
Bootstrap 5
GitHub Actionsdeploys toGitHub Pages
By the numbers
09 · Control points · by the numbers
12presets
120msrender debounce
10frames per GIF export
71lines in the GIF encoder
0network requests, ever
10Plates
Survey plates
Four captures from the live tool: the workbench mid-edit, and three exports straight out of the encoder.
Plate 02 · Export sample · a portrait through scanlines, RGB shift, and glitch blocks
Plate 03 · Export sample · The Great Wave in a vaporwave palette, scanlines and grain
Plate 04 · Export sample · the same dog, RGB channels pulled apart
11Next traverse
Need a browser app that feels like a native tool?
GlitchFilter is the kind of canvas and motion work I enjoy most: real-time canvas rendering, deliberate state, and exports that never touch a server. If your product needs an editor, a configurator, or a live preview pipeline that runs entirely on the client, I build those, from the Chicago area and remotely.