Boids flocking · glm-4.7
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 · 6 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
The user wants me to create a single-page web app with a full-screen flocking simulation of boids. This needs to be a self-contained index.html file with CSS and JS inline, no external resources. Key features needed: 1. Full-screen canvas 2. Boids with separation, alignment, and cohesion behaviors (Craig Reynolds' boids algorithm) 3. Drawn as oriented triangles 4. Smooth steering 5. Flee from mouse cursor Let me create this simulation: 1. HTML structure with a full-screen canvas 2. CSS for styling (full screen, no scrollbars) 3. JavaScript for: - Boid class with position, velocity, acceleration - Flocking behaviors: separation, alignment, cohesion - Mouse flee behavior - Smooth steering with acceleration/velocity limits - Rendering as oriented triangles pointing in velocity direction The boid algorithm: - Separation: Steer to avoid crowding local flockmates - Alignment: Steer towards the average heading of local flockmates - Cohesion: Steer to move toward the average position of local flockmates I'll implement this with proper vector math, smooth acceleration, and nice visual styling.
I'll create a full-screen boid flocking simulation with separation, alignment, cohesion, and mouse fleeing behavior.
/workspace/index.html (13827 bytes)Successfully wrote 13827 bytes to /workspace/index.html
thinking
I've created a complete single-page boid flocking simulation. Let me verify the file was created correctly.
{"path":"/workspace/index.html"}<!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> * { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; background: #0a0a1a; font-family: 'Segoe UI', sans-serif; } canvas { display: block; } #info { position: fixed; top: 20px; left: 20px; color: rgba(255, 255, 255, 0.6); font-size: 14px; pointer-events: none; user-select: none; z-index: 10; } #controls { position: fixed; bottom: 20px; left: 20px; color: rgba(255, 255, 255, 0.8); font-size: 12px; pointer-events: none; user-select: none; z-index: 10; } </style> </head> <body> <canvas id="canvas"></canvas> <div id="info"> <div>Boids: <span id="boidCount">0</span></div> </div> <div id="controls"> Move mouse to scatter • Click to add boids </div> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const boidCountEl = document.getElementById('boidCount'); let width, height; let boids = []; let mouse = { x: -1000, y: -1000, active: false }; // Configuration const CONFIG = { numBoids: 150, maxSpeed: 4, maxForce: 0.1, perceptionRadius: 50, separationRadius: 30, mouseRepelRadius: 150, mouseRepelForce: 0.8, weights: { separation: 1.5, alignment: 1.0, cohesion: 1.0, mouseRepel: 3.0, boundaries: 2.0 } }; // Vector class for helper methods class Vector { constructor(x = 0, y = 0) { this.x = x; this.y = y; } add(v) { this.x += v.x; this.y += v.y; return this; } sub(v) { this.x -= v.x; this.y -= v.y; return this; } mult(n) { this.x *= n; this.y *= n; return this; } div(n) { if (n !== 0) { this.x /= n; this.y /= n; } return this; } mag() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const m = this.mag(); if (m > 0) { this.div(m); } return this; } limit(max) { if (this.mag() > max) { this.normalize(); this.mult(max); } return this; } copy() { return new Vector(this.x, this.y); } static dist(v1, v2) { const dx = v2.x - v1.x; const dy = v2.y - v1.y; return Math.sqrt(dx * dx + dy * dy); } static sub(v1, v2) { return new Vector(v1.x - v2.x, v1.y - v2.y); } } // Boid class class Boid { constructor(x, y) { this.position = new Vector(x, y); this.velocity = new Vector( (Math.random() - 0.5) * CONFIG.maxSpeed * 2, (Math.random() - 0.5) * CONFIG.maxSpeed * 2 ); this.acceleration = new Vector(); this.size = 8; this.hue = Math.random() * 60 + 180; // Blue-cyan range } edges() { const margin = 100; const force = new Vector(); if (this.position.x < margin) force.x = CONFIG.maxForce; else if (this.position.x > width - margin) force.x = -CONFIG.maxForce; if (this.position.y < margin) force.y = CONFIG.maxForce; else if (this.position.y > height - margin) force.y = -CONFIG.maxForce; if (force.mag() > 0) { force.normalize().mult(CONFIG.maxSpeed).sub(this.velocity).limit(CONFIG.maxForce); force.mult(CONFIG.weights.boundaries); } return force; } align(boids) { let steering = new Vector(); let total = 0; for (let other of boids) { if (other !== this && Vector.dist(this.position, other.position) < CONFIG.perceptionRadius) { steering.add(other.velocity); total++; } } if (total > 0) { steering.div(total); steering.normalize().mult(CONFIG.maxSpeed); steering.sub(this.velocity); steering.limit(CONFIG.maxForce); } return steering.mult(CONFIG.weights.alignment); } cohesion(boids) { let steering = new Vector(); let total = 0; for (let other of boids) { if (other !== this && Vector.dist(this.position, other.position) < CONFIG.perceptionRadius) { steering.add(other.position); total++; } } if (total > 0) { steering.div(total); steering.sub(this.position); steering.normalize().mult(CONFIG.maxSpeed); steering.sub(this.velocity); steering.limit(CONFIG.maxForce); } return steering.mult(CONFIG.weights.cohesion); } separation(boids) { let steering = new Vector(); let total = 0; for (let other of boids) { const d = Vector.dist(this.position, other.position); if (other !== this && d < CONFIG.separationRadius) { let diff = Vector.sub(this.position, other.position); diff.normalize(); diff.div(d); // Weight by distance steering.add(diff); total++; } } if (total > 0) { steering.div(total); steering.normalize().mult(CONFIG.maxSpeed); steering.sub(this.velocity); steering.limit(CONFIG.maxForce); } return steering.mult(CONFIG.weights.separation); } fleeFromMouse() { if (!mouse.active) return new Vector(); const mousePos = new Vector(mouse.x, mouse.y); const d = Vector.dist(this.position, mousePos); if (d < CONFIG.mouseRepelRadius && d > 0) { let steering = Vector.sub(this.position, mousePos); steering.normalize(); // Stronger force when closer const strength = 1 - (d / CONFIG.mouseRepelRadius); steering.mult(CONFIG.maxSpeed * strength * 2); steering.sub(this.velocity); steering.limit(CONFIG.maxForce * 3); return steering.mult(CONFIG.weights.mouseRepel); } return new Vector(); } flock(boids) { this.acceleration.add(this.align(boids)); this.acceleration.add(this.cohesion(boids)); this.acceleration.add(this.separation(boids)); this.acceleration.add(this.edges()); this.acceleration.add(this.fleeFromMouse()); } update() { this.velocity.add(this.acceleration); this.velocity.limit(CONFIG.maxSpeed); this.position.add(this.velocity); this.acceleration.mult(0); // Reset acceleration // Update hue based on speed for visual effect const speedRatio = this.velocity.mag() / CONFIG.maxSpeed; this.hue = 180 + speedRatio * 40 + Math.sin(Date.now() * 0.001 + this.position.x * 0.01) * 20; } draw() { const angle = Math.atan2(this.velocity.y, this.velocity.x); const speed = this.velocity.mag(); const size = this.size + speed * 0.5; ctx.save(); ctx.translate(this.position.x, this.position.y); ctx.rotate(angle); // Draw triangle boid ctx.beginPath(); ctx.moveTo(size, 0); // Tip ctx.lineTo(-size * 0.7, size * 0.5); // Back right ctx.lineTo(-size * 0.4, 0); // Back center indent ctx.lineTo(-size * 0.7, -size * 0.5); // Back left ctx.closePath(); // Gradient fill const gradient = ctx.createLinearGradient(-size, 0, size, 0); gradient.addColorStop(0, `hsla(${this.hue}, 80%, 40%, 0.6)`); gradient.addColorStop(1, `hsla(${this.hue}, 90%, 60%, 0.9)`); ctx.fillStyle = gradient; ctx.fill(); // Outline ctx.strokeStyle = `hsla(${this.hue}, 100%, 80%, 0.5)`; ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); } } // Initialize function init() { resize(); boids = []; for (let i = 0; i < CONFIG.numBoids; i++) { boids.push(new Boid( Math.random() * width, Math.random() * height )); } boidCountEl.textContent = boids.length; } function resize() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; } // Animation loop function animate() { // Semi-transparent background for trail effect ctx.fillStyle = 'rgba(10, 10, 26, 0.2)'; ctx.fillRect(0, 0, width, height); // Draw mouse influence area if (mouse.active) { const gradient = ctx.createRadialGradient( mouse.x, mouse.y, 0, mouse.x, mouse.y, CONFIG.mouseRepelRadius ); gradient.addColorStop(0, 'rgba(255, 100, 100, 0.1)'); gradient.addColorStop(0.5, 'rgba(255, 100, 100, 0.05)'); gradient.addColorStop(1, 'rgba(255, 100, 100, 0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(mouse.x, mouse.y, CONFIG.mouseRepelRadius, 0, Math.PI * 2); ctx.fill(); } // Update and draw boids for (let boid of boids) { boid.flock(boids); boid.update(); boid.draw(); } requestAnimationFrame(animate); } // Event listeners window.addEventListener('resize', resize); canvas.addEventListener('mousemove', (e) => { mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true; }); canvas.addEventListener('mouseleave', () => { mouse.active = false; }); canvas.addEventListener('click', (e) => { // Add 5 new boids at click position for (let i = 0; i < 5; i++) { const angle = Math.random() * Math.PI * 2; const dist = Math.random() * 30; boids.push(new Boid( e.clientX + Math.cos(angle) * dist, e.clientY + Math.sin(angle) * dist )); } boidCountEl.textContent = boids.length; }); // Touch support canvas.addEventListener('touchmove', (e) => { e.preventDefault(); const touch = e.touches[0]; mouse.x = touch.clientX; mouse.y = touch.clientY; mouse.active = true; }, { passive: false }); canvas.addEventListener('touchend', () => { mouse.active = false; }); canvas.addEventListener('touchstart', (e) => { const touch = e.touches[0]; mouse.x = touch.clientX; mouse.y = touch.clientY; mouse.active = true; // Add boids on tap for (let i = 0; i < 5; i++) { const angle = Math.random() * Math.PI * 2; const dist = Math.random() * 30; boids.push(new Boid( touch.clientX + Math.cos(angle) * dist, touch.clientY + Math.sin(angle) * dist )); } boidCountEl.textContent = boids.length; }); // Start init(); animate(); </script> </body> </html>
thinking
The file has been created successfully. It's a complete self-contained HTML file with: 1. Full-screen canvas with dark background 2. Boid flocking simulation with: - Separation behavior (boids avoid crowding) - Alignment behavior (boids steer toward average heading) - Cohesion behavior (boids move toward average position) - Edge avoidance (soft boundaries) - Mouse fleeing behavior 3. Visual features: - Boids drawn as oriented triangles pointing in velocity direction - Smooth steering with acceleration/velocity limits - Color gradients based on speed - Subtle trail effect from semi-transparent background clearing - Mouse influence area visualization (red glow) - Blue-cyan color palette that shifts with speed 4. Interactivity: - Mouse movement causes boids to flee - Click/tap adds more boids - Touch support for mobile devices - Responsive to window resize 5. UI elements: - Boid count display - Instructions for interaction The simulation uses Craig Reynolds' classic boid algorithm with proper steering behaviors (separation, alignment, cohesion) and adds mouse repulsion as an additional force.
I've created a complete single-page flocking simulation in `index.html`. The simulation features: **Boid Behaviors (Craig Reynolds' algorithm):** - **Separation** - Boids steer to avoid crowding neighbors - **Alignment** - Boids steer toward the average heading of nearby flockmates - **Cohesion** - Boids steer toward the average position of nearby flockmates - **Mouse repulsion** - Boids flee from the cursor when it approaches **Visual Features:** - 150 boids drawn as oriented triangles pointing in their direction of movement - Smooth steering with acceleration-based physics - Blue-cyan gradient colors that shift based on speed - Subtle motion trail effect from semi-transparent background clearing - Red glow around mouse showing the repulsion radius **Interactivity:** - Move mouse over the canvas to scatter the flock - Click/tap anywhere to add 5 new boids at that location - Full touch support for mobile devices - Responsive to window resizing **Technical Details:** - Custom Vector class for all physics calculations - Perception radius of 50px for flocking, 30px for separation - 150px mouse repel radius with distance-based force - Soft boundary avoidance at screen edges - Maximum speed and force limits for natural movement
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 in cohesive swirling groups with oriented triangles and visibly flee/scatter from cursor position.
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.