oneshotlm
Compare all models on this prompt →

Boids flocking · seed-2.0-mini

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.

⛨ sandboxed Open in new tab ↗
transcript · 4 messages
user

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.

assistant
thinking
Got it, let's tackle this. The user wants a full-screen boids flocking simulation with separation, alignment, cohesion, plus mouse flee, all in one single index.html file with no external resources.

First, I need to remember how boids work: three main rules, plus avoid mouse. Let's outline the steps:

1. Create a basic HTML5 canvas that's full-screen, no margins, covers the whole window.
2. Set up the canvas context, handle window resizing so it always fills the screen.
3. Create a Boid class or struct that has position, velocity, acceleration properties. Each boid is drawn as an oriented triangle (pointing along their velocity direction).
4. Implement the three core rules:
   - Separation: steer to avoid crowding local flockmates
   - Alignment: steer towards the average heading of local flockmates
   - Cohesion: steer to move towards the average position of local flockmates
5. Add mouse avoidance: boids flee from the mouse cursor when it's nearby.
6. Add boundary handling: boids wrap around the screen edges (or bounce? Wait, wrap is nicer for flocking)
7. Animate the simulation with requestAnimationFrame.

Wait, let's think about the details:

First, the canvas: set width and height to window.innerWidth/innerHeight, listen for resize events to update them. Also, disable scrollbars so it's full screen.

For each boid:
- Position: x, y
- Velocity: vx, vy (limited to a max speed)
- Acceleration: ax, ay (applied each frame, then reset)
- When drawing: calculate the angle of the velocity, draw a triangle rotated by that angle, so the point is facing the direction of movement.

The rules:
For each boid, iterate over all other boids, calculate the distance between them. For each rule, only consider boids within a certain perception radius.

Separation: for each nearby boid, calculate a vector pointing away from it, add to separation steering. Normalize and scale by a separation strength.

Alignment: calculate the average velocity of all nearby boids, subtract the current boid's velocity to get the alignment steering, scale by alignment strength.

Cohesion: calculate the average position of all nearby boids, subtract the current boid's position to get a vector towards that center, scale by cohesion strength.

Mouse flee: calculate distance from mouse position, if within a flee radius, calculate a vector away from the mouse, add to steering, scale by flee strength.

Then, apply the steering forces: clamp the acceleration to a max force, add to velocity, clamp velocity to max speed, update position, reset acceleration each frame.

Also, need to track the mouse position: add mousemove event listener to get clientX/clientY, store as mouseX, mouseY.

Let's structure the code:

