library / retro & cyberpunk / electric-arcs
live - 60fps 1920 x 1080
electric-arcs.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Electric Arcs</title>
<style>
    :root { --bg-color: #04060f; --bolt-color: #7dd3fc; --bolt-core: #ffffff; }
    * { 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'), ctx = cv.getContext('2d');
    const css = getComputedStyle(document.documentElement);
    const bg = (css.getPropertyValue('--bg-color') || '#04060f').trim();
    const bolt = (css.getPropertyValue('--bolt-color') || '#7dd3fc').trim();
    const core = (css.getPropertyValue('--bolt-core') || '#ffffff').trim();
    let w, h; const arcs = [];
    const mouse = { x: -1, y: -1 };
    function build() { w = cv.width = innerWidth; h = cv.height = innerHeight; }
    addEventListener('resize', build);
    addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
    build();
    function makePath(x1, y1, x2, y2, disp, depth, branches, out) {
        let pts = [[x1, y1], [x2, y2]];
        for (let g = 0; g < depth; g++) {
            const np = [];
            for (let i = 0; i < pts.length - 1; i++) {
                const a = pts[i], b = pts[i + 1];
                const mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2;
                let nx = -(b[1] - a[1]), ny = (b[0] - a[0]); const len = Math.hypot(nx, ny) || 1;
                const o = (Math.random() - 0.5) * disp;
                np.push(a); np.push([mx + nx / len * o, my + ny / len * o]);
            }
            np.push(pts[pts.length - 1]); pts = np; disp *= 0.55;
        }
        out.push(pts);
        if (branches > 0 && Math.random() < 0.9) {
            const bi = (Math.random() * (pts.length - 2) | 0) + 1, from = pts[bi];
            makePath(from[0], from[1], from[0] + (Math.random() - 0.5) * 320, from[1] + (Math.random() - 0.3) * 320, disp * 5, 4, branches - 1, out);
        }
    }
    function spawn(x1, y1, x2, y2) {
        const paths = []; makePath(x1, y1, x2, y2, Math.hypot(x2 - x1, y2 - y1) * 0.34, 6, 3, paths);
        arcs.push({ paths, life: 1, end: [x2, y2] });
    }
    function strokePaths(a, width) { for (const p of a.paths) { ctx.beginPath(); ctx.moveTo(p[0][0], p[0][1]); for (let k = 1; k < p.length; k++) ctx.lineTo(p[k][0], p[k][1]); ctx.stroke(); } }
    let acc = 0;
    function step() {
        ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h);
        acc++;
        if (acc % 16 === 0 || arcs.length === 0) {
            const n = 1 + (Math.random() < 0.5 ? 1 : 0);
            for (let b = 0; b < n; b++) {
                const fromTop = Math.random() < 0.5;
                const x1 = fromTop ? Math.random() * w : (Math.random() < 0.5 ? 0 : w), y1 = fromTop ? 0 : Math.random() * h;
                const useM = mouse.x > 0 && Math.random() < 0.55;
                spawn(x1, y1, useM ? mouse.x : Math.random() * w, useM ? mouse.y : Math.random() * h);
            }
        }
        ctx.lineJoin = 'round'; ctx.lineCap = 'round';
        for (let i = arcs.length - 1; i >= 0; i--) {
            const a = arcs[i]; a.life -= 0.045;
            if (a.life <= 0) { arcs.splice(i, 1); continue; }
            // strike flash at the endpoint
            const fg = ctx.createRadialGradient(a.end[0], a.end[1], 0, a.end[0], a.end[1], 90);
            fg.addColorStop(0, bolt); fg.addColorStop(1, 'rgba(0,0,0,0)');
            ctx.globalAlpha = a.life * 0.4; ctx.fillStyle = fg;
            ctx.beginPath(); ctx.arc(a.end[0], a.end[1], 90, 0, Math.PI * 2); ctx.fill();
            // glow pass
            ctx.shadowBlur = 18; ctx.shadowColor = bolt;
            ctx.strokeStyle = bolt; ctx.globalAlpha = a.life * 0.9; ctx.lineWidth = 3.4; strokePaths(a);
            // bright core
            ctx.shadowBlur = 6; ctx.shadowColor = core;
            ctx.strokeStyle = core; ctx.globalAlpha = a.life; ctx.lineWidth = 1.6; strokePaths(a);
        }
        ctx.shadowBlur = 0; ctx.globalAlpha = 1;
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) { ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h); spawn(w * 0.3, 0, w * 0.6, h * 0.7); spawn(w * 0.7, 0, w * 0.4, h * 0.6); step(); }
    else (function loop() { step(); requestAnimationFrame(loop); })();
</script>
</body>
</html>

About this animation

Branching bolts of neon lightning crackle across the dark, striking toward your cursor with a glowing flash at every impact.

Source Code
Copied to clipboard