library / particles & systems / flocking-boids
live - 60fps 1920 x 1080
flocking-boids.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Flocking Boids</title>
<style>
    :root { --bg-color: #060a14; --boid-1: #38bdf8; --boid-2: #c084fc; }
    * { 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') || '#060a14').trim();
    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];}
    const c1 = hex(css.getPropertyValue('--boid-1') || '#38bdf8');
    const c2 = hex(css.getPropertyValue('--boid-2') || '#c084fc');
    let w, h; const N = 140, boids = [];
    const mouse = { x: -1e4, y: -1e4 };
    function build() {
        w = cv.width = innerWidth; h = cv.height = innerHeight;
        boids.length = 0;
        for (let i = 0; i < N; i++) boids.push({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2 });
    }
    addEventListener('resize', build);
    addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
    addEventListener('mouseleave', () => { mouse.x = mouse.y = -1e4; });
    build();
    const R = 60, R2 = R * R, SEP = 24 * 24, MAX = 3.4;
    function step() {
        ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h);
        for (const b of boids) {
            let ax = 0, ay = 0, cxs = 0, cys = 0, sx = 0, sy = 0, n = 0;
            for (const o of boids) {
                if (o === b) continue;
                const dx = o.x - b.x, dy = o.y - b.y, d2 = dx * dx + dy * dy;
                if (d2 < R2) {
                    ax += o.vx; ay += o.vy; cxs += o.x; cys += o.y; n++;
                    if (d2 < SEP) { sx -= dx; sy -= dy; }
                }
            }
            if (n > 0) {
                b.vx += (ax / n - b.vx) * 0.05 + (cxs / n - b.x) * 0.0009;
                b.vy += (ay / n - b.vy) * 0.05 + (cys / n - b.y) * 0.0009;
            }
            b.vx += sx * 0.008; b.vy += sy * 0.008;
            // flee cursor
            const mdx = b.x - mouse.x, mdy = b.y - mouse.y, md2 = mdx * mdx + mdy * mdy;
            if (md2 < 140 * 140) { b.vx += mdx / (md2 + 40) * 90; b.vy += mdy / (md2 + 40) * 90; }
            const sp = Math.hypot(b.vx, b.vy); if (sp > MAX) { b.vx = b.vx / sp * MAX; b.vy = b.vy / sp * MAX; }
            b.x += b.vx; b.y += b.vy;
            if (b.x < 0) b.x += w; else if (b.x > w) b.x -= w;
            if (b.y < 0) b.y += h; else if (b.y > h) b.y -= h;
            const sp2 = Math.min(1, sp / MAX);
            ctx.fillStyle = `rgb(${c1[0]+(c2[0]-c1[0])*sp2|0},${c1[1]+(c2[1]-c1[1])*sp2|0},${c1[2]+(c2[2]-c1[2])*sp2|0})`;
            const a = Math.atan2(b.vy, b.vx);
            ctx.beginPath();
            ctx.moveTo(b.x + Math.cos(a) * 6, b.y + Math.sin(a) * 6);
            ctx.lineTo(b.x + Math.cos(a + 2.5) * 4, b.y + Math.sin(a + 2.5) * 4);
            ctx.lineTo(b.x + Math.cos(a - 2.5) * 4, b.y + Math.sin(a - 2.5) * 4);
            ctx.closePath(); ctx.fill();
        }
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) { for (let i = 0; i < 60; i++) step(); }
    else (function loop() { step(); requestAnimationFrame(loop); })();
</script>
</body>
</html>

About this animation

Hundreds of boids swarm with lifelike flocking — separating, aligning, and drawing together into murmurations that scatter from your cursor.

Source Code
Copied to clipboard