All articles
15 min read

Glitch Art: How to Create Stunning Digital Glitch Effects With Code

glitch artcreative codinggenerative artglitch effectJavaScript

Glitch art transforms digital errors into aesthetic experiences. Corrupted data, displaced pixels, broken scan lines — the visual artifacts that engineers spend careers eliminating become the raw material for one of the most distinctive art forms of the digital age. What makes glitch art compelling is its tension between control and chaos: you engineer the conditions for failure, then curate the results.

From a programming perspective, glitch art is about understanding how images are stored and displayed, then deliberately breaking those assumptions. An image is just an array of numbers — red, green, blue, alpha values packed sequentially. Shift a channel, sort by brightness, corrupt a byte, and the result is instantly recognizable as "glitched" because our brains are exquisitely tuned to detect when visual data goes wrong.

This guide covers eight working glitch art systems you can build in your browser. Every example uses vanilla JavaScript and the Canvas API — no libraries, no frameworks. Just pixels, math, and creative destruction.

1. RGB channel shift — the classic glitch

The most fundamental glitch effect: splitting an image into its red, green, and blue channels and offsetting them from each other. In normal rendering, all three channels are perfectly aligned. Shift them apart and you get the iconic chromatic aberration that screams "digital error." This version generates a test pattern and applies animated channel displacement.

const C = document.createElement('canvas');
C.width = 480; C.height = 360;
document.body.appendChild(C);
const ctx = C.getContext('2d');

// Generate source pattern
function drawSource() {
  for (let y = 0; y < C.height; y += 20) {
    for (let x = 0; x < C.width; x += 20) {
      const h = (x + y) * 0.5;
      ctx.fillStyle = `hsl(${h}, 70%, ${40 + Math.sin(x * 0.02) * 20}%)`;
      ctx.fillRect(x, y, 20, 20);
    }
  }
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 48px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('GLITCH', C.width / 2, C.height / 2);
  ctx.fillText('ART', C.width / 2, C.height / 2 + 55);
}

let t = 0;
function render() {
  drawSource();
  const src = ctx.getImageData(0, 0, C.width, C.height);
  const dst = ctx.createImageData(C.width, C.height);
  const w = C.width, h = C.height;

  // Animated channel offsets
  const rx = Math.sin(t * 0.03) * 12 | 0;
  const ry = Math.cos(t * 0.02) * 4 | 0;
  const bx = Math.cos(t * 0.025) * 10 | 0;
  const by = Math.sin(t * 0.035) * 6 | 0;

  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const di = (y * w + x) * 4;
      // Red channel — shifted
      const rsx = Math.min(Math.max(x + rx, 0), w - 1);
      const rsy = Math.min(Math.max(y + ry, 0), h - 1);
      dst.data[di] = src.data[(rsy * w + rsx) * 4];
      // Green channel — centered
      dst.data[di + 1] = src.data[di + 1];
      // Blue channel — shifted opposite
      const bsx = Math.min(Math.max(x + bx, 0), w - 1);
      const bsy = Math.min(Math.max(y + by, 0), h - 1);
      dst.data[di + 2] = src.data[(bsy * w + bsx) * 4 + 2];
      dst.data[di + 3] = 255;
    }
  }
  ctx.putImageData(dst, 0, 0);
  t++;
  requestAnimationFrame(render);
}
render();

The key insight is that each color channel is just an independent grayscale image. When they're aligned, you see normal color. When they drift apart, you see color fringes — the same artifact that happens in cheap camera lenses (chromatic aberration) or old CRT monitors with misaligned electron guns. The animated sine waves create a breathing, organic quality rather than a static offset.

2. Pixel sorting — order from chaos

Pixel sorting is the signature technique of digital glitch art. You take rows (or columns) of pixels, sort them by brightness within certain thresholds, and leave the rest untouched. The result: liquid streaks of color flowing through an otherwise intact image. Made famous by Kim Asendorf's 2010 Processing sketch, it remains one of the most visually striking code-based art techniques.

const C = document.createElement('canvas');
C.width = 400; C.height = 400;
document.body.appendChild(C);
const ctx = C.getContext('2d');

