library / retro & cyberpunk / synthwave-grid
live - 60fps 1920 x 1080
synthwave-grid.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Synthwave Grid</title>
<style>
    :root {
        --bg-top: #0a0420;
        --bg-horizon: #ff2e97;
        --grid-color: #22d3ee;
        --sun-color: #ffcc33;
    }
    * { margin: 0; padding: 0; }
    body { height: 100vh; overflow: hidden; background: var(--bg-top); }
    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 bgTop = (css.getPropertyValue('--bg-top') || '#0a0420').trim();
    const horizonCol = (css.getPropertyValue('--bg-horizon') || '#ff2e97').trim();
    const gridCol = (css.getPropertyValue('--grid-color') || '#22d3ee').trim();
    const sunCol = (css.getPropertyValue('--sun-color') || '#ffcc33').trim();
    let w, h, horizon;
    const COLS = 26, ROWS = 40, SPACING = 1;
    const FOCAL = 260, CAM_H = 2.4;
    function build() {
        w = cv.width = innerWidth;
        h = cv.height = innerHeight;
        horizon = h * 0.42;
    }
    addEventListener('resize', build);
    build();
    // deterministic ridge height for a world row/col — flat "road" in the middle
    function height(col, row) {
        const edge = Math.abs(col - COLS / 2) / (COLS / 2);       // 0 centre -> 1 edge
        const ridge = Math.sin(col * 1.3 + row * 0.7) * 0.5 + Math.sin(row * 0.4 + col * 0.9) * 0.5;
        return ridge * edge * edge * 3.2;
    }
    function project(wx, wy, wz) {
        const s = FOCAL / wz;
        return { x: w / 2 + wx * s, y: horizon - (wy - CAM_H) * s, s };
    }
    function drawSky() {
        const g = ctx.createLinearGradient(0, 0, 0, horizon);
        g.addColorStop(0, bgTop);
        g.addColorStop(1, horizonCol);
        ctx.fillStyle = g; ctx.fillRect(0, 0, w, horizon);
        // sun with scanline gaps
        const sr = Math.min(w, h) * 0.16, sx = w / 2, sy = horizon - sr * 0.35;
        const sg = ctx.createLinearGradient(0, sy - sr, 0, sy + sr);
        sg.addColorStop(0, sunCol); sg.addColorStop(1, horizonCol);
        ctx.save();
        ctx.beginPath(); ctx.arc(sx, sy, sr, 0, Math.PI * 2); ctx.clip();
        ctx.fillStyle = sg; ctx.fillRect(sx - sr, sy - sr, sr * 2, sr * 2);
        ctx.fillStyle = bgTop;
        for (let i = 0; i < 9; i++) { const yy = sy + sr * 0.15 + i * sr * 0.14; ctx.fillRect(sx - sr, yy, sr * 2, i * 0.7 + 1); }
        ctx.restore();
        // ground
        const gg = ctx.createLinearGradient(0, horizon, 0, h);
        gg.addColorStop(0, '#1a0a2e'); gg.addColorStop(1, bgTop);
        ctx.fillStyle = gg; ctx.fillRect(0, horizon, w, h - horizon);
    }
    function step(t) {
        ctx.fillStyle = bgTop; ctx.fillRect(0, 0, w, h);
        drawSky();
        const scroll = (t * 2.4) % SPACING;
        const base = Math.floor(t * 2.4);
        ctx.lineWidth = 1;
        // depth lines (running away) + horizontal lines
        for (let r = 0; r < ROWS - 1; r++) {
            const wz1 = (r + 1) * SPACING - scroll;
            const wz2 = (r + 2) * SPACING - scroll;
            if (wz1 <= 0.2) continue;
            const alpha = Math.max(0, 1 - r / (ROWS - 2));
            ctx.strokeStyle = gridCol;
            ctx.globalAlpha = alpha * 0.8;
            ctx.beginPath();
            for (let c = 0; c <= COLS; c++) {
                const wx = (c - COLS / 2) * SPACING;
                const p1 = project(wx, height(c, base + r), wz1);
                const p2 = project(wx, height(c, base + r + 1), wz2);
                ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y);
            }
            // horizontal segment at this row
            for (let c = 0; c < COLS; c++) {
                const wx1 = (c - COLS / 2) * SPACING, wx2 = (c + 1 - COLS / 2) * SPACING;
                const a = project(wx1, height(c, base + r), wz1);
                const b = project(wx2, height(c + 1, base + r), wz1);
                ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
            }
            ctx.stroke();
        }
        ctx.globalAlpha = 1;
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
        step(0.5);
    } else {
        let t = 0;
        (function loop() { t += 0.016; step(t); requestAnimationFrame(loop); })();
    }
</script>
</body>
</html>

About this animation

An endless neon wireframe terrain races toward a retro striped sun on the horizon — the quintessential 80s synthwave flythrough.

Source Code
Copied to clipboard