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 point of the tool was to keep the chaos reachable once you had found it.
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 on every machine.
That one decision (randomness as a parameter, not an event) carries the rest of the tool. Presets, share links, and the ten-frame GIF export all depend on it, with each GIF frame a controlled variation of the same seed 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 runs on Canvas 2D and raw ImageData bytes, with typed arrays and arithmetic you can read in the source. No WebGL and no workers.
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. The spinner tells the user why the output has not updated yet, the kind of signal argued for 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.
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
The whole app runs client-side
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.
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 that carries your settings, not 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.
Grep check · 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.
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.
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 or state library.
07Margin of error
Known limits
GlitchFilter is at v0.2.0.
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). A fifth effect that muddies every image would drag down the four that work, so it stays benched.
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.
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. The GIF encoder is gifenc plus 71 lines of glue, and the share link carries the current settings instead of the image. If your product needs a browser-side editor or live preview, I build those from the Chicago area and remotely.