// Generate a gradient scene
for (let y = 0; y < C.height; y++) {
  for (let x = 0; x < C.width; x++) {
    const r = 128 + Math.sin(x * 0.03 + y * 0.02) * 80;
    const g = 100 + Math.cos(y * 0.04) * 60 + Math.sin(x * 0.05) * 40;
    const b = 160 + Math.sin((x + y) * 0.02) * 70;
    ctx.fillStyle = `rgb(${r},${g},${b})`;
    ctx.fillRect(x, y, 1, 1);
  }
}

const img = ctx.getImageData(0, 0, C.width, C.height);
const d = img.data;
const w = C.width;

const LO = 60, HI = 220; // brightness thresholds

function brightness(i) {
  return (d[i] + d[i + 1] + d[i + 2]) / 3;
}

// Sort each row
for (let y = 0; y < C.height; y++) {
  let x = 0;
  while (x < w) {
    // Find start of sortable span
    while (x < w && brightness((y * w + x) * 4) < LO) x++;
    const start = x;
    // Find end of sortable span
    while (x < w && brightness((y * w + x) * 4) < HI) x++;
    const end = x;
    if (end - start < 2) continue;

    // Extract span pixels
    const span = [];
    for (let i = start; i < end; i++) {
      const idx = (y * w + i) * 4;
      span.push([d[idx], d[idx+1], d[idx+2], d[idx+3]]);
    }
    // Sort by brightness
    span.sort((a, b) => (a[0]+a[1]+a[2]) - (b[0]+b[1]+b[2]));
    // Write back
    for (let i = 0; i < span.length; i++) {
      const idx = (y * w + (start + i)) * 4;
      d[idx] = span[i][0]; d[idx+1] = span[i][1];
      d[idx+2] = span[i][2]; d[idx+3] = span[i][3];
    }
  }
}
ctx.putImageData(img, 0, 0);

The brightness thresholds are everything. Set them too wide and the entire row sorts into a smooth gradient — boring. Set them too narrow and nothing changes. The sweet spot creates runs of sorted pixels interrupted by unsorted anchors, giving the characteristic "dripping" effect. Experiment with sorting by hue instead of brightness, or sorting columns instead of rows, for completely different aesthetics.

3. Block displacement — digital earthquake

Block displacement simulates the visual corruption you see when a video codec loses keyframe data: rectangular chunks of the image teleport to wrong positions. This technique is closely related to how pixel-level manipulation works, but at the macro scale — shuffling blocks rather than individual pixels.

const C = document.createElement('canvas');
C.width = 480; C.height = 360;
document.body.appendChild(C);
const ctx = C.getContext('2d');

function drawScene() {
  const grad = ctx.createLinearGradient(0, 0, C.width, C.height);
  grad.addColorStop(0, '#1a0533');
  grad.addColorStop(0.5, '#0f4c75');
  grad.addColorStop(1, '#00b4d8');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, C.width, C.height);
  for (let i = 0; i < 30; i++) {
    ctx.beginPath();
    ctx.arc(Math.random()*C.width, Math.random()*C.height,
            10 + Math.random()*40, 0, Math.PI*2);
    ctx.fillStyle = `hsla(${Math.random()*360}, 70%, 60%, 0.4)`;
    ctx.fill();
  }
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 36px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('CORRUPTED', C.width/2, C.height/2);
}

let t = 0;
function render() {
  drawScene();
  const intensity = (Math.sin(t * 0.05) + 1) * 0.5; // 0-1 pulsing
  const blocks = 5 + (intensity * 15) | 0;

  for (let i = 0; i < blocks; i++) {
    const bw = 20 + Math.random() * 120 | 0;
    const bh = 5 + Math.random() * 40 | 0;
    const sx = Math.random() * (C.width - bw) | 0;
    const sy = Math.random() * (C.height - bh) | 0;
    const dx = sx + (Math.random() - 0.5) * 60 * intensity | 0;
    const dy = sy + (Math.random() - 0.5) * 20 | 0;
    const block = ctx.getImageData(sx, sy, bw, bh);
    ctx.putImageData(block, dx, dy);
  }

  // Scan line corruption
  for (let i = 0; i < 3; i++) {
    const y = Math.random() * C.height | 0;
    const h = 1 + Math.random() * 3 | 0;
    const shift = (Math.random() - 0.5) * 30 | 0;
    const line = ctx.getImageData(0, y, C.width, h);
    ctx.putImageData(line, shift, y);
  }

  t++;
  requestAnimationFrame(render);
}
render();

