About this technique →
boids-2d/sketch.js
// Boids 2D
//
// Craig Reynolds's 1986 flocking algorithm — three local rules (separate, align, cohere)
// acting on each agent produce flocks that turn, swarm, and split as if intelligent.
// Each boid senses neighbors within a radius and adjusts velocity by a weighted
// sum of three steering forces. Edges wrap. Boids render as small triangles.

export const WARMUP = { framesBeforeReady: 240 };

export const PARAMS = {
  count:           { value: 200, min: 20, max: 600, step: 5, label: "Boid Count", folder: "Structure", rebuildOnChange: true },
  separationRadius:{ value: 45, min: 5, max: 100, step: 1, label: "Separation Radius", folder: "Behavior" },
  alignRadius:     { value: 54, min: 10, max: 200, step: 1, label: "Align/Cohesion Radius", folder: "Behavior" },
  separation:      { value: 1.85, min: 0, max: 4, step: 0.05, label: "Separation Weight", folder: "Behavior" },
  alignment:       { value: 0.95, min: 0, max: 4, step: 0.05, label: "Alignment Weight", folder: "Behavior" },
  cohesion:        { value: 0,    min: 0, max: 4, step: 0.05, label: "Cohesion Weight", folder: "Behavior" },
  maxSpeed:        { value: 3.0, min: 0.5, max: 8, step: 0.1, label: "Max Speed", folder: "Behavior" },
  minSpeed:        { value: 1.5, min: 0, max: 4, step: 0.1, label: "Min Speed", folder: "Behavior" },
  maxForce:        { value: 0.06, min: 0.01, max: 0.5, step: 0.005, label: "Max Steer Force", folder: "Behavior" },
  noise:           { value: 0.62, min: 0, max: 1.0, step: 0.005, label: "Random Noise", folder: "Behavior" },
  trail:           { value: 0.08, min: 0, max: 0.3, step: 0.01, label: "Trail Fade", folder: "Appearance" },
  hue:             { value: 200, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
};

export const SHARE = {
  bookmarked: ["count", "separationRadius", "alignRadius", "separation", "alignment", "cohesion", "maxSpeed", "minSpeed", "noise", "trail", "hue"],
};

let state;

class Boid {
  constructor(x, y, vx, vy) {
    this.x  = x;
    this.y  = y;
    this.vx = vx;
    this.vy = vy;
  }
}

export function sketchSetup(p, w, h, params) {
  p.colorMode(p.HSB, 360, 100, 100, 1);
  p.background(0);

  const n = params.count;
  const boids = [];
  for (let i = 0; i < n; i++) {
    const angle = p.random(Math.PI * 2);
    const speed = p.random(1, 3);
    boids.push(new Boid(
      p.random(w),
      p.random(h),
      Math.cos(angle) * speed,
      Math.sin(angle) * speed,
    ));
  }

  state = { boids, w, h };
  return state;
}

// Reynolds steering helper: compute the bounded steering force toward a desired
// velocity vector. Returns [sx, sy] with magnitude ≤ maxForce.
function steer(desiredX, desiredY, vx, vy, maxSpeed, maxForce) {
  const dmag = Math.hypot(desiredX, desiredY);
  if (dmag === 0) return [0, 0];
  // Scale the desired direction to maxSpeed, then subtract current velocity
  let sx = (desiredX / dmag) * maxSpeed - vx;
  let sy = (desiredY / dmag) * maxSpeed - vy;
  const smag = Math.hypot(sx, sy);
  if (smag > maxForce && smag > 0) {
    sx *= maxForce / smag;
    sy *= maxForce / smag;
  }
  return [sx, sy];
}

// Wrap coordinate into [0, bound)
function wrapCoord(v, bound) {
  return ((v % bound) + bound) % bound;
}

// Limit vector magnitude to max
function limitVec(vx, vy, max) {
  const mag = Math.sqrt(vx * vx + vy * vy);
  if (mag > max && mag > 0) {
    const s = max / mag;
    return [vx * s, vy * s];
  }
  return [vx, vy];
}

export function sketchDraw(p, w, h, params) {
  // Fade previous frame for trail effect
  p.noStroke();
  p.fill(0, 0, 0, params.trail);
  p.rect(0, 0, w, h);

  const { boids } = state;
  const n = boids.length;
  const sepR  = params.separationRadius;
  const sepR2 = sepR * sepR;
  const algR  = params.alignRadius;
  const algR2 = algR * algR;
  const maxSpd = params.maxSpeed;

  // Compute new velocities
  const newVx = new Float32Array(n);
  const newVy = new Float32Array(n);

  const maxForce = params.maxForce;

  for (let i = 0; i < n; i++) {
    const bi = boids[i];

    // Separation: weighted-away vector summed (each push proportional to 1/dist
    // so closer neighbors push harder). Using a SMALLER radius so it only fires
    // when boids are uncomfortably close.
    let sepX = 0, sepY = 0;
    let sepCount = 0;

    // Alignment + cohesion: averaged neighbor velocity / displacement.
    let alignX = 0, alignY = 0;
    let dispX = 0, dispY = 0;  // toroidal displacement sum (centroid - self)
    let algCount = 0;

    for (let j = 0; j < n; j++) {
      if (i === j) continue;
      const bj = boids[j];

      // Toroidal shortest-path delta
      let dx = bj.x - bi.x;
      let dy = bj.y - bi.y;
      if (dx >  w * 0.5) dx -= w;
      if (dx < -w * 0.5) dx += w;
      if (dy >  h * 0.5) dy -= h;
      if (dy < -h * 0.5) dy += h;

      const dist2 = dx * dx + dy * dy;

      if (dist2 < sepR2 && dist2 > 0) {
        const dist = Math.sqrt(dist2);
        // Push direction is FROM neighbor TO self (so the boid moves away)
        // weighted by 1/dist (closer neighbors push harder).
        sepX += -dx / dist / dist;
        sepY += -dy / dist / dist;
        sepCount++;
      }

      if (dist2 < algR2) {
        alignX += bj.vx;
        alignY += bj.vy;
        dispX += dx;
        dispY += dy;
        algCount++;
      }
    }

    // Build steering forces, each as a desired velocity vector, then convert
    // to a bounded steer via Reynolds's standard formula.
    let fx = 0, fy = 0;

    if (sepCount > 0) {
      // sepX/sepY is already a direction-summed vector pointing away from neighbors.
      const [sx, sy] = steer(sepX, sepY, bi.vx, bi.vy, maxSpd, maxForce);
      fx += sx * params.separation;
      fy += sy * params.separation;
    }

    if (algCount > 0) {
      // Alignment: desired = average neighbor velocity (DIRECTION).
      const avx = alignX / algCount;
      const avy = alignY / algCount;
      const [sx, sy] = steer(avx, avy, bi.vx, bi.vy, maxSpd, maxForce);
      fx += sx * params.alignment;
      fy += sy * params.alignment;

      // Cohesion: desired = average toroidal displacement (= centroid - self).
      const cx = dispX / algCount;
      const cy = dispY / algCount;
      const [csx, csy] = steer(cx, cy, bi.vx, bi.vy, maxSpd, maxForce);
      fx += csx * params.cohesion;
      fy += csy * params.cohesion;
    }

    // Random jitter — gentle wobble that breaks symmetry over time.
    const noiseAmt = params.noise;
    fx += (p.random() - 0.5) * noiseAmt;
    fy += (p.random() - 0.5) * noiseAmt;

    let nvx = bi.vx + fx;
    let nvy = bi.vy + fy;

    [newVx[i], newVy[i]] = limitVec(nvx, nvy, maxSpd);

    // Enforce minimum speed floor.
    const minSpd = params.minSpeed;
    const spd = Math.sqrt(newVx[i] * newVx[i] + newVy[i] * newVy[i]);
    if (spd < minSpd && spd > 0) {
      const s = minSpd / spd;
      newVx[i] *= s;
      newVy[i] *= s;
    } else if (spd === 0) {
      const angle = p.random(Math.PI * 2);
      newVx[i] = Math.cos(angle) * minSpd;
      newVy[i] = Math.sin(angle) * minSpd;
    }
  }

  // Draw boids as triangles and advance positions
  const hueBase = params.hue;

  for (let i = 0; i < n; i++) {
    const bi = boids[i];
    bi.vx = newVx[i];
    bi.vy = newVy[i];

    bi.x = wrapCoord(bi.x + bi.vx, w);
    bi.y = wrapCoord(bi.y + bi.vy, h);

    // Draw as triangle pointing in velocity direction
    const angle = Math.atan2(bi.vy, bi.vx);
    const speed  = Math.sqrt(bi.vx * bi.vx + bi.vy * bi.vy);

    // Hue shifts slightly with speed for visual depth
    const hue = ((hueBase + speed * 15) % 360 + 360) % 360;

    p.push();
    p.translate(bi.x, bi.y);
    p.rotate(angle);

    p.fill(hue, 70, 90, 0.85);
    p.noStroke();

    // Triangle: tip at front, base at back
    const len = 8;
    const w2  = 3;
    p.triangle(len, 0, -w2, -w2, -w2, w2);

    p.pop();
  }
}