First, the HTML boilerplate:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Boid Flocking Simulation</title>
    <style>
        /* Remove margins, padding, scrollbars */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { overflow: hidden; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
        // All JS here
    </script>
</body>
</html>

Then inside the script tag:

First, get the canvas and context:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;

Then handle resize:
window.addEventListener('resize', () => {
    width = canvas.width = window.innerWidth;
    height = canvas.height = window.innerHeight;
});

Then mouse position:
let mouseX = 0;
let mouseY = 0;
window.addEventListener('mousemove', (e) => {
    mouseX = e.clientX;
    mouseY = e.clientY;
});

Then set up boid parameters:
const BOID_COUNT = 150; // Number of boids
const PERCEPTION_RADIUS = 50; // How far a boid can see others
const MAX_SPEED = 3; // Max velocity of a boid
const MAX_FORCE = 0.1; // Max steering force
const SEPARATION_WEIGHT = 1.5;
const ALIGNMENT_WEIGHT = 1.0;
const COHESION_WEIGHT = 1.0;
const FLEE_RADIUS = 80; // How far mouse affects boids
const FLEE_WEIGHT = 2.0;

Then the Boid class:
class Boid {
    constructor() {
        // Random position on screen
        this.x = Math.random() * width;
        this.y = Math.random() * height;
        // Random velocity, between -MAX_SPEED and MAX_SPEED for each axis
        this.vx = (Math.random() - 0.5) * 2 * MAX_SPEED;
        this.vy = (Math.random() - 0.5) * 2 * MAX_SPEED;
        this.ax = 0;
        this.ay = 0;
    }

    // Calculate steering forces
    applyBehaviors(boids) {
        let separation = this.separate(boids);
        let alignment = this.align(boids);
        let cohesion = this.cohesion(boids);
        let flee = this.fleeMouse();

        // Apply weights
        separation.multiply(SEPARATION_WEIGHT);
        alignment.multiply(ALIGNMENT_WEIGHT);
        cohesion.multiply(COHESION_WEIGHT);
        flee.multiply(FLEE_WEIGHT);

        // Add all forces to acceleration
        this.addForce(separation);
        this.addForce(alignment);
        this.addForce(cohesion);
        this.addForce(flee);
    }

    // Separation: avoid crowding neighbors
    separate(boids) {
        let steer = { x: 0, y: 0 };
        let count = 0;
        for (let other of boids) {
            let d = Math.hypot(this.x - other.x, this.y - other.y);
            if (d > 0 && d < PERCEPTION_RADIUS) {
                // Vector away from other boid, scaled by distance (closer = stronger repulsion)
                let diff = {
                    x: this.x - other.x,
                    y: this.y - other.y
                };
                diff.x /= d; // Normalize
                diff.y /= d;
                steer.x += diff.x;
                steer.y += diff.y;
                count++;
            }
        }
        if (count > 0) {
            steer.x /= count;
            steer.y /= count;
        }
        // Convert to steering force
        if (Math.hypot(steer.x, steer.y) > 0) {
            // Set magnitude to MAX_SPEED, subtract current velocity
            let mag = Math.hypot(steer.x, steer.y);
            steer.x = (steer.x / mag) * MAX_SPEED;
            steer.y = (steer.y / mag) * MAX_SPEED;
            steer.x -= this.vx;
            steer.y -= this.vy;
            // Clamp to max force
            if (Math.hypot(steer.x, steer.y) > MAX_FORCE) {
                let mag = Math.hypot(steer.x, steer.y);
                steer.x = (steer.x / mag) * MAX_FORCE;
                steer.y = (steer.y / mag) * MAX_FORCE;
            }
        }
        return steer;
    }

    // Alignment: match velocity with neighbors
    align(boids) {
        let sum = { x: 0, y: 0 };
        let count = 0;
        for (let other of boids) {
            let d = Math.hypot(this.x - other.x, this.y - other.y);
            if (d > 0 && d < PERCEPTION_RADIUS) {
                sum.x += other.vx;
                sum.y += other.vy;
                count++;
            }
        }
        if (count > 0) {
            sum.x /= count;
            sum.y /= count;
            // Set magnitude to MAX_SPEED, subtract current velocity
            let mag = Math.hypot(sum.x, sum.y);
            sum.x = (sum.x / mag) * MAX_SPEED;
            sum.y = (sum.y / mag) * MAX_SPEED;
            sum.x -= this.vx;
            sum.y -= this.vy;
            // Clamp to max force
            if (Math.hypot(sum.x, sum.y) > MAX_FORCE) {
                let mag = Math.hypot(sum.x, sum.y);
                sum.x = (sum.x / mag) * MAX_FORCE;
                sum.y = (sum.y / mag) * MAX_FORCE;
            }
        }
        return sum;
    }

    // Cohesion: move towards center of neighbors
    cohesion(boids) {
        let sum = { x: 0, y: 0 };
        let count = 0;
        for (let other of boids) {
            let d = Math.hypot(this.x - other.x, this.y - other.y);
            if (d > 0 && d < PERCEPTION_RADIUS) {
                sum.x += other.x;
                sum.y += other.y;
                count++;
            }
        }
        if (count > 0) {
            sum.x /= count;
            sum.y /= count;
            // Steer towards that position
            let steer = {
                x: sum.x - this.x,
                y: sum.y - this.y
            };
            let mag = Math.hypot(steer.x, steer.y);
            if (mag > 0) {
                steer.x = (steer.x / mag) * MAX_SPEED;
                steer.y = (steer.y / mag) * MAX_SPEED;
                steer.x -= this.vx;
                steer.y -= this.vy;
                if (Math.hypot(steer.x, steer.y) > MAX_FORCE) {
                    let mag = Math.hypot(steer.x, steer.y);
                    steer.x = (steer.x / mag) * MAX_FORCE;
                    steer.y = (steer.y / mag) * MAX_FORCE;
                }
            }
            return steer;
        }
        return { x: 0, y: 0 };
    }

    // Flee from mouse cursor
    fleeMouse() {
        let d = Math.hypot(this.x - mouseX, this.y - mouseY);
        let steer = { x: 0, y: 0 };
        if (d < FLEE_RADIUS && d > 0) {
            // Vector away from mouse, scaled by distance (closer = stronger flee)
            steer.x = this.x - mouseX;
            steer.y = this.y - mouseY;
            let mag = Math.hypot(steer.x, steer.y);
            steer.x = (steer.x / mag) * MAX_SPEED;
            steer.y = (steer.y / mag) * MAX_SPEED;
            steer.x -= this.vx;
            steer.y -= this.vy;
            if (Math.hypot(steer.x, steer.y) > MAX_FORCE) {
                let mag = Math.hypot(steer.x, steer.y);
                steer.x = (steer.x / mag) * MAX_FORCE;
                steer.y = (steer.y / mag) * MAX_FORCE;
            }
        }
        return steer;
    }

    // Add a force to acceleration
    addForce(force) {
        this.ax += force.x;
        this.ay += force.y;
    }

    // Update position and velocity
    update() {
        // Update velocity
        this.vx += this.ax;
        this.vy += this.ay;
        // Clamp velocity to max speed
        let speed = Math.hypot(this.vx, this.vy);
        if (speed > MAX_SPEED) {
            this.vx = (this.vx / speed) * MAX_SPEED;
            this.vy = (this.vy / speed) * MAX_SPEED;
        }
        // Update position
        this.x += this.vx;
        this.y += this.vy;
        // Reset acceleration
        this.ax = 0;
        this.ay = 0;
        // Wrap around screen edges
        this.wrapEdges();
    }

    // Wrap boid to opposite side when it goes off screen
    wrapEdges() {
        if (this.x < 0) this.x += width;
        if (this.x > width) this.x -= width;
        if (this.y < 0) this.y += height;
        if (this.y > height) this.y -= height;
    }

    // Draw the boid as a triangle pointing along velocity direction
    draw() {
        // Calculate angle of velocity
        let angle = Math.atan2(this.vy, this.vx);
        // Save current context state
        ctx.save();
        // Translate to boid position
        ctx.translate(this.x, this.y);
        // Rotate to face velocity direction
        ctx.rotate(angle);
        // Draw triangle: base at back, point forward
        ctx.beginPath();
        ctx.moveTo(8, 0); // Forward point
        ctx.lineTo(-4, 4); // Back right
        ctx.lineTo(-4, -4); // Back left
        ctx.closePath();
        // Fill white with black stroke
        ctx.fillStyle = '#ffffff';
        ctx.strokeStyle = '#000000';
        ctx.lineWidth = 1;
        ctx.fill();
        ctx.stroke();
        // Restore context
        ctx.restore();
    }
}

Then create the boids array:
let boids = [];
for (let i = 0; i < BOID_COUNT; i++) {
    boids.push(new Boid());
}

Then the animation loop:
function animate() {
    // Clear canvas each frame
    ctx.fillStyle = '#222222'; // Dark background
    ctx.fillRect(0, 0, width, height);

    // Update and draw all boids
    for (let boid of boids) {
        boid.applyBehaviors(boids);
        boid.update();
        boid.draw();
    }

    // Request next frame
    requestAnimationFrame(animate);
}

// Start the animation
animate();

Wait, let's check for any issues:

- The separate, align, cohesion functions: when calculating distance, using Math.hypot which is good.
- The fleeMouse function: yes, if the mouse is within FLEE_RADIUS, calculate the vector away.
- The draw function: rotates the triangle so it points along the velocity, that's correct. The triangle is 8 units long forward, -4 back, so it's a nice oriented triangle.
- Wrapping edges: yes, so boids don't go off screen, they wrap around.
- The background is dark grey, boids are white with black outlines, that's visible.
- The parameters: BOID_COUNT 150 is a good number, not too slow.
- MAX_SPEED 3, MAX_FORCE 0.1: that gives smooth steering.

Wait, let's test if the vector math is correct. For example, in separation: for each other boid, subtract the other's position from this boid's position to get a vector pointing from other to this, then normalize, add to separation sum, then average, then scale to MAX_SPEED, subtract current velocity to get the steering force. Yes, that's the standard way.

Also, in the update function: reset acceleration each frame, correct.

Wait, the addForce function just adds the force to ax and ay, which is correct because each force is a steering acceleration.

Another thing: when we calculate the steer for separation, we do (this.x - other.x) which is away from other, yes, because if other is to the left of this boid, this.x - other.x is positive, so the steer is to the right, away from other. Correct.

Also, the mouse position: mousemove event uses clientX/Y, which is correct relative to the viewport, since the canvas is full screen and positioned as block, so clientX/Y is exactly the mouse position over the canvas.

What about resizing? When the window resizes, we update width and height, but the boids' positions are still based on the old width/height? Oh, right, that's a small issue. But for a simple simulation, it's okay, or we could reposition them, but maybe just leave it, or add a resize handler that resets all boids? Wait, no, maybe better to just let them adjust, but actually, when the window resizes, the canvas size changes, but the boids' wrapEdges function uses the current width and height, so that's okay. Wait, yes, because in wrapEdges, we use the global width and height variables, which are updated on resize, so that's correct.

Wait, let's test: if the window is resized, width and height are updated, so when a boid wraps, it uses the new width/height. Correct.

Another thing: the initial velocity is random between -MAX_SPEED and MAX_SPEED, correct.

Let's put all this together into a single index.html file. Let's make sure there are no external dependencies, all inline, no CDNs. Yes, the code uses only native JS APIs, canvas, requestAnimationFrame, etc.

Wait, let's check the style tag: * { margin:0; padding:0; box-sizing:border-box; } body { overflow:hidden; } canvas { display:block; } That's correct, removes all scrollbars, makes the canvas fill the body.

Wait, the viewport meta tag: yes, for mobile devices, that's correct, sets the initial scale to 1.0, so the canvas is correctly sized on mobile.

Let's write the full code now, combining all parts into one index.html file.

Wait, also, in the Boid class, the separate function: when count is 0, returns {x:0,y:0}, correct. Same for align and cohesion.

Another thing: in the fleeMouse function, if d is 0 (mouse is exactly on the boid), we skip, which is good, avoids division by zero.

Yes, that's all covered.

Now, let's write the complete file, then use the write tool to save it as /tmp/ws/index.html.
write /tmp/ws/index.html (9645 bytes)
toolResult

Successfully wrote 9645 bytes to /tmp/ws/index.html

assistant
thinking
Great, the file was written successfully. Now I should let the user know that the simulation is ready, and that they can open index.html in a browser to run it. Let's also mention the features: full-screen, boids as oriented triangles, separation/alignment/cohesion rules, mouse flee, wraps around screen edges, resizes with window.

The full-screen boid flocking simulation has been created successfully! The file `index.html` is saved in your current working directory. To run it: 1. Open the file in any modern web browser (Chrome, Firefox, Safari, Edge, etc.) 2. Move your mouse cursor around the screen to watch the boids flee from your pointer Key features: - 150 animated boids drawn as oriented white triangles pointing along their movement direction - Core flocking behaviors: separation (avoid crowding), alignment (match neighbors' velocity), cohesion (stick to the flock) - Mouse cursor flee behavior (boids avoid your mouse within an 80px radius) - Screen edge wrapping (boids reappear on the opposite side when they go off-screen) - Full-screen responsive design that adapts to window resizing - Smooth, natural steering with configurable physics parameters The simulation is completely self-contained with no external resources or network requests.

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

modelseed-2.0-mini
vendorByteDance · Seed
harnesspi-agent
generated2026-08-01

EVALUATION

3/5 working

Boids flock and move continuously as small triangles across the screen; no visible reaction to clicks or keys, likely no mouse-flee effect.

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.

idle motion4.4%
on click4.9%
on drag4.7%
on wheel4.9%
on enter + space4.8%
on arrow keys4.9%
on w/a/s/d5.0%
frame spread7.6 / 255
console errors0
js errors none

METRICS

runtime51.7s
tokens in11k
tokens out7.2k
cached0
cost / run$0.0039