The pulsing intensity creates a rhythm — moments of calm followed by bursts of chaos. The combination of large block displacements and thin scan line shifts mimics real video corruption, where both macro and micro errors happen simultaneously. The horizontal bias on scan line shifts is intentional: real digital video stores data in rows, so corruption tends to displace horizontally.

4. Scan line and interlace effects — CRT nostalgia

Scan lines and interlacing artifacts harken back to CRT displays and VHS tapes. These effects simulate the physical limitations of analog display technology: the electron beam scanning left to right, the fields interlacing odd and even lines, the phosphor glow between passes. Combined with modern content, they create a powerful sense of retro-futurism.

const C = document.createElement('canvas');
C.width = 480; C.height = 360;
document.body.appendChild(C);
const ctx = C.getContext('2d');

let t = 0;
function render() {
  // Draw animated gradient background
  for (let y = 0; y < C.height; y += 4) {
    const h = (y * 0.5 + t) % 360;
    ctx.fillStyle = `hsl(${h}, 60%, ${30 + Math.sin(y*0.02+t*0.03)*15}%)`;
    ctx.fillRect(0, y, C.width, 4);
  }
  ctx.fillStyle = '#0f0';
  ctx.font = 'bold 32px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('SCAN LINE', C.width/2, 140);
  ctx.fillText('SIMULATION', C.width/2, 185);

  const img = ctx.getImageData(0, 0, C.width, C.height);
  const d = img.data;
  const w = C.width;

  for (let y = 0; y < C.height; y++) {
    const row = y * w * 4;
    // Darken every other line (interlace)
    if (y % 2 === (t >> 1) % 2) {
      for (let x = 0; x < w; x++) {
        const i = row + x * 4;
        d[i] = d[i] * 0.6 | 0;
        d[i+1] = d[i+1] * 0.6 | 0;
        d[i+2] = d[i+2] * 0.6 | 0;
      }
    }
    // Horizontal jitter on some lines
    if (Math.random() < 0.02) {
      const shift = (Math.random() - 0.5) * 8 | 0;
      if (shift > 0) {
        for (let x = w - 1; x >= shift; x--) {
          const di = row + x * 4, si = row + (x - shift) * 4;
          d[di]=d[si]; d[di+1]=d[si+1]; d[di+2]=d[si+2];
        }
      } else if (shift < 0) {
        for (let x = 0; x < w + shift; x++) {
          const di = row + x * 4, si = row + (x - shift) * 4;
          d[di]=d[si]; d[di+1]=d[si+1]; d[di+2]=d[si+2];
        }
      }
    }
    // Add subtle scanline darkening every 3rd line
    if (y % 3 === 0) {
      for (let x = 0; x < w; x++) {
        const i = row + x * 4;
        d[i] = d[i] * 0.85 | 0;
        d[i+1] = d[i+1] * 0.85 | 0;
        d[i+2] = d[i+2] * 0.85 | 0;
      }
    }
  }

  // Phosphor glow: slight green tint
  for (let i = 0; i < d.length; i += 4) {
    d[i+1] = Math.min(255, d[i+1] + 8);
  }

  ctx.putImageData(img, 0, 0);
  t++;
  requestAnimationFrame(render);
}
render();

The alternating interlace field (`(t >> 1) % 2`) creates the authentic flicker of interlaced video — odd lines display one frame, even lines the next. The sparse horizontal jitter (2% probability) mimics electromagnetic interference, and the phosphor green tint recalls CRT monitor glow. These small details compound into a convincingly analog aesthetic from purely digital code.

5. Data corruption — breaking the byte stream

True glitch art often involves corrupting the raw data of an image file — databending. In the browser, we simulate this by treating the pixel buffer as raw bytes and introducing errors: flipping bits, copying wrong regions, overwriting with patterns. The result looks like a JPEG that survived a bad download or a file opened in the wrong program.

const C = document.createElement('canvas');
C.width = 400; C.height = 400;
document.body.appendChild(C);
const ctx = C.getContext('2d');

