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