Boids flocking · minimax-m3
Create a full-screen flocking simulation of boids using separation, alignment, and cohesion, drawn as oriented triangles that steer smoothly and flee from the mouse cursor.
transcript · 8 messages
Create a full-screen flocking simulation of boids using separation, alignment, and cohesion, drawn as oriented triangles that steer smoothly and flee from the mouse cursor.
thinking
Let me create a beautiful flocking simulation of boids. I'll make it full-screen, with smooth oriented triangles, classic Reynolds flocking rules (separation, alignment, cohesion), plus mouse flee behavior.
Let me think about the design:
1. **Boids algorithm**:
- Separation: steer away from neighbors within a small radius
- Alignment: steer toward average heading of neighbors
- Cohesion: steer toward average position of neighbors
- Mouse flee: steer away from cursor within some radius
2. **Visual**: Oriented triangles that smoothly rotate to face their heading direction.
3. **Performance**: Use spatial hashing/grid for neighbor queries since we want many boids.
4. **Aesthetics**: Dark background, glowing triangles maybe, with a subtle trail effect.
Let me write a single self-contained index.html with canvas, requestAnimationFrame loop, spatial grid for O(n) neighbor queries.
Design choices:
- ~200-400 boids
- Triangle drawn as 3 points then filled
- Wrapping or soft-bounded edges (let's use soft turning at edges)
- Mouse flee only when mouse is pressed or always when near? Let's always so it's always interactive.
- Add a subtle trail by not fully clearing the canvas (drawing semi-transparent black over it).
Let me code this carefully.
```javascript
const W = window.innerWidth;
const H = window.innerHeight;
class Boid {
constructor() {
this.x = Math.random() * W;
this.y = Math.random() * H;
const angle = Math.random() * Math.PI * 2;
this.vx = Math.cos(angle) * 1.5;
this.vy = Math.sin(angle) * 1.5;
}
update(boids, mouse) {
// separation, alignment, cohesion
// ...
// mouse flee
if (mouse) {
const dx = this.x - mouse.x;
const dy = this.y - mouse.y;
const d2 = dx*dx+dy*dy;
if (d2 < mouseR2) {
const d = Math.sqrt(d2);
const f = (mouseR - d) / mouseR;
this.ax += (dx/d) * f * mouseForce;
this.ay += (dy/d) * f * mouseForce;
}
}
// integrate
this.vx += this.ax;
this.vy += this.ay;
// clamp speed
const sp = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
if (sp > maxSpeed) {
this.vx = this.vx/sp * maxSpeed;
this.vy = this.vy/sp * maxSpeed;
} else if (sp < minSpeed) {
this.vx = this.vx/sp * minSpeed;
this.vy = this.vy/sp * minSpeed;
}
this.x += this.vx;
this.y += this.vy;
// soft walls
// ...
}
}
```
For neighbor finding, let me use a spatial grid.
```javascript
class SpatialGrid {
constructor(w, h, cellSize) {
this.cellSize = cellSize;
this.cols = Math.ceil(w / cellSize);
this.rows = Math.ceil(h / cellSize);
this.cells = new Array(this.cols * this.rows);
for (let i = 0; i < this.cells.length; i++) this.cells[i] = [];
}
clear() {
for (let i = 0; i < this.cells.length; i++) this.cells[i].length = 0;
}
add(b) {
const cx = Math.floor(b.x / this.cellSize);
const cy = Math.floor(b.y / this.cellSize);
this.cells[cy * this.cols + cx].push(b);
}
*neighbors(b, radius) {
const cx = Math.floor(b.x / this.cellSize);
const cy = Math.floor(b.y / this.cellSize);
const cr = Math.ceil(radius / this.cellSize);
for (let dy = -cr; dy <= cr; dy++) {
for (let dx = -cr; dx <= cr; dx++) {
const x = cx + dx, y = cy + dy;
if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) continue;
const cell = this.cells[y * this.cols + x];
for (let i = 0; i < cell.length; i++) yield cell[i];
}
}
}
}
```
Hmm, generators with `for...of` may not be the fastest. Let me use a function that returns an array.
Actually for performance in tight loops with hundreds of boids, let me inline the neighbor search.
Let me think about colors. I want a nice look:
- Dark navy/black background
- Triangles with a hue that varies (e.g., cyan-to-magenta based on speed or position)
- Add a small trail effect
Let me draft this fully and write it.
Performance considerations: With ~300 boids, we have ~90,000 pair checks. With neighbor radius of ~50px and a 60px cell size, each boid checks ~9 cells. Average maybe 10-30 neighbors. That's ~9000 ops, times 300 = manageable.
Actually it's per-boid, 300 boids * ~25 neighbors * 3 ops = 22,500 per frame. Then 300 updates. Plus rendering 300 triangles. Should be fine.
Let me write the complete code.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Flocking Boids</title>
<style>
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
background: #05060d;
overflow: hidden;
cursor: crosshair;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
#hud {
position: fixed;
top: 16px; left: 16px;
color: rgba(220, 230, 255, 0.85);
pointer-events: none;
text-shadow: 0 0 8px rgba(0,0,0,0.6);
font-size: 13px;
line-height: 1.6;
user-select: none;
}
#hud h1 {
margin: 0 0 6px;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
color: #a8e1ff;
}
#hud .row { display: flex; gap: 8px; align-items: center; }
#hud .label { width: 90px; opacity: 0.7; }
#hud input[type=range] { width: 140px; }
#hud .val { width: 32px; text-align: right; opacity: 0.7; font-variant-numeric: tabular-nums;}
#hud kbd {
background: rgba(255,255,255,0.08);
padding: 1px 6px; border-radius: 4px;
border: 1px solid rgba(255,255,255,0.12);
font-size: 11px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<h1>FLOCKING BOIDS</h1>
<div class="row"><span class="label">Separation</span><input type="range" id="sep" min="0" max="3" step="0.01" value="1.6"><span class="val" id="sepV">1.60</span></div>
<div class="row"><span class="label">Alignment</span><input type="range" id="ali" min="0" max="3" step="0.01" value="1.0"><span class="val" id="aliV">1.00</span></div>
<div class="row"><span class="label">Cohesion</span><input type="range" id="coh" min="0" max="3" step="0.01" value="0.8"><span class="val" id="cohV">0.80</span></div>
<div class="row"><span class="label">Flee</span><input type="range" id="flee" min="0" max="3" step="0.01" value="1.6"><span class="val" id="fleeV">1.60</span></div>
<div class="row" style="margin-top:6px"><span class="label">Count</span><input type="range" id="cnt" min="50" max="600" step="10" value="260"><span class="val" id="cntV">260</span></div>
<div style="margin-top:8px; font-size:11px; opacity:0.55">Move mouse near canvas to scatter · <kbd>Space</kbd> panic · <kbd>R</kbd> reset</div>
</div>
<script>
(() => {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let DPR = Math.min(2, window.devicePixelRatio || 1);
let W = 0, H = 0;
function resize() {
DPR = Math.min(2, window.devicePixelRatio || 1);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * DPR;
canvas.height = H * DPR;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// Tunables
const params = {
separation: 1.6,
alignment: 1.0,
cohesion: 0.8,
flee: 1.6,
count: 260,
perceptionR: 60,
sepR: 24,
mouseR: 110,
maxSpeed: 3.2,
minSpeed: 1.6,
maxForce: 0.12,
trailAlpha: 0.18,
};
// Hook up HUD
function bind(id, key) {
const el = document.getElementById(id);
const v = document.getElementById(id + 'V');
el.addEventListener('input', () => {
params[key] = parseFloat(el.value);
v.textContent = (params[key]).toFixed(2);
if (key === 'count') {
v.textContent = Math.round(params.count);
adjustCount();
}
});
}
bind('sep', 'separation');
bind('ali', 'alignment');
bind('coh', 'cohesion');
bind('flee', 'flee');
bind('cnt', 'count');
document.getElementById('sepV').textContent = params.separation.toFixed(2);
document.getElementById('aliV').textContent = params.alignment.toFixed(2);
document.getElementById('cohV').textContent = params.cohesion.toFixed(2);
document.getElementById('fleeV').textContent = params.flee.toFixed(2);
document.getElementById('cntV').textContent = params.count;
// Boid
class Boid {
constructor(x, y) {
this.x = x; this.y = y;
const a = Math.random() * Math.PI * 2;
const s = 1.5 + Math.random() * 1.5;
this.vx = Math.cos(a) * s;
this.vy = Math.sin(a) * s;
this.ax = 0; this.ay = 0;
this.hue = 180 + Math.random() * 80;
}
}
let boids = [];
function spawn(n) {
boids = new Array(n);
for (let i = 0; i < n; i++) {
boids[i] = new Boid(Math.random()*W, Math.random()*H);
}
}
spawn(params.count);
function adjustCount() {
const target = Math.round(params.count);
if (target > boids.length) {
for (let i = boids.length; i < target; i++) boids.push(new Boid(Math.random()*W, Math.random()*H));
} else {
boids.length = target;
}
}
// Spatial grid
const CELL = 50;
let cols, rows;
let grid;
function buildGrid() {
cols = Math.ceil(W / CELL) + 2;
rows = Math.ceil(H / CELL) + 2;
grid = new Array(cols * rows);
for (let i = 0; i < grid.length; i++) grid[i] = [];
}
buildGrid();
window.addEventListener('resize', buildGrid);
let mouse = null;
window.addEventListener('mousemove', (e) => {
mouse = { x: e.clientX, y: e.clientY };
});
window.addEventListener('mouseleave', () => { mouse = null; });
let panic = 0;
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') { panic = 1; e.preventDefault(); }
if (e.code === 'KeyR') {
for (const b of boids) {
b.x = Math.random()*W; b.y = Math.random()*H;
const a = Math.random() * Math.PI * 2;
const s = 1.5 + Math.random() * 1.5;
b.vx = Math.cos(a)*s; b.vy = Math.sin(a)*s;
}
}
});
window.addEventListener('keyup', (e) => {
if (e.code === 'Space') panic = 0;
});
// wrap function boids across edges smoothly? Let's do soft-turn at walls
const margin = 60;
function steer(x, y) { return { x, y }; }
function step() {
// rebuild grid
for (let i = 0; i < grid.length; i++) grid[i].length = 0;
for (let i = 0; i < boids.length; i++) {
const b = boids[i];
const cx = Math.max(0, Math.min(cols-1, Math.floor(b.x / CELL)));
const cy = Math.max(0, Math.min(rows-1, Math.floor(b.y / CELL)));
grid[cy * cols + cx].push(b);
}
const pR = params.perceptionR, pR2 = pR*pR;
const sR = params.sepR, sR2 = sR*sR;
const mR = params.mouseR, mR2 = mR*mR;
const sepW = params.separation;
const aliW = params.alignment;
const cohW = params.cohesion;
const fleeW = params.flee;
const maxF = params.maxForce;
const maxS = params.maxSpeed;
const minS = params.minSpeed;
const n = boids.length;
for (let i = 0; i < n; i++) {
const b = boids[i];
let sepX = 0, sepY = 0;
let aliX = 0, aliY = 0, aliN = 0;
let cohX = 0, cohY = 0, cohN = 0;
const cx = Math.max(0, Math.min(cols-1, Math.floor(b.x / CELL)));
const cy = Math.max(0, Math.min(rows-1, Math.floor(b.y / CELL)));
// search 3x3 neighbor cells (CELL=50, perceptionR=60)
for (let dy = -1; dy <= 1; dy++) {
const ny = cy + dy;
if (ny < 0 || ny >= rows) continue;
for (let dx = -1; dx <= 1; dx++) {
const nx = cx + dx;
if (nx < 0 || nx >= cols) continue;
const cell = grid[ny * cols + nx];
for (let k = 0; k < cell.length; k++) {
const o = cell[k];
if (o === b) continue;
const ddx = o.x - b.x, ddy = o.y - b.y;
const d2 = ddx*ddx + ddy*ddy;
if (d2 > pR2 || d2 === 0) continue;
// alignment + cohesion
aliX += o.vx; aliY += o.vy; aliN++;
cohX += o.x; cohY += o.y; cohN++;
if (d2 < sR2) {
// separation - inverse weighted
const inv = 1 / d2;
sepX -= ddx * inv;
sepY -= ddy * inv;
}
}
}
}
let ax = 0, ay = 0;
// separation force (already a vector)
if (sepX !== 0 || sepY !== 0) {
const len = Math.hypot(sepX, sepY);
if (len > 0) {
// normalize to maxSpeed, then subtract velocity, clamp force
let dx = (sepX/len) * maxS - b.vx;
let dy = (sepY/len) * maxS - b.vy;
const m = Math.hypot(dx, dy);
if (m > maxF) { dx = dx/m*maxF; dy = dy/m*maxF; }
ax += dx * sepW;
ay += dy * sepW;
}
}
// alignment
if (aliN > 0) {
aliX /= aliN; aliY /= aliN;
const len = Math.hypot(aliX, aliY);
if (len > 0) {
let dx = (aliX/len) * maxS - b.vx;
let dy = (aliY/len) * maxS - b.vy;
const m = Math.hypot(dx, dy);
if (m > maxF) { dx = dx/m*maxF; dy = dy/m*maxF; }
ax += dx * aliW;
ay += dy * aliW;
}
}
// cohesion
if (cohN > 0) {
cohX = cohX/cohN - b.x;
cohY = cohY/cohN - b.y;
const len = Math.hypot(cohX, cohY);
if (len > 0) {
let dx = (cohX/len) * maxS - b.vx;
let dy = (cohY/len) * maxS - b.vy;
const m = Math.hypot(dx, dy);
if (m > maxF) { dx = dx/m*maxF; dy = dy/m*maxF; }
ax += dx * cohW;
ay += dy * cohW;
}
}
// mouse flee
if (mouse) {
const ddx = b.x - mouse.x;
const ddy = b.y - mouse.y;
const d2 = ddx*ddx + ddy*ddy;
if (d2 < mR2 && d2 > 0) {
const d = Math.sqrt(d2);
// power increases as cursor approaches
const power = 1 - d/mR;
let dx = (ddx/d) * maxS - b.vx;
let dy = (ddy/d) * maxS - b.vy;
const m = Math.hypot(dx, dy);
if (m > maxF * 3) { dx = dx/m*(maxF*3); dy = dy/m*(maxF*3); }
ax += dx * fleeW * power;
ay += dy * fleeW * power;
}
}
// soft walls
const wx = wall(b.x, W, margin);
const wy = wall(b.y, H, margin);
ax += wx; ay += wy;
// panic space key
if (panic) {
ax += (Math.random()-0.5) * 0.5;
ay += (Math.random()-0.5) * 0.5;
}
b.ax = ax; b.ay = ay;
}
// Integrate after all forces computed
for (let i = 0; i < n; i++) {
const b = boids[i];
b.vx += b.ax;
b.vy += b.ay;
const sp = Math.hypot(b.vx, b.vy);
if (sp > maxS) {
b.vx = b.vx/sp * maxS;
b.vy = b.vy/sp * maxS;
} else if (sp > 0 && sp < minS) {
b.vx = b.vx/sp * minS;
b.vy = b.vy/sp * minS;
}
b.x += b.vx;
b.y += b.vy;
// wrap-around could help explore edges; let's wrap to keep things full-screen
if (b.x < -10) b.x = W + 10;
if (b.x > W + 10) b.x = -10;
if (b.y < -10) b.y = H + 10;
if (b.y > H + 10) b.y = -10;
}
}
function wall(pos, dim, m) {
if (pos < m) return (m - pos) / m * params.maxForce * 4;
if (pos > dim - m) return (dim - m - pos) / m * params.maxForce * 4;
return 0;
}
function render() {
// Trail effect
ctx.fillStyle = 'rgba(5, 6, 13, ' + params.trailAlpha + ')';
ctx.fillRect(0, 0, W, H);
ctx.lineWidth = 1;
ctx.lineJoin = 'round';
const t = performance.now() * 0.001;
const n = boids.length;
const sz = 9; // triangle size
for (let i = 0; i < n; i++) {
const b = boids[i];
const ang = Math.atan2(b.vy, b.vx);
const cos = Math.cos(ang);
const sin = Math.sin(ang);
// triangle in local frame: nose at (sz,0), tail at (-sz/2, +/-sz/2*sqrt(3))
const tip = { x: b.x + cos * sz, y: b.y + sin * sz };
const bL = { x: b.x - cos * (sz*0.55) - sin * (sz*0.7), y: b.y - sin * (sz*0.55) + cos * (sz*0.7) };
const bR = { x: b.x - cos * (sz*0.55) + sin * (sz*0.7), y: b.y - sin * (sz*0.55) - cos * (sz*0.7) };
const speed = Math.hypot(b.vx, b.vy);
const t01 = (speed - params.minSpeed) / (params.maxSpeed - params.minSpeed);
const hue = (180 + t01*120 + (b.x + b.y) * 0.02 + t*15) % 360;
ctx.beginPath();
ctx.moveTo(tip.x, tip.y);
ctx.lineTo(bL.x, bL.y);
ctx.lineTo(bR.x, bR.y);
ctx.closePath();
const sat = 90;
const light = 55 + t01*10;
ctx.fillStyle = `hsla(${hue}, ${sat}%, ${light}%, 0.85)`;
ctx.fill();
// glow stroke
ctx.strokeStyle = `hsla(${hue}, ${sat}%, 70%, 0.35)`;
ctx.stroke();
}
// mouse indicator
if (mouse) {
ctx.beginPath();
ctx.arc(mouse.x, mouse.y, params.mouseR, 0, Math.PI*2);
ctx.strokeStyle = 'rgba(255, 200, 220, 0.10)';
ctx.stroke();
ctx.beginPath();
ctx.arc(mouse.x, mouse.y, 4, 0, Math.PI*2);
ctx.fillStyle = 'rgba(255, 200, 220, 0.7)';
ctx.fill();
}
}
function loop() {
step();
render();
requestAnimationFrame(loop);
}
loop();
})();
</script>
</body>
</html>
```
Wait, I realize I have a bug: I defined `steer(x, y)` and never use it, and I defined `wall(pos, dim, m)` after I use it. JS function hoisting will save us, but let me clean that up.
Also, let me double-check the rule math. The standard approach for Reynolds boids:
- Each rule produces an acceleration vector
- Steering force = desired_velocity - current_velocity, clamped to maxForce
For separation: desired = vector away from neighbors (normalized to maxSpeed)
For alignment: desired = average neighbor velocity, normalized to maxSpeed
For cohesion: desired = vector toward average neighbor position, normalized to maxSpeed
Then desired - current = steering force, clamped to maxForce.
My code is doing that correctly.
A nice touch: the wall function. With wrapping edges, walls are unnecessary, but soft walls can also work. I added both - let me actually keep wrapping only since soft walls may conflict. Let me remove soft walls or maybe keep just a gentle one.
Actually I think wrapping is fine because the boids will mostly flock together. Let me simplify and just use wrapping. But then the edges might look weird. Actually with the mouse flee, having wrapping means they could appear from any direction.
Hmm let me think... For visual interest, let's use soft-turn at walls so they don't wrap. Wait, that creates clutter at edges. Wrap is actually fine for a fullscreen composition. Let me keep wrap.
Actually, "fullscreen flocking" typically uses wrap or bounce. Bounce is more visually obvious but boids pile up. Wrap is more fluid. Let me go with wrap.
Let me remove wall function since unused.
Also, for the trail effect, with light trails (alpha 0.18), the colors should be vibrant against the dark background. Let me also consider: when params.count changes, the boid count slider needs to fire input events properly. I check that.
One more thing: I should call adjustCount when the count slider changes. Done.
I want to refine colors. The current `hue` calculation `(180 + t01*120 + ... ) % 360` -- 180 to 300 is cyan to magenta to pink. That works.
Let me also add a subtle "neighborhood" indication at mouse cursor radius. Already added.
Let me finalize. Actually let me re-examine separation force. The conventional method:
```
sepForce = sum over close neighbors: (self - other) / dist^2 (or normalizer)
then scale that to a desired velocity
```
Some implementations use 1/d weighting, others use just the vector. I'll use 1/d^2 to make close neighbors push stronger, then normalize.
Actually, looking at my code: I do `sepX -= ddx * inv` where inv = 1/d^2, and ddx = (neighbor.x - self.x). So sepX = -sum((neighbor-self) * 1/d^2) = sum((self-neighbor) * 1/d^2). That's correct (away vector weighted by closeness).
Good.
Let me also double check the triangle orientation. In local frame (before rotation):
- Nose: (sz, 0)
- Left: (-sz*0.55, +sz*0.7) -- WAIT, +y is down in canvas, so left-of-forward should be rotated 90° CCW
- Right: (-sz*0.55, -sz*0.7)
Rotation by angle θ (with cos, sin) of point (x,y):
- x' = x*cos - y*sin
- y' = x*sin + y*cos
For nose (sz, 0): (sz*cos, sz*sin) ✓
For back-left (-sz*0.55, +sz*0.7):
- x' = -sz*0.55 * cos - sz*0.7 * sin
- y' = -sz*0.55 * sin + sz*0.7 * cos
In my code:
```
bL.x = b.x - cos*(sz*0.55) - sin*(sz*0.7) // ✓
bL.y = b.y - sin*(sz*0.55) + cos*(sz*0.7) // ✓
```
Good.
Actually for a sleeker triangular look, let me make the triangle a bit elongated. Nose at distance sz, back at distance sz*0.5 with width sz*0.6.
Let me also lower trail alpha maybe to 0.22 for slightly clearer trails... Actually 0.18 gives nice streaks.
Let me finalize. One last consideration: the HUD takes up space and might detract from the fullscreen feel. Let me make it semi-transparent and small in the corner. The user said "full-screen" but having a small HUD is typical and can be made transparent. Let me add a `H` key to toggle it.
Actually let me keep it simple but add `H` to toggle the HUD.
OK let me write the final file.
Let me also think about the look-and-feel. I want:
- Slightly glowy triangles - done via outer stroke
- Trails for movement - done
- Color variation across boids based on speed/position
- Cursor influence
One more touch: vary triangle size slightly per boid to add visual richness.
```javascript
b.size = 7 + Math.random()*3;
```
And use `b.size` in render.
Let me also add a slight blur or hue gradient. Actually keep it crisp; trails give the smoothness.
Final code now. Let me also think about potential bugs:
1. `let grid;` declared but `buildGrid` uses it. ✓
2. `cols, rows` declared inside buildGrid? Let me declare them outside. Currently I do `let cols, rows;` outside. ✓
3. Window resize: I have two resize listeners. Let me consolidate.
4. The count input - when changed, reset positions? No need.
Let me write the final file now.
Actually one improvement: instead of a hard separation radius, I can also weight alignment/cohesion by 1/d so closer neighbors matter more. But that's overkill for this; let me keep it simple.
Let me also consider: when `panic` is pressed (Space), boids should spread out. I'm adding random jitter. A better effect: temporarily invert cohesion or boost separation. Or just go wild. Let me invert: make them flee from centroid or just add lots of random force. I'll just add random force scaled with their speed.
Final code ready. Let me write it.
Wait, one more thought - the resize handler calls resize() which re-sets canvas dimensions, and separately I added a resize handler that calls buildGrid. Let me merge them.
OK let me finalize.
```javascript
function onResize() {
resize();
buildGrid();
}
window.addEventListener('resize', onResize);
```
And not the separate addEventListener.
Also initial setup: do I need to wait for DOM ready? The script is at end of body so DOM is parsed. The hud elements exist. Good.
Let me also handle touch for mobile? Add touchmove. Let me add it.
```javascript
window.addEventListener('touchmove', (e) => {
if (e.touches[0]) mouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
e.preventDefault();
}, { passive: false });
window.addEventListener('touchend', () => { mouse = null; });
```
But this prevents scrolling. Maybe not ideal. Let me skip and only handle mouse.
Actually for fullscreen app it's fine.
Let me write the final file now.
Final sizing: triangle size ~8-10 px, canvas full viewport, ~250 boids default, mouse flee radius 110, separation radius 24, perception radius 60.
Now writing the file.I'll create a full-screen flocking simulation with smooth, oriented triangles. Let me plan the design first: - **Algorithm**: classic Reynolds boids (separation + alignment + cohesion) with mouse flee, using a spatial grid for O(n) neighbor queries - **Visuals**: oriented glowing triangles with subtle motion trails, color varies with speed - **Interactivity**: HUD with live-tunable parameters, mouse-flee, Space panic, R reset
/workspace/index.html (18779 bytes)Successfully wrote 18779 bytes to /workspace/index.html
thinking
Let me verify the file looks correct by checking it loads properly. Let me also do a quick review of the code logic.
Key things to check:
1. Spatial grid - cell size 48, perception radius 64. Searching 3x3 cells = covers 144x144 area, more than needed. ✓
2. Separation radius 26 < cell size 48 so separation might miss some boids near edge of cell. Wait, in the worst case, a boid in the center of its cell could see neighbors up to perception radius away. If neighbor is just outside its cell (at +CELL/2 distance), the 3x3 search covers it. ✓
3. Mouse flee - applied as a force away from cursor. ✓
4. Walls - soft turn near edges. With soft turn + hard clamp as safety. ✓
5. Triangle drawing - rotated by heading angle. ✓
6. Color - varies with speed, position, time. ✓
7. Motion trails - drawn each frame with alpha. ✓
8. HUD - sliders bound to params, count slider adjusts number. ✓
9. Key bindings: Space=panic, R=reset, H=toggle HUD. ✓
Let me also check the order of declarations - JavaScript hoists function declarations but `class` and `let`/`const` are not hoisted in terms of usable. Let me trace:
```js
resizeCanvas(); // function declaration - OK
function resizeCanvas() {...}
const params = {...}; // const declared
bindSlider(...) // function declaration - OK
function bindSlider(...) {...}
class Boid {...} // class declaration - hoisted but not initialized until reached
let boids = [];
function spawn() {...}
function adjustCount() {...}
spawn(params.count); // calls Boid - Boid must be defined by now
buildGrid(); // needs grid/cell/etc declared first - they are ✓
function buildGrid() {...}
```
Yes that's fine.
One thing - the slider event listener for `count` calls `adjustCount()` which mutates `boids`. Let me trace:
```js
function adjustCount() {
const target = Math.round(params.count);
while (boids.length < target) boids.push(new Boid(...));
if (boids.length > target) boids.length = target;
}
```
That works. New boids spawn at random positions with random velocities. ✓
Let me also think about whether the rendering will be fast enough. With 280 boids and trails, we're doing canvas ops for ~280 boids per frame plus the fill rect. Should hit 60fps on most hardware.
The HUD might overlap the simulation. The user can press H to hide.
Actually, looking at HUD position - it's in the top-left corner with about 240px width. That covers some area. Let me make it more compact? Actually it's already pretty small. Users can hide with H.
Let me look at the file once more to make sure nothing is broken.Let me verify the file works correctly:
{"command":"ls -la index.html && head -1 index.html && wc -l index.html"}-rw-r--r-- 1 node node 18787 Jul 22 16:48 index.html <!DOCTYPE html> 588 index.html
{"command":"node -e \"\nconst fs = require('fs');\nconst html = fs.readFileSync('index.html', 'utf8');\n\n// Extract and verify the JS is syJS syntax OK OK class Boid OK function rebuildGrid OK function step OK function render OK requestAnimationFrame(loop) OK mousemove OK addEventListener('resize'
thinking
JS syntax is valid and all key features are present. The file is self-contained at `/workspace/index.html`. Let me give a quick summary.
Done. The simulation is at `/workspace/index.html` — a single self-contained file (~19 KB) with everything inlined. **What's in the simulation** - **Full Reynolds flocking**: separation (inverse-square weighted push from near neighbors), alignment (match neighbor velocity), cohesion (steer toward local center of mass). Each steering rule is computed as `(desired − current_velocity)`, then clamped to a max force and weighted by its HUD slider. - **Mouse flee**: every boid within ~120 px of the cursor gets a repulsion force whose strength scales with `1 − d/R²`, growing rapidly as the cursor closes in. A faint pink halo marks the danger radius. - **Spatial grid** (48 px cells, 3×3 neighbor lookup) keeps neighbor queries near-O(n) so ~280 boids run smoothly. - **Oriented triangles**: each boid is drawn as a rotated 3‑vertex triangle whose heading comes from `atan2(vy, vx)`. Color is HSL-mapped to speed (cyan ↔ magenta) with per-boid hue bias and a subtle time drift. - **Motion trails**: a translucent dark fill each frame creates streaks behind fast-moving boids. - **Soft wall turn**: within 80 px of any edge a steering force pulls boids back, with a hard fallthrough guard for the corners. **Controls** - Move the cursor near the canvas to scatter the flock - **`Space`** — panic (radial burst outward + jitter) - **`R`** — re-seed positions/velocities - **`H`** — toggle the HUD + stats overlay - Sliders for Separation / Alignment / Cohesion / Flee / Count / Speed / Trails
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Boids flock smoothly as oriented triangles, flee from cursor with visible flee radius, and controls are present.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.