// Generate source image
for (let y = 0; y < C.height; y++) {
  for (let x = 0; x < C.width; x++) {
    const r = Math.sin(x * 0.03) * 127 + 128;
    const g = Math.cos(y * 0.04 + x * 0.01) * 100 + 128;
    const b = Math.sin((x + y) * 0.025) * 127 + 128;
    ctx.fillStyle = `rgb(${r|0},${g|0},${b|0})`;
    ctx.fillRect(x, y, 1, 1);
  }
}

let t = 0;
function corrupt() {
  const img = ctx.getImageData(0, 0, C.width, C.height);
  const d = img.data;
  const len = d.length;

  // Type 1: Byte shift — copy a chunk to wrong location
  for (let i = 0; i < 3; i++) {
    const src = Math.random() * (len - 2000) | 0;
    const dst = Math.random() * (len - 2000) | 0;
    const size = 200 + Math.random() * 1800 | 0;
    for (let j = 0; j < size; j++) {
      d[dst + j] = d[src + j];
    }
  }

  // Type 2: Channel swap — make R=B or G=R in a region
  const swapStart = Math.random() * (len - 4000) | 0;
  const swapLen = 1000 + Math.random() * 3000 | 0;
  for (let i = swapStart; i < swapStart + swapLen; i += 4) {
    const tmp = d[i];
    d[i] = d[i + 2];   // R = B
    d[i + 2] = tmp;     // B = old R
  }

  // Type 3: Repeating pattern — overwrite with short repeating sequence
  if (Math.random() < 0.5) {
    const patStart = Math.random() * len | 0;
    const pat = [d[patStart], d[patStart+1], d[patStart+2], d[patStart+3]];
    const fillStart = Math.random() * (len - 800) | 0;
    const fillLen = 200 + Math.random() * 600 | 0;
    for (let i = 0; i < fillLen; i++) {
      d[fillStart + i] = pat[i % 4];
    }
  }

  // Type 4: Bit flip — randomly flip bits
  for (let i = 0; i < 50; i++) {
    const idx = Math.random() * len | 0;
    d[idx] ^= (1 << (Math.random() * 8 | 0));
  }

  // Keep alpha intact
  for (let i = 3; i < len; i += 4) d[i] = 255;

  ctx.putImageData(img, 0, 0);
  t++;
  if (t < 60) requestAnimationFrame(corrupt);
}
corrupt();

Each corruption type mimics a different real-world failure mode. Byte shifting simulates buffer overflows where memory regions bleed into each other. Channel swapping is what happens when a codec misidentifies the color space. Pattern repetition mimics stuck disk sectors returning the same data. Bit flips are cosmic ray errors — random single-bit changes that create speckled noise. Running for 60 frames creates a progressive degradation that's hypnotic to watch.

6. JPEG artifact simulation — compression as art

JPEG compression divides images into 8x8 blocks and applies the Discrete Cosine Transform. When quality is set low, quantization errors create the characteristic "blocky" artifacts at block boundaries, and ringing artifacts (Gibbs phenomenon) around sharp edges. This simulation recreates that aesthetic by quantizing blocks and exaggerating the boundaries.

const C = document.createElement('canvas');
C.width = 480; C.height = 360;
document.body.appendChild(C);
const ctx = C.getContext('2d');

// Generate scene
const grad = ctx.createRadialGradient(240,180,10,240,180,250);
grad.addColorStop(0, '#ff3366');
grad.addColorStop(0.5, '#6633ff');
grad.addColorStop(1, '#000');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, C.width, C.height);
ctx.fillStyle = '#fff';
ctx.font = 'bold 40px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('COMPRESSED', C.width/2, C.height/2);

const src = ctx.getImageData(0, 0, C.width, C.height);
const dst = ctx.createImageData(C.width, C.height);
const BLOCK = 8;
const Q = 40; // quantization level (higher = more artifacts)

