Boids flocking · qwen3-235b-a22b
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 · 4 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
Okay, I need to create a full-screen flocking simulation of boids using separation, alignment, and cohesion. The boids should be drawn as oriented triangles that steer smoothly and flee from the mouse cursor. All of this has to be in a single index.html file with inline CSS and JS, no external resources.
First, I remember that boids typically have three rules: separation (avoid crowding neighbors), alignment (steer towards the average heading of neighbors), and cohesion (steer towards the average position of neighbors). Plus, adding a flee behavior from the mouse cursor.
I'll need to use the HTML5 canvas element for rendering. So the HTML structure will have a canvas that covers the full window. The CSS should make the canvas take up the entire viewport.
For the JavaScript part, I'll need to set up the canvas context, create boid objects with position and velocity. Each frame, update their velocities based on the three rules plus the flee force. Then update positions, and draw them as triangles.
Handling the mouse interaction: track the mouse position and apply a repulsion force from the mouse location to each boid. Maybe calculate a vector away from the mouse and apply it if the mouse is within a certain distance.
Drawing the boids as triangles: each boid's orientation should be based on their velocity direction. To draw a triangle, I can use the context.rotate() method, translating to the boid's position, rotating to match velocity direction, then drawing a triangle pointing forward.
The animation loop will use requestAnimationFrame to keep smooth updates. Also, handle window resizing to adjust the canvas size.
Potential variables for each boid: position (x,y), velocity (x,y), maybe acceleration. Constants for the three rules' weights, flee distance, etc.
Need to loop through all boids each frame, compute the forces, apply them, update velocity and position. Also, wrap around the edges of the screen so they don't disappear.
Let me outline the steps:
1. Set up HTML structure with canvas.
2. Add CSS to style the canvas to full screen.
3. JavaScript to initialize the canvas, create boids, handle mouse events.
4. Functions for each behavior (separation, alignment, cohesion, flee).
5. Update and draw each boid each frame.
Testing edge cases: making sure the boids don't go off-screen by wrapping their positions. Also, optimizing the distance calculations to avoid performance issues, maybe by limiting the neighborhood radius.
Wait, but for simplicity, since it's a single file and needs to work without external resources, I'll have to code all of this inline.
Let me start drafting the code. The canvas element in HTML:
<canvas id="boids"></canvas>
CSS to make it full screen:
html, body {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
In JavaScript:
- Get canvas context, set dimensions.
- Create an array of boids. Each boid could be an object with position, velocity, etc.
- Mouse position variables to track.
- In the animation loop, clear the canvas, update each boid's velocity based on the rules, then draw.
For the flee behavior: when the mouse is pressed or moved, each boid checks the distance to the mouse. If within a radius, apply a force away from the mouse.
Each behavior contributes a steering force. These forces are added to the boid's acceleration.
Let me think about the boid class. Maybe:
class Boid {
constructor() {
this.position = createVector(random(width), random(height));
this.velocity = p5.Vector.random2D().mult(random(2, 4));
this.acceleration = createVector(0, 0);
}
update() {
// Apply acceleration
this.velocity.add(this.acceleration);
// Limit speed
this.velocity.limit(5);
this.position.add(this.velocity);
// Reset acceleration
this.acceleration.mult(0);
}
applyForce(force) {
this.acceleration.add(force);
}
// Other methods for flocking behaviors
}
But since I'm not using p5.js, I need to implement vector math manually. Oh right, because no external libraries. So I'll have to create vector operations as functions.
Wait, but in vanilla JS, I can use objects with x and y properties and functions to handle vector math. Like:
function addVector(a, b) {
return {x: a.x + b.x, y: a.y + b.y};
}
function subtractVector(a, b) {
return {x: a.x - b.x, y: a.y - b.y};
}
function multiplyVector(v, scalar) {
return {x: v.x * scalar, y: v.y * scalar};
}
function divideVector(v, scalar) {
return {x: v.x / scalar, y: v.y / scalar};
}
function vectorLength(v) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
function normalizeVector(v) {
const len = vectorLength(v);
if (len === 0) return {x: 0, y: 0};
return divideVector(v, len);
}
These functions can help with the calculations.
Now, for each boid, during the flocking step, iterate through all other boids, calculate separation, alignment, cohesion.
Separation: steer to avoid crowding. For each nearby boid, compute the vector pointing away from them and sum up, then divide by count and apply.
Alignment: average the velocity of nearby boids, steer towards that.
Cohesion: average the position of nearby boids, steer towards that point.
Each of these contributes a steering force. Then, add the flee force from the mouse.
The steering force is calculated as the desired velocity minus current velocity, then limited to a maximum steer.
Putting this into code:
For each boid, in each frame:
- For all other boids, check distance. If within a certain radius, process for separation, alignment, cohesion.
For example:
let sep = {x:0, y:0};
let align = {x:0, y:0};
let coh = {x:0, y:0};
let count = 0;
for (let other of boids) {
let d = distance(boid.position, other.position);
if (d > 0 && d < separationDistance) {
// Separation
let diff = subtractVector(boid.position, other.position);
diff = divideVector(diff, d); // Weight by distance
sep = addVector(sep, diff);
count++;
}
if (d > 0 && d < alignmentDistance) {
align = addVector(align, other.velocity);
}
if (d > 0 && d < cohesionDistance) {
coh = addVector(coh, other.position);
}
}
if (count > 0) {
sep = divideVector(sep, count);
sep = setMagnitude(sep, maxSpeed);
let steerSep = subtractVector(sep, boid.velocity);
applyForce(steerSep.multiply(separationWeight));
align = divideVector(align, count);
align = setMagnitude(align, maxSpeed);
let steerAlign = subtractVector(align, boid.velocity);
applyForce(steerAlign.multiply(alignmentWeight));
coh = divideVector(coh, count);
coh = subtractVector(coh, boid.position);
coh = setMagnitude(coh, maxSpeed);
let steerCoh = subtractVector(coh, boid.velocity);
applyForce(steerCoh.multiply(cohesionWeight));
}
Then for the flee force:
if (mouseIsPressed) {
let d = distance(boid.position, mousePos);
if (d < fleeDistance) {
let diff = subtractVector(boid.position, mousePos);
let fleeForce = normalizeVector(diff);
fleeForce = multiplyVector(fleeForce, fleeStrength);
applyForce(fleeForce);
}
}
But I need to calculate this for each boid each frame.
Now, the drawing part: each boid is a triangle pointing in the direction of velocity.
To draw the triangle, translate to the boid's position, rotate based on velocity angle, then draw the triangle.
The rotation can be calculated using Math.atan2(dy, dx). So:
const angle = Math.atan2(boid.velocity.y, boid.velocity.x);
Then, context.translate(boid.position.x, boid.position.y);
context.rotate(angle);
context.beginPath();
context.moveTo(10, 0);
context.lineTo(-10, -5);
context.lineTo(-10, 5);
context.closePath();
context.fill();
But need to reset the transformation after each draw.
Putting all this together.
Now, handling the animation loop. The main loop will:
1. Clear the canvas.
2. Update all boids' positions and velocities.
3. Draw all boids.
Also, handle window resizing to reset canvas size.
Now, putting all this into code.
Potential issues:
- Performance with many boids, but for the sake of the problem, maybe limit the number to 100 or so.
- Edge wrapping: when a boid goes off the screen, reappear on the opposite side.
Let me outline the code structure in index.html:
<!DOCTYPE html>
<html>
<head>
<style>
/* CSS here */
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// JavaScript here
</script>
</body>
</html>
Now, writing the code step by step.
First, the CSS:
html, body {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
In the script:
Initialize canvas, context, variables.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
Mouse variables:
let mouse = {x: width/2, y: height/2};
let mouseDown = false;
Boid array:
class Boid {
constructor() {
this.position = {x: Math.random() * width, y: Math.random() * height};
this.velocity = {x: Math.random() * 4 - 2, y: Math.random() * 4 - 2};
this.acceleration = {x: 0, y: 0};
this.maxSpeed = 3;
this.maxForce = 0.05;
}
applyForce(force) {
this.acceleration.x += force.x;
this.acceleration.y += force.y;
}
update() {
// Apply acceleration
this.velocity.x += this.acceleration.x;
this.velocity.y += this.acceleration.y;
// Limit speed
const speed = Math.sqrt(this.velocity.x**2 + this.velocity.y**2);
if (speed > this.maxSpeed) {
this.velocity.x = (this.velocity.x / speed) * this.maxSpeed;
this.velocity.y = (this.velocity.y / speed) * this.maxSpeed;
}
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Reset acceleration
this.acceleration.x = 0;
this.acceleration.y = 0;
// Wrap around edges
if (this.position.x < 0) this.position.x = width;
else if (this.position.x > width) this.position.x = 0;
if (this.position.y < 0) this.position.y = height;
else if (this.position.y > height) this.position.y = 0;
}
flock(boids) {
const separationDist = 25;
const alignmentDist = 50;
const cohesionDist = 50;
const fleeDist = 100;
const separationWeight = 1.5;
const alignmentWeight = 1.0;
const cohesionWeight = 1.0;
const fleeWeight = 0.5;
let sep = {x: 0, y: 0};
let align = {x: 0, y: 0};
let coh = {x: 0, y: 0};
let flee = {x: 0, y: 0};
let count = 0;
for (let other of boids) {
const d = distance(this.position, other.position);
// Separation
if (d > 0 && d < separationDist) {
let diff = subtract(this.position, other.position);
diff = divide(diff, d); // Weight by distance
sep = add(sep, diff);
count++;
}
// Alignment
if (d < alignmentDist) {
align = add(align, other.velocity);
}
// Cohesion
if (d < cohesionDist) {
coh = add(coh, other.position);
}
}
if (count > 0) {
sep = divide(sep, count);
sep = setMagnitude(sep, this.maxSpeed);
let steerSep = subtract(sep, this.velocity);
steerSep = limit(steerSep, this.maxForce);
this.applyForce(multiply(steerSep, separationWeight));
align = divide(align, count);
align = setMagnitude(align, this.maxSpeed);
let steerAlign = subtract(align, this.velocity);
steerAlign = limit(steerAlign, this.maxForce);
this.applyForce(multiply(steerAlign, alignmentWeight));
coh = divide(coh, count);
coh = subtract(coh, this.position);
coh = setMagnitude(coh, this.maxSpeed);
let steerCoh = subtract(coh, this.velocity);
steerCoh = limit(steerCoh, this.maxForce);
this.applyForce(multiply(steerCoh, cohesionWeight));
}
// Flee from mouse
if (mouseDown) {
const d = distance(this.position, mouse);
if (d < fleeDist) {
let diff = subtract(this.position, mouse);
let fleeForce = normalize(diff);
fleeForce = multiply(fleeForce, fleeWeight);
this.applyForce(fleeForce);
}
}
}
draw() {
const angle = Math.atan2(this.velocity.y, this.velocity.x);
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(10, 0);
ctx.lineTo(-5, 5);
ctx.lineTo(-5, -5);
ctx.closePath();
ctx.fillStyle = 'white';
ctx.fill();
ctx.restore();
}
}
Need helper functions:
function add(a, b) {
return {x: a.x + b.x, y: a.y + b.y};
}
function subtract(a, b) {
return {x: a.x - b.x, y: a.y - b.y};
}
function multiply(a, scalar) {
return {x: a.x * scalar, y: a.y * scalar};
}
function divide(a, scalar) {
return {x: a.x / scalar, y: a.y / scalar};
}
function normalize(a) {
const mag = Math.sqrt(a.x**2 + a.y**2);
if (mag === 0) return {x: 0, y: 0};
return {x: a.x/mag, y: a.y/mag};
}
function setMagnitude(a, mag) {
const normal = normalize(a);
return multiply(normal, mag);
}
function limit(a, max) {
const mag = Math.sqrt(a.x**2 + a.y**2);
if (mag > max) {
return multiply(normalize(a), max);
}
return a;
}
function distance(a, b) {
return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2);
}
Now, initializing the boids array:
const boids = [];
const NUM_BOIDS = 50;
function setup() {
for (let i = 0; i < NUM_BOIDS; i++) {
boids.push(new Boid());
}
}
setup();
Animation loop:
function animate() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, width, height);
for (let boid of boids) {
boid.flock(boids);
boid.update();
boid.draw();
}
requestAnimationFrame(animate);
}
animate();
Handling mouse events:
window.addEventListener('mousemove', (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
window.addEventListener('mousedown', () => {
mouseDown = true;
});
window.addEventListener('mouseup', () => {
mouseDown = false;
});
Also, handle window resize:
window.addEventListener('resize', () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
});
Putting all together in the script tag.
Potential issues:
- The initial velocity might be too slow. Adjusting the maxSpeed in the Boid class.
Testing the code, maybe the numbers need tweaking for better visual effect.
Also, the fillRect in animate uses a semi-transparent black to create a trailing effect. Maybe adjust the alpha for smoother trails.
Now, compiling all this into the index.html file.
I need to make sure that all the code is in the script tag, with proper syntax. Also, check for any typos in function names or variables.
Another thing: the canvas is cleared each frame with ctx.fillRect, and the semi-transparent color allows the previous frames to fade out, creating a trail effect.
Putting it all together, the final index.html would look like:
<!DOCTYPE html>
<html>
<head>
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
let mouse = {x: width/2, y: height/2};
let mouseDown = false;
class Boid {
constructor() {
this.position = {x: Math.random() * width, y: Math.random() * height};
this.velocity = {x: Math.random() * 4 - 2, y: Math.random() * 4 - 2};
this.acceleration = {x: 0, y: 0};
this.maxSpeed = 3;
this.maxForce = 0.05;
}
applyForce(force) {
this.acceleration.x += force.x;
this.acceleration.y += force.y;
}
update() {
this.velocity.x += this.acceleration.x;
this.velocity.y += this.acceleration.y;
const speed = Math.sqrt(this.velocity.x**2 + this.velocity.y**2);
if (speed > this.maxSpeed) {
const ratio = this.maxSpeed / speed;
this.velocity.x *= ratio;
this.velocity.y *= ratio;
}
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.acceleration.x = 0;
this.acceleration.y = 0;
if (this.position.x < 0) this.position.x = width;
else if (this.position.x > width) this.position.x = 0;
if (this.position.y < 0) this.position.y = height;
else if (this.position.y > height) this.position.y = 0;
}
flock(boids) {
const separationDist = 25;
const alignmentDist = 50;
const cohesionDist = 50;
const fleeDist = 100;
const separationWeight = 1.5;
const alignmentWeight = 1.0;
const cohesionWeight = 1.0;
const fleeWeight = 0.5;
let sep = {x: 0, y: 0};
let align = {x: 0, y: 0};
let coh = {x: 0, y: 0};
let flee = {x: 0, y: 0};
let count = 0;
for (let other of boids) {
const d = distance(this.position, other.position);
if (d > 0 && d < separationDist) {
let diff = subtract(this.position, other.position);
diff = divide(diff, d);
sep = add(sep, diff);
count++;
}
if (d < alignmentDist) {
align = add(align, other.velocity);
}
if (d < cohesionDist) {
coh = add(coh, other.position);
}
}
if (count > 0) {
sep = divide(sep, count);
sep = setMagnitude(sep, this.maxSpeed);
let steerSep = subtract(sep, this.velocity);
steerSep = limit(steerSep, this.maxForce);
this.applyForce(multiply(steerSep, separationWeight));
align = divide(align, count);
align = setMagnitude(align, this.maxSpeed);
let steerAlign = subtract(align, this.velocity);
steerAlign = limit(steerAlign, this.maxForce);
this.applyForce(multiply(steerAlign, alignmentWeight));
coh = divide(coh, count);
coh = subtract(coh, this.position);
coh = setMagnitude(coh, this.maxSpeed);
let steerCoh = subtract(coh, this.velocity);
steerCoh = limit(steerCoh, this.maxForce);
this.applyForce(multiply(steerCoh, cohesionWeight));
}
if (mouseDown) {
const d = distance(this.position, mouse);
if (d < fleeDist) {
let diff = subtract(this.position, mouse);
let fleeForce = normalize(diff);
fleeForce = multiply(fleeForce, fleeWeight);
this.applyForce(fleeForce);
}
}
}
draw() {
const angle = Math.atan2(this.velocity.y, this.velocity.x);
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(10, 0);
ctx.lineTo(-5, 5);
ctx.lineTo(-5, -5);
ctx.closePath();
ctx.fillStyle = 'white';
ctx.fill();
ctx.restore();
}
}
function add(a, b) {
return {x: a.x + b.x, y: a.y + b.y};
}
function subtract(a, b) {
return {x: a.x - b.x, y: a.y - b.y};
}
function multiply(a, scalar) {
return {x: a.x * scalar, y: a.y * scalar};
}
function divide(a, scalar) {
return {x: a.x / scalar, y: a.y / scalar};
}
function normalize(a) {
const mag = Math.sqrt(a.x**2 + a.y**2);
if (mag === 0) return {x: 0, y: 0};
return {x: a.x/mag, y: a.y/mag};
}
function setMagnitude(a, mag) {
const normal = normalize(a);
return multiply(normal, mag);
}
function limit(a, max) {
const mag = Math.sqrt(a.x**2 + a.y**2);
if (mag > max) {
return multiply(normalize(a), max);
}
return a;
}
function distance(a, b) {
return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2);
}
const boids = [];
const NUM_BOIDS = 50;
function setup() {
for (let i = 0; i < NUM_BOIDS; i++) {
boids.push(new Boid());
}
}
setup();
function animate() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, width, height);
for (let boid of boids) {
boid.flock(boids);
boid.update();
boid.draw();
}
requestAnimationFrame(animate);
}
animate();
window.addEventListener('mousemove', (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
window.addEventListener('mousedown', () => {
mouseDown = true;
});
window.addEventListener('mouseup', () => {
mouseDown = false;
});
window.addEventListener('resize', () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
});
</script>
</body>
</html>
I think this should work. Let me check for possible errors.
In the Boid's update method, the velocity is limited correctly. The flock function applies the forces with the weights. The mouse events are handled. The draw function uses ctx.save and restore to manage transformations.
Testing this code should give a flocking simulation with fleeing from the mouse. The triangles should rotate based on their velocity direction.
I'll proceed to write this into the index.html file using the write function.index.html (7469 bytes)Successfully wrote 7469 bytes to index.html
thinking
Okay, I need to create a full-screen flocking simulation of boids using separation, alignment, and cohesion, with the boids drawn as oriented triangles that steer smoothly and flee from the mouse cursor. All of this has to be in a single index.html file with inline CSS and JS, no external resources. First, I'll start by setting up the HTML structure. The page should have a canvas element that takes up the entire viewport. The CSS will make sure the canvas is full screen and remove any default margins or padding from the body. Next, the JavaScript part. I'll need to define the Boid class with properties like position, velocity, and acceleration. The Boid's update method will handle the physics, applying forces and updating positions. The flocking behavior involves three main rules: separation, alignment, and cohesion. Each of these will calculate a steering force based on the positions and velocities of nearby boids. For separation, each boid steers to avoid crowding its neighbors. Alignment makes the boid steer towards the average heading of its neighbors. Cohesion pulls the boid towards the center of mass of nearby boids. Additionally, when the mouse is pressed, boids should flee from the cursor's position. Drawing the boids as triangles requires calculating the direction of the velocity vector and rotating the canvas to draw the triangle facing the correct direction. The triangles should be small and positioned at the boid's location. Handling mouse events is important. I'll track the mouse position and whether the button is pressed to apply the flee force. Also, resizing the window should adjust the canvas size to maintain full-screen. I'll need helper functions for vector math, like adding, subtracting, normalizing vectors, and limiting their magnitude. These functions will help calculate the steering forces for each boid. Putting it all together, the animation loop will clear the canvas, update each boid's position and velocity based on the flocking rules and mouse interaction, and then draw the boids. The loop uses requestAnimationFrame for smooth updates. Testing the code, I might need to adjust parameters like the maximum speed, force weights, and neighbor distances to get a realistic flocking behavior. Also, ensuring the canvas resizes correctly and the mouse events are properly captured. Finally, I'll compile all the code into a single HTML file, making sure the CSS and JS are embedded within the style and script tags respectively. No external files or CDNs are used, so everything is self-contained in index.html.
I've created a full-screen boid flocking simulation with mouse interaction. The simulation features: 1. **Core Behaviors**: - Separation: Boids avoid crowding neighbors - Alignment: Boids steer toward their neighbors' average heading - Cohesion: Boids move toward their neighbors' center of mass - Flee: Boids scatter when mouse is pressed 2. **Visual Elements**: - Boids rendered as oriented triangles - Smooth steering with velocity-based rotation - Semi-transparent trails for motion effect 3. **Interactive Features**: - Flee from mouse cursor (hold mouse button) - Full-screen responsive canvas - Dynamic resizing support The implementation is contained in a single self-contained HTML file with no external dependencies. You can open `index.html` in any modern browser to see the simulation in action. Key controls: - Hold mouse button to activate flee behavior - Resize window to see responsive canvas - Watch boids naturally form cohesive flocks with emergent behavior The code is optimized for performance while maintaining readability, with all vector math implemented directly in vanilla JavaScript.
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 move and flock continuously as small streaked triangles; no clear distinct reaction to inputs beyond ambient animation.
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.