library / backgrounds / dither-drift
live - 60fps 1920 x 1080
dither-drift.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Dither Drift</title>
<style>
    :root {
        --bg-color: #0b0f1a;
        --tone-1: #0b0f1a;
        --tone-2: #3b4d9c;
        --tone-3: #38bdf8;
        --tone-4: #f0abfc;
    }
    * { margin: 0; padding: 0; }
    body { height: 100vh; background: var(--bg-color); overflow: hidden; }
    canvas { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; }
</style>
</head>
<body>
    <canvas id="c"></canvas>
<script>
    const cv = document.getElementById('c');
    const ctx = cv.getContext('2d');
    ctx.imageSmoothingEnabled = false;
    const css = getComputedStyle(document.documentElement);
    function hex(h) {
        h = (h || '').trim().replace('#', '');
        if (h.length === 3) h = h.split('').map(c => c + c).join('');
        const n = parseInt(h, 16);
        return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
    }
    // Ordered ramp of tones — dithering blends between adjacent stops
    const pal = [
        hex(css.getPropertyValue('--tone-1') || '#0b0f1a'),
        hex(css.getPropertyValue('--tone-2') || '#3b4d9c'),
        hex(css.getPropertyValue('--tone-3') || '#38bdf8'),
        hex(css.getPropertyValue('--tone-4') || '#f0abfc')
    ];
    // 8x8 Bayer ordered-dither matrix (normalized 0..1)
    const B = [
        0,48,12,60,3,51,15,63, 32,16,44,28,35,19,47,31,
        8,56,4,52,11,59,7,55, 40,24,36,20,43,27,39,23,
        2,50,14,62,1,49,13,61, 34,18,46,30,33,17,45,29,
        10,58,6,54,9,57,5,53, 42,26,38,22,41,25,37,21
    ].map(v => (v + 0.5) / 64);
    const SCALE = 4; // low-res factor -> chunky dither
    let lw, lh, img, buf;
    function build() {
        cv.width = Math.ceil(innerWidth / SCALE);
        cv.height = Math.ceil(innerHeight / SCALE);
        lw = cv.width; lh = cv.height;
        img = ctx.createImageData(lw, lh);
        buf = img.data;
    }
    addEventListener('resize', build);
    build();
    function step(t) {
        const levels = pal.length - 1;
        for (let y = 0; y < lh; y++) {
            for (let x = 0; x < lw; x++) {
                const nx = x / lw, ny = y / lh;
                // flowing plasma field in 0..1
                let v = 0.5 + 0.5 * (
                    Math.sin(nx * 6 + t) * 0.5 +
                    Math.sin((nx + ny) * 5 - t * 0.8) * 0.3 +
                    Math.sin(Math.hypot(nx - 0.5, ny - 0.5) * 10 - t * 1.2) * 0.4
                ) / 1.2;
                v = Math.max(0, Math.min(0.999, v));
                const scaled = v * levels;
                let lvl = Math.floor(scaled);
                const frac = scaled - lvl;
                // ordered dither between palette stop lvl and lvl+1
                if (frac > B[(y & 7) * 8 + (x & 7)]) lvl++;
                if (lvl > levels) lvl = levels;
                const c = pal[lvl];
                const i = (y * lw + x) * 4;
                buf[i] = c[0]; buf[i + 1] = c[1]; buf[i + 2] = c[2]; buf[i + 3] = 255;
            }
        }
        ctx.putImageData(img, 0, 0);
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
        step(1.2);
    } else {
        let t = 0;
        (function loop() { t += 0.02; step(t); requestAnimationFrame(loop); })();
    }
</script>
</body>
</html>

About this animation

A smooth plasma gradient rendered through an ordered Bayer dither into chunky retro pixels — the crunchy, nostalgic-yet-modern gradient look.

Source Code
Copied to clipboard