for (let by = 0; by < C.height; by += BLOCK) {
  for (let bx = 0; bx < C.width; bx += BLOCK) {
    // Compute block average per channel
    let rSum=0, gSum=0, bSum=0, count=0;
    for (let y = by; y < Math.min(by+BLOCK, C.height); y++) {
      for (let x = bx; x < Math.min(bx+BLOCK, C.width); x++) {
        const i = (y * C.width + x) * 4;
        rSum += src.data[i]; gSum += src.data[i+1]; bSum += src.data[i+2];
        count++;
      }
    }
    // Quantize
    const rAvg = Math.round(rSum / count / Q) * Q;
    const gAvg = Math.round(gSum / count / Q) * Q;
    const bAvg = Math.round(bSum / count / Q) * Q;

    // Fill block with quantized color + noise
    for (let y = by; y < Math.min(by+BLOCK, C.height); y++) {
      for (let x = bx; x < Math.min(bx+BLOCK, C.width); x++) {
        const i = (y * C.width + x) * 4;
        const orig = src.data[i];
        // Mix quantized block with original (partial compression)
        const mix = 0.6;
        dst.data[i] = (rAvg * mix + src.data[i] * (1-mix)) | 0;
        dst.data[i+1] = (gAvg * mix + src.data[i+1] * (1-mix)) | 0;
        dst.data[i+2] = (bAvg * mix + src.data[i+2] * (1-mix)) | 0;
        dst.data[i+3] = 255;

        // Block boundary emphasis
        if (x === bx || y === by) {
          dst.data[i] = Math.max(0, dst.data[i] - 15);
          dst.data[i+1] = Math.max(0, dst.data[i+1] - 15);
          dst.data[i+2] = Math.max(0, dst.data[i+2] - 15);
        }
      }
    }
  }
}

// Add ringing artifacts near high-contrast edges
for (let y = 1; y < C.height-1; y++) {
  for (let x = 1; x < C.width-1; x++) {
    const i = (y*C.width+x)*4;
    const dx = Math.abs(src.data[i] - src.data[i+4]);
    const dy = Math.abs(src.data[i] - src.data[(i+C.width*4)]);
    if (dx + dy > 100) {
      const ring = (Math.sin(x*0.8) * 12) | 0;
      dst.data[i] = Math.min(255, Math.max(0, dst.data[i] + ring));
      dst.data[i+1] = Math.min(255, Math.max(0, dst.data[i+1] + ring));
      dst.data[i+2] = Math.min(255, Math.max(0, dst.data[i+2] + ring));
    }
  }
}

ctx.putImageData(dst, 0, 0);

Real JPEG uses DCT (Discrete Cosine Transform), but the visual result is well-approximated by block averaging with quantization. The boundary darkening creates the visible grid lines, and the ringing artifacts near edges simulate the Gibbs phenomenon where high-frequency information is lost. Increasing the quantization level (Q) makes the effect more extreme — try Q=80 for heavily compressed aesthetics.

7. VHS distortion — analog warmth

VHS tape artifacts combine several analog phenomena: tracking errors (horizontal displacement), color bleeding (chroma smearing), head switching noise at the bottom of the frame, and dropout lines where the tape surface is damaged. This simulation layers all of these for an authentic retro video look.

const C = document.createElement('canvas');
C.width = 480; C.height = 360;
document.body.appendChild(C);
const ctx = C.getContext('2d');

let t = 0;
function render() {
  // Background scene
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, C.width, C.height);
  for (let i = 0; i < 5; i++) {
    const y = (C.height * 0.3 + i * 50 + Math.sin(t*0.02+i)*5) | 0;
    ctx.fillStyle = `hsl(${200+i*30}, 70%, ${40+i*8}%)`;
    ctx.fillRect(40, y, C.width-80, 35);
  }
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 28px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('VHS TRACKING', C.width/2, 80);
  ctx.font = '16px monospace';
  ctx.fillText('REC \u25CF', 60, 30);
  ctx.fillText(`${String(t/60|0).padStart(2,'0')}:${String(t%60).padStart(2,'0')}`, C.width-70, 30);

  const img = ctx.getImageData(0, 0, C.width, C.height);
  const d = img.data;
  const w = C.width, h = C.height;

  // Tracking error — horizontal wave displacement
  const trackY = (h * 0.6 + Math.sin(t * 0.04) * h * 0.3) | 0;
  const trackH = 20 + Math.sin(t * 0.07) * 15 | 0;
  for (let y = trackY; y < Math.min(trackY + trackH, h); y++) {
    const shift = (Math.sin(y * 0.3 + t * 0.1) * 30) | 0;
    const row = y * w * 4;
    if (shift > 0) {
      for (let x = w-1; x >= shift; x--) {
        const di = row+x*4, si = row+(x-shift)*4;
        d[di]=d[si]; d[di+1]=d[si+1]; d[di+2]=d[si+2];
      }
    }
  }

  // Color bleeding — smear chroma horizontally
  for (let y = 0; y < h; y++) {
    for (let x = 2; x < w; x++) {
      const i = (y*w+x)*4;
      const pi = (y*w+x-2)*4;
      d[i+1] = (d[i+1]*0.7 + d[pi+1]*0.3) | 0; // G bleeds
      d[i+2] = (d[i+2]*0.6 + d[pi+2]*0.4) | 0; // B bleeds more
    }
  }

  // Head switch noise at bottom
  for (let y = h-12; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const i = (y*w+x)*4;
      if (Math.random() < 0.3) {
        d[i] = d[i+1] = d[i+2] = Math.random() * 255 | 0;
      }
    }
  }

  // Dropout lines
  if (Math.random() < 0.1) {
    const dy = Math.random() * h | 0;
    for (let x = 0; x < w; x++) {
      const i = (dy*w+x)*4;
      d[i] = d[i+1] = d[i+2] = 255;
    }
  }

  // Overall noise
  for (let i = 0; i < d.length; i += 4) {
    const n = (Math.random() - 0.5) * 20 | 0;
    d[i] = Math.min(255, Math.max(0, d[i]+n));
    d[i+1] = Math.min(255, Math.max(0, d[i+1]+n));
    d[i+2] = Math.min(255, Math.max(0, d[i+2]+n));
  }

  ctx.putImageData(img, 0, 0);
  t++;
  requestAnimationFrame(render);
}
render();

