library / backgrounds / topographic-drift
live - 60fps 1920 x 1080
topographic-drift.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Topographic Drift</title>
<style>
    :root {
        --bg-color: #060a12;
        --line-color: #2dd4bf;
        --peak-color: #f0abfc;
    }
    * { margin: 0; padding: 0; }
    body { height: 100vh; background: var(--bg-color); overflow: hidden; }
    canvas { display: block; }
</style>
</head>
<body>
    <canvas id="c"></canvas>
<script>
    const cv = document.getElementById('c');
    const ctx = cv.getContext('2d');
    const css = getComputedStyle(document.documentElement);
    const bg = (css.getPropertyValue('--bg-color') || '#060a12').trim();
    const lineCol = (css.getPropertyValue('--line-color') || '#2dd4bf').trim();
    const peakCol = (css.getPropertyValue('--peak-color') || '#f0abfc').trim();
    const CELL = 12;
    const LEVELS = 18;
    let w, h, cols, rows;
    function build() {
        w = cv.width = innerWidth;
        h = cv.height = innerHeight;
        cols = Math.ceil(w / CELL) + 1;
        rows = Math.ceil(h / CELL) + 1;
    }
    addEventListener('resize', build);
    build();
    // Height field 0..1 from layered trig, drifting with time
    function fieldVal(gx, gy, t) {
        const x = gx * CELL, y = gy * CELL;
        const s = 0.0042;
        let v = Math.sin(x * s + t) * Math.cos(y * s * 1.2 - t * 0.5)
              + Math.sin((x - y) * s * 0.8 + t * 0.6) * 0.8
              + Math.cos(Math.hypot(x - w / 2, y - h / 2) * s * 1.1 - t) * 0.6
              + Math.sin(x * s * 2.1 - y * s * 1.7 + t * 0.3) * 0.4;
        return (v / 2.8) * 0.5 + 0.5;
    }
    // linear interpolation of crossing point on a cell edge
    function lerp(a, b, iso) { return (iso - a) / (b - a); }
    function step(t) {
        ctx.fillStyle = bg;
        ctx.fillRect(0, 0, w, h);
        // sample grid once per frame
        const grid = new Float32Array(cols * rows);
        for (let j = 0; j < rows; j++)
            for (let i = 0; i < cols; i++)
                grid[j * cols + i] = fieldVal(i, j, t);
        for (let l = 1; l < LEVELS; l++) {
            const iso = l / LEVELS;
            const isPeak = l >= LEVELS - 2;
            ctx.strokeStyle = isPeak ? peakCol : lineCol;
            ctx.globalAlpha = isPeak ? 0.9 : 0.35 + (l / LEVELS) * 0.4;
            ctx.lineWidth = isPeak ? 1.4 : 1;
            ctx.beginPath();
            for (let j = 0; j < rows - 1; j++) {
                for (let i = 0; i < cols - 1; i++) {
                    const tl = grid[j * cols + i];
                    const tr = grid[j * cols + i + 1];
                    const br = grid[(j + 1) * cols + i + 1];
                    const bl = grid[(j + 1) * cols + i];
                    let ci = 0;
                    if (tl > iso) ci |= 8;
                    if (tr > iso) ci |= 4;
                    if (br > iso) ci |= 2;
                    if (bl > iso) ci |= 1;
                    if (ci === 0 || ci === 15) continue;
                    const x0 = i * CELL, y0 = j * CELL;
                    const T = { x: x0 + lerp(tl, tr, iso) * CELL, y: y0 };
                    const R = { x: x0 + CELL, y: y0 + lerp(tr, br, iso) * CELL };
                    const Bt = { x: x0 + lerp(bl, br, iso) * CELL, y: y0 + CELL };
                    const L = { x: x0, y: y0 + lerp(tl, bl, iso) * CELL };
                    const seg = (p, q) => { ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); };
                    switch (ci) {
                        case 1: case 14: seg(L, Bt); break;
                        case 2: case 13: seg(Bt, R); break;
                        case 3: case 12: seg(L, R); break;
                        case 4: case 11: seg(T, R); break;
                        case 5: seg(L, T); seg(Bt, R); break;
                        case 6: case 9: seg(T, Bt); break;
                        case 7: case 8: seg(L, T); break;
                        case 10: seg(T, R); seg(L, Bt); break;
                    }
                }
            }
            ctx.stroke();
        }
        ctx.globalAlpha = 1;
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
        step(1.5);
    } else {
        let t = 0;
        (function loop() { t += 0.004; step(t); requestAnimationFrame(loop); })();
    }
</script>
</body>
</html>

About this animation

Living contour lines flow and reshape like a topographic map in slow motion, nested rings rising and falling into peaks and valleys.

Source Code
Copied to clipboard