Flocking Boids
Live previewPerformance varies by device
No account needed to copy or download. Check dependencies and thelicense status before publishing.
<!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.
Under the hood
Canvas Reynolds Boids Flocking
A classic Reynolds boids simulation on
Canvas 2D: 140 boids each steer from neighbours within 60px, aligning velocity by 0.05, cohering toward the neighbour centre by 0.0009, and separating within 24px. Speed is capped at 3.4, boids wrap at the edges and are drawn as small triangles oriented with Math.atan2(), and boids within 140px flee the mouse position.