The tracking error follows a sine wave that slowly traverses the frame height — exactly like a VHS player with a slightly misaligned head drum. Color bleeding is asymmetric (blue bleeds more than green) because VHS stores chroma at lower bandwidth than luma. The head switching noise at the bottom 12 pixels is where the video head physically switches between recordings — a signature VHS artifact. Together they create an unmistakable analog video aesthetic.

8. Interactive glitch painting — controlled chaos

This final example turns glitch into an instrument. Move the mouse to paint glitch effects onto a canvas: the position controls where corruption happens, speed controls intensity, and the mouse button switches between glitch modes. It combines creative coding interaction with glitch aesthetics for a tool that produces unique results every time.

const C = document.createElement('canvas');
C.width = 480; C.height = 360;
document.body.appendChild(C);
const ctx = C.getContext('2d');

// Initial gradient
const grad = ctx.createLinearGradient(0,0,C.width,C.height);
grad.addColorStop(0, '#1a1a2e');
grad.addColorStop(0.3, '#16213e');
grad.addColorStop(0.6, '#0f3460');
grad.addColorStop(1, '#533483');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, C.width, C.height);

let mx = 0, my = 0, pmx = 0, pmy = 0, down = false;

C.addEventListener('mousemove', e => {
  const r = C.getBoundingClientRect();
  pmx = mx; pmy = my;
  mx = (e.clientX - r.left) * (C.width / r.width) | 0;
  my = (e.clientY - r.top) * (C.height / r.height) | 0;
});
C.addEventListener('mousedown', () => down = true);
C.addEventListener('mouseup', () => down = false);

