Boids Flocking Simulation using the Boids algorithm created by Craig Reynolds in 1986, simulating the flocking behavior of birds through emergent complexity from simple rules.
import React from "react";
import { createRoot } from "react-dom/client";
import { useEffect, useRef } from "react";
type Vec2 = { x: number; y: number };
const vec = {
sub: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y }),
add: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y }),
scale: (v: Vec2, s: number): Vec2 => ({ x: v.x * s, y: v.y * s }),
mag: (v: Vec2) => Math.sqrt(v.x * v.x + v.y * v.y),
norm: (v: Vec2): Vec2 => { const m = vec.mag(v); return m > 0 ? vec.scale(v, 1 / m) : { x: 0, y: 0 }; },
limit: (v: Vec2, max: number): Vec2 => { const m = vec.mag(v); return m > max ? vec.scale(vec.norm(v), max) : v; }
};
class Boid {
pos: Vec2;
vel: Vec2;
acc: Vec2 = { x: 0, y: 0 };
flapPhase = 0;
flapTimer = 50 + Math.random() * 100;
isFlapping = false;
constructor(x: number, y: number) {
this.pos = { x, y };
const angle = Math.random() * Math.PI * 2;
this.vel = { x: Math.cos(angle) * 2, y: Math.sin(angle) * 2 };
}
steer(boids: Boid[], radius: number, fn: (o: Boid, d: number) => Vec2, seek = false): Vec2 {
let sum = { x: 0, y: 0 }, count = 0;
for (const o of boids) {
const d = vec.mag(vec.sub(this.pos, o.pos));
if (d > 0 && d < radius) { const v = fn(o, d); sum.x += v.x; sum.y += v.y; count++; }
}
if (!count) return { x: 0, y: 0 };
const avg = vec.scale(sum, 1 / count);
const target = seek ? vec.sub(avg, this.pos) : avg;
return vec.mag(target) ? vec.limit(vec.sub(vec.scale(vec.norm(target), 3), this.vel), 0.03) : { x: 0, y: 0 };
}
flock(boids: Boid[]) {
const sep = this.steer(boids, 55, (o, d) => vec.scale(vec.norm(vec.sub(this.pos, o.pos)), ((55 - d) / 55) ** 2));
const ali = this.steer(boids, 90, o => o.vel);
const coh = this.steer(boids, 90, o => o.pos, true);
this.acc = vec.add(this.acc, vec.add(vec.scale(sep, 1.3), vec.add(vec.scale(ali, 1.2), coh)));
}
update(w: number, h: number) {
this.vel = vec.limit(vec.add(this.vel, this.acc), 3);
this.pos = vec.add(this.pos, this.vel);
this.acc = { x: 0, y: 0 };
this.pos.x = (this.pos.x + w) % w;
this.pos.y = (this.pos.y + h) % h;
this.flapTimer--;
if (this.isFlapping) {
this.flapPhase += 0.5;
if (this.flapTimer <= 0) { this.isFlapping = false; this.flapTimer = 60 + Math.random() * 120; }
} else if (this.flapTimer <= 0) {
this.isFlapping = true; this.flapTimer = 20 + Math.random() * 30; this.flapPhase = 0;
}
}
draw(ctx: CanvasRenderingContext2D) {
ctx.save();
ctx.translate(this.pos.x, this.pos.y);
ctx.rotate(Math.atan2(this.vel.y, this.vel.x));
const f = this.isFlapping ? Math.sin(this.flapPhase) : 0;
const t = 1 - f * 0.4, m = 1 - f * 0.2;
ctx.beginPath();
ctx.moveTo(10, 0);
for (const [x, y] of [[2, -2], [-4, -14 * t], [-6, -10 * m], [-2, -2], [-8, 0], [-2, 2], [-6, 10 * m], [-4, 14 * t], [2, 2]])
ctx.lineTo(x, y);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
function App() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const boidsRef = useRef<Boid[]>([]);
useEffect(() => {
const canvas = canvasRef.current!, ctx = canvas.getContext("2d")!;
const resize = () => { canvas.width = innerWidth; canvas.height = innerHeight; };
resize();
addEventListener("resize", resize);
if (!boidsRef.current.length)
boidsRef.current = Array.from({ length: 100 }, () => new Boid(Math.random() * canvas.width, Math.random() * canvas.height));
let frameId: number;
const animate = () => {
const style = getComputedStyle(document.documentElement);
ctx.fillStyle = style.getPropertyValue("--bg-color");
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = style.getPropertyValue("--bird-color");
for (const b of boidsRef.current) { b.flock(boidsRef.current); b.update(canvas.width, canvas.height); b.draw(ctx); }
frameId = requestAnimationFrame(animate);
};
animate();
return () => { removeEventListener("resize", resize); cancelAnimationFrame(frameId); };
}, []);
return (
<div className="container">
<canvas ref={canvasRef} className="canvas" />
<div className="info">
<h1>Boids Flocking Simulation</h1>
<p>Watch as 100 birds demonstrate emergent flocking behavior through three simple rules:</p>
<ul>
<li><strong>Separation:</strong> Avoid crowding neighbors</li>
<li><strong>Alignment:</strong> Steer toward average heading</li>
<li><strong>Cohesion:</strong> Steer toward average position</li>
</ul>
</div>
</div>
);
}
createRoot(document.getElementById("root")!).render(<App />);