Birds

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.

code.tsx

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 />);

Markdown source · More bulbs by samples · Typebulb home