function render() {
  const speed = Math.hypot(mx-pmx, my-pmy);
  if (speed < 1) { requestAnimationFrame(render); return; }

  const img = ctx.getImageData(0, 0, C.width, C.height);
  const d = img.data;
  const w = C.width, h = C.height;
  const radius = Math.min(80, 10 + speed * 3) | 0;
  const intensity = Math.min(1, speed / 30);

  if (down) {
    // Mode 2: pixel sort in region
    for (let row = Math.max(0,my-radius); row < Math.min(h,my+radius); row++) {
      const xs = Math.max(0, mx - radius);
      const xe = Math.min(w, mx + radius);
      const span = [];
      for (let x = xs; x < xe; x++) {
        const i = (row*w+x)*4;
        span.push([d[i],d[i+1],d[i+2]]);
      }
      span.sort((a,b) => (a[0]+a[1]+a[2]) - (b[0]+b[1]+b[2]));
      for (let i = 0; i < span.length; i++) {
        const idx = (row*w+(xs+i))*4;
        d[idx]=span[i][0]; d[idx+1]=span[i][1]; d[idx+2]=span[i][2];
      }
    }
  } else {
    // Mode 1: channel shift + block displace
    for (let y = Math.max(0,my-radius); y < Math.min(h,my+radius); y++) {
      for (let x = Math.max(0,mx-radius); x < Math.min(w,mx+radius); x++) {
        const dist = Math.hypot(x-mx, y-my);
        if (dist > radius) continue;
        const i = (y*w+x)*4;
        const shift = (intensity * 15 * (1 - dist/radius)) | 0;
        // RGB shift
        const sx = Math.min(w-1, x + shift);
        const si = (y*w+sx)*4;
        d[i] = d[si]; // R from shifted position
        const bx = Math.max(0, x - shift);
        const bi = (y*w+bx)*4;
        d[i+2] = d[bi+2]; // B from opposite shift
      }
    }
    // Random block displacement near cursor
    if (speed > 5) {
      const bw = 10 + Math.random()*40 | 0;
      const bh = 5 + Math.random()*15 | 0;
      const bx = mx - bw/2 + (Math.random()-0.5)*radius | 0;
      const by = my - bh/2 + (Math.random()-0.5)*radius | 0;
      for (let y = Math.max(0,by); y < Math.min(h,by+bh); y++) {
        for (let x = Math.max(0,bx); x < Math.min(w,bx+bw); x++) {
          const di = (y*w+x)*4;
          const sx = x + (Math.random()-0.5)*20 | 0;
          const sy = y + (Math.random()-0.5)*10 | 0;
          if (sx >= 0 && sx < w && sy >= 0 && sy < h) {
            const si = (sy*w+sx)*4;
            d[di]=d[si]; d[di+1]=d[si+1]; d[di+2]=d[si+2];
          }
        }
      }
    }
  }

  ctx.putImageData(img, 0, 0);
  requestAnimationFrame(render);
}
render();

The two modes create distinct textures: mouse-up paints chromatic aberration and block noise (chaotic), mouse-down applies pixel sorting (ordered). Speed maps to intensity — slow movements create subtle color fringing, fast strokes leave dramatic tears. The radius grows with speed too, so quick gestures affect larger areas. The initial dark gradient provides a rich starting canvas that reveals beautiful colors as the glitch effects redistribute pixels across it.

Tips for more compelling glitch art

  • Preserve enough of the original. The best glitch art maintains a tension between recognition and corruption. If the source image is completely destroyed, it just looks like noise. If too much survives, the glitch feels timid. Aim for 30-60% corruption — enough to disturb, not enough to obliterate.
  • Use thresholds to target specific tonal ranges. Pixel sorting is most dramatic when applied only to mid-tones, leaving highlights and shadows as anchors. The same principle applies to channel shifts — corrupt the areas of interest, preserve the edges.
  • Layer multiple techniques. A channel shift alone looks simplistic. Add scan lines, a few displaced blocks, some noise, and suddenly it looks authentic. Real glitches are never just one failure mode.
  • Temporal variation matters for animation. Constant glitching is monotonous. Use sine waves, random bursts with cooldown periods, or easing functions to create rhythm. Moments of clarity make the corrupted frames more impactful.
  • Work with the color space. RGB channel shifting is standard, but converting to HSL first lets you corrupt hue independently of brightness — glitching the color without destroying the form. Or corrupt only the saturation channel for desaturation artifacts.

The art of deliberate failure

Glitch art occupies a unique position in creative coding: it's one of the few art forms defined by what goes wrong rather than what goes right. Every technique in this guide simulates a specific failure mode — analog video degradation, digital compression errors, cosmic ray bit flips, buffer overflows. Understanding the failure gives you control over the aesthetic.

The techniques here combine naturally with other creative coding approaches:

On Lumitree, glitch aesthetics appear in the art world branches — micro-worlds where pixels rebel against their orderly grid. Some branches run perfect simulations that periodically corrupt themselves. Others start glitched and gradually resolve into clarity. Each one lives in under 50KB because glitch effects are computationally cheap — you're breaking data, not building it. And breaking things has always been faster than making them.

Start with the RGB channel shift (example 1) to understand the fundamentals. Try pixel sorting (example 2) for the most photogenic results. Experiment with the interactive painter (example 8) to develop an intuition for how different glitch modes feel as brush strokes. Then layer VHS distortion over JPEG artifacts over channel shifts, and discover the rich aesthetic space where digital decay becomes digital art.

Related articles