01The idea

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 */ }
}
  • 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 ImageData is Cloned to a fresh Uint8ClampedArray runs through Enabled effects, in order then putImageData · 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.

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.

  1. 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.

  2. 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.

  3. Quantize per frame

    Every frame gets its own 256-color palette through gifenc's quantize and applyPalette.

  4. 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.

  5. 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

  • React 18
  • Vite 5
  • JavaScript + JSX
  • Canvas 2D
  • gifenc
  • Bootstrap 5
  • GitHub Actionsdeploys toGitHub Pages

By the numbers

  • 12 presets
  • 120ms render debounce
  • 10 frames per GIF export
  • 71 lines in the GIF encoder
  • 0 network requests, ever

10Plates

Survey plates

Four captures from the live tool: the workbench mid-edit, and three exports straight out of the encoder.

The GlitchFilter workbench: effect stack on the left, a glitched cat photo on the canvas, and Glitch Blocks controls on the right
Plate 01 · Workbench · effect stack, canvas, self-rendering controls
An exported GlitchFilter image: a dog portrait cut by scanlines, RGB shift, and glitch-block displacement
Plate 02 · Export sample · a portrait through scanlines, RGB shift, and glitch blocks
An exported GlitchFilter image: Hokusai's Great Wave hue-shifted into neon pinks and teals, textured with scanlines and grain
Plate 03 · Export sample · The Great Wave in a vaporwave palette, scanlines and grain
An exported GlitchFilter image: the dog portrait again, its color channels split into rainbow offsets over soft scanlines
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 frontend 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.