live - 60fps 1920 x 1080
<!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.