Boids flocking · kimi-k2.7-code
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
The user wants a full-screen flocking simulation (boids) with separation, alignment, and cohesion. The boids should be drawn as oriented triangles, steer smoothly, and flee from the mouse cursor. Everything must be in a single `index.html` file with inline CSS/JS, no external resources.
We need to produce the file. Let's design:
- HTML full screen canvas, no scrollbars.
- CSS: margin 0, padding 0, overflow hidden, background color.
- JS: Boid class with position, velocity, acceleration. Update using rules:
- Separation: steer away from nearby boids.
- Alignment: steer towards average velocity of neighbors.
- Cohesion: steer towards average position of neighbors.
- Flee mouse: steer away from cursor.
- Boundaries: wrap around or turn away. Full-screen wrap can be nice. But "flee from mouse cursor". Could also turn away from screen edges? Wrapping avoids edge clumping. User asked full-screen, boids fleeing mouse. Use wrapping for seamless.
- Drawing: triangle oriented to velocity. Use canvas transforms. Draw as filled triangle.
- Parameters: sliders? User didn't request UI, but can include hidden or simple controls? Since single-file self-contained, maybe minimal controls at top? But "no external resources" doesn't preclude simple UI. Better to keep clean full-screen simulation. We can add simple clickable info. But not necessary. Maybe include small instruction text overlay? However "full-screen" suggests minimal. Could still include a small title overlay. But user didn't ask. I think just pure canvas. Could add parameters at top of JS for tuning.
- Interactions: mouse move tracked globally; fleeing triggered when mouse active. Use pointer position. On touch too.
- Performance: number of boids e.g., 150-200. Use squared distances. Neighbor radius ~50. Separation radius ~25. Use max force, max speed.
- Smooth steering: use acceleration and limit; accumulate forces with weights.
- Implementation details:
- `Boid` class methods: `flock(boids)`, `update()`, `edges()`, `show(ctx)`, `applyForce(force)`.
- `Flock` array.
- Rules functions: `separate`, `align`, `cohesion`, `flee`.
- For each boid, loop all boids O(N^2). For 200 boids fine.
- Flee: compute vector boid->mouse, distance; if within flee radius, scale force inverse proportional maybe. Add weight.
- Smooth triangle: draw isosceles triangle pointing along velocity: maybe `translate(x,y); rotate(angle); beginPath; moveTo(10,0); lineTo(-6,5); lineTo(-6,-5); closePath; fill`.
- Colors: maybe white triangles with slight blue hue, on dark background. Add subtle trails? Could leave no trails.
- Responsive: resize canvas to window innerWidth/innerHeight; on resize reset canvas size. If wrapping, no issue.
- Code style: plain JS, ES6 classes. Use requestAnimationFrame loop.
- Provide final concise summary and file path.
Potential improvements: use devicePixelRatio for crisp rendering on high DPI. Set canvas width/height to CSS size * DPR, scale ctx. This is good. Use `window.devicePixelRatio || 1`. Mouse coordinates must be scaled accordingly.
Let's write the index.html. Need to ensure no external resources, all inline.
Let's code.
Plan:
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Boids Flocking Simulation</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; background:#0b1020; }
canvas { display:block; }
.hint { position:absolute; top:10px; left:10px; color:rgba(255,255,255,0.4); font-family:sans-serif; font-size:12px; pointer-events:none; user-select:none; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="hint">Move your cursor to scare the flock.</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width, height, dpr;
let mouse = { x: -1000, y: -1000, active: false };
let boids = [];
const CONFIG = {
count: 160,
neighborDist: 60,
separationDist: 24,
fleeDist: 180,
maxSpeed: 4,
maxForce: 0.08,
fleeWeight: 2.5,
sepWeight: 1.8,
aliWeight: 1.0,
cohWeight: 1.0,
color: '#7ec8e3'
};
function resize() {
dpr = window.devicePixelRatio || 1;
width = window.innerWidth;
height = window.innerHeight;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
class Boid {
constructor() {
this.position = { x: Math.random()*width, y: Math.random()*height };
const angle = Math.random()*Math.PI*2;
this.velocity = { x: Math.cos(angle)*CONFIG.maxSpeed, y: Math.sin(angle)*CONFIG.maxSpeed };
this.acceleration = { x:0, y:0 };
}
edges() {
if (this.position.x < -10) this.position.x = width + 10;
else if (this.position.x > width + 10) this.position.x = -10;
if (this.position.y < -10) this.position.y = height + 10;
else if (this.position.y > height + 10) this.position.y = -10;
}
applyForce(force) {
this.acceleration.x += force.x;
this.acceleration.y += force.y;
}
flock(all) {
let sep = {x:0,y:0}, ali = {x:0,y:0}, coh = {x:0,y:0};
let sepCount=0, aliCount=0, cohCount=0;
for (let other of all) {
if (other === this) continue;
let dx = other.position.x - this.position.x;
let dy = other.position.y - this.position.y;
let d2 = dx*dx + dy*dy;
if (d2 > 0 && d2 < CONFIG.separationDist*CONFIG.separationDist) {
let d = Math.sqrt(d2);
sep.x -= dx/d;
sep.y -= dy/d;
sepCount++;
}
if (d2 > 0 && d2 < CONFIG.neighborDist*CONFIG.neighborDist) {
ali.x += other.velocity.x;
ali.y += other.velocity.y;
aliCount++;
coh.x += other.position.x;
coh.y += other.position.y;
cohCount++;
}
}
if (sepCount>0) {
sep.x /= sepCount; sep.y /= sepCount;
this.steerTo(sep, CONFIG.sepWeight, sep);
}
if (aliCount>0) {
ali.x /= aliCount; ali.y /= aliCount;
this.steerTo(ali, CONFIG.aliWeight, ali);
}
if (cohCount>0) {
coh.x /= cohCount; coh.y /= cohCount;
let desired = {x: coh.x - this.position.x, y: coh.y - this.position.y};
this.steerTo(desired, CONFIG.cohWeight, coh);
}
// flee mouse
let mdx = this.position.x - mouse.x;
let mdy = this.position.y - mouse.y;
let md2 = mdx*mdx + mdy*mdy;
if (mouse.active && md2 > 0 && md2 < CONFIG.fleeDist*CONFIG.fleeDist) {
let md = Math.sqrt(md2);
let flee = {x: mdx/md, y: mdy/md};
let strength = (1 - md/CONFIG.fleeDist) * CONFIG.fleeWeight;
this.steerTo(flee, strength, flee);
}
}
steerTo(vector, weight, target) {
// Normalize and scale to maxSpeed
let mag = Math.hypot(vector.x, vector.y);
if (mag === 0) return;
let desired = { x: vector.x/mag*CONFIG.maxSpeed, y: vector.y/mag*CONFIG.maxSpeed };
let steer = { x: desired.x - this.velocity.x, y: desired.y - this.velocity.y };
let smag = Math.hypot(steer.x, steer.y);
if (smag > CONFIG.maxForce) {
steer.x = steer.x/smag*CONFIG.maxForce;
steer.y = steer.y/smag*CONFIG.maxForce;
}
steer.x *= weight; steer.y *= weight;
this.applyForce(steer);
}
update() {
this.velocity.x += this.acceleration.x;
this.velocity.y += this.acceleration.y;
let speed = Math.hypot(this.velocity.x, this.velocity.y);
if (speed > CONFIG.maxSpeed) {
this.velocity.x = this.velocity.x/speed*CONFIG.maxSpeed;
this.velocity.y = this.velocity.y/speed*CONFIG.maxSpeed;
}
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.acceleration.x = 0;
this.acceleration.y = 0;
this.edges();
}
draw(ctx) {
let angle = Math.atan2(this.velocity.y, this.velocity.x);
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.rotate(angle);
ctx.fillStyle = CONFIG.color;
ctx.beginPath();
ctx.moveTo(10, 0);
ctx.lineTo(-6, 5);
ctx.lineTo(-3, 0);
ctx.lineTo(-6, -5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
function init() {
resize();
boids = [];
for (let i=0; i<CONFIG.count; i++) boids.push(new Boid());
}
function animate() {
ctx.clearRect(0,0,width,height);
// maybe background fill not needed because body background
for (let b of boids) {
b.flock(boids);
b.update();
b.draw(ctx);
}
requestAnimationFrame(animate);
}
window.addEventListener('resize', () => { resize(); });
window.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true; });
window.addEventListener('mouseleave', () => { mouse.active = false; });
window.addEventListener('touchmove', e => { mouse.x = e.touches[0].clientX; mouse.y = e.touches[0].clientY; mouse.active = true; }, {passive:true});
window.addEventListener('touchend', () => { mouse.active = false; });
init();
animate();
</script>
</body>
</html>
```
Need to double-check the `steerTo` method: I pass vector and target, but target not used. Actually can simplify. The separation vector `sep` is average of normalized away vectors. Then steer to that direction. I wrote `this.steerTo(sep, CONFIG.sepWeight, sep);` The method signature takes `vector` and uses it; target unused. OK but can clean.
For alignment: `ali` is average velocity; we want steer towards average heading. So vector=ali. Good.
For cohesion: desired vector = center - position. Good.
For flee: direction away normalized. Good.
Potential issue: `steerTo` normalizes vector and sets desired max speed, then subtract velocity and limit. But if we pass `sep` which is average unit away direction, desired = that unit * maxSpeed. Good.
However for alignment, average velocity might be small magnitude; still steerTo normalizes, so aligns heading but ignores magnitude. That's standard.
One subtle issue: `this.steerTo` uses `target` parameter unnecessary. Could remove. Also `flock` uses `target` argument identical. Could simplify to `this.applySteer(sep, CONFIG.sepWeight)`. But code works.
Performance: O(N^2) with count 160 fine.
Mouse flee: It calls steerTo with `flee` unit vector and strength weight. Since `steerTo` multiplies weight after limiting maxForce, strength could exceed 1 causing large force. FleeWeight 2.5; good.
Smoothness: acceleration limited per rule. But because multiple forces added, total acceleration can exceed maxForce; acceptable. Could scale combined force, but not necessary. The maxSpeed cap ensures smooth movement.
Maybe make triangle shape nicer: We used line to (-3,0) creating a small notch? Actually path: moveTo(10,0); lineTo(-6,5); lineTo(-3,0); lineTo(-6,-5); closePath. That creates a chevron shape with a notch. Better simple triangle: moveTo(9,0); lineTo(-5,4); lineTo(-5,-4); closePath. The notch isn't bad but might look like arrow. Oriented triangle could be just triangle. Let's use simple triangle.
Color: perhaps slight transparency? `rgba(126,200,227,0.9)`.
Maybe add motion trails? Not needed. Maybe background fill each frame with low alpha to create trails. User wants drawn as oriented triangles steering smoothly. Trails optional. Use `ctx.fillStyle='rgba(11,16,32,0.35)'; ctx.fillRect(...)` each frame to produce trails. This looks nice and smooth. But clearRect + no trails is also fine. Trails can enhance smooth visual. Let's add subtle trails: `ctx.fillStyle = 'rgba(11,16,32,0.2)'; ctx.fillRect(0,0,width,height);` Body background same color. This leaves trails. However if user moves mouse, trails might look like smearing. Flocking sims often use trails. I think it's okay but might obscure. Maybe not include to keep crisp. I'll keep clearRect for crisp triangles.
Responsive: On resize, if width changes, boids outside will wrap next update. Fine.
Let's also handle `pointermove` to capture mouse and touch; but use separate listeners. Fine.
Let's improve code quality: define `applySteer` without unused target. Use `let` but maybe use const where appropriate. No need.
Potential issue with `steerTo` modifying passed object: It doesn't modify; it uses to create desired and steer. Fine.
Let's finalize file. But before writing, we can consider adding sliders to adjust weights? Not requested. Avoid UI to keep full-screen.
Let's also ensure `maxForce` and `maxSpeed` units in CSS pixels. Good.
One more consideration: `ctx.setTransform(dpr,...)` after resize; on initial resize. Good. Since we clear canvas using width/height in CSS pixels, with setTransform, clearRect uses CSS coordinates, clears full canvas. Good.
Let's make triangle dimensions scale with dpr? Since ctx scaled by dpr, drawing in CSS coordinates; triangle size in CSS px. Fine.
Now write file.
Let's create /workspace/index.html. Use write tool.
Also final response: mention built index.html with separation/alignment/cohesion and mouse flee.
Proceed./workspace/index.html (7190 bytes)Successfully wrote 7190 bytes to /workspace/index.html
thinking
The file is written. Provide concise summary. Ensure final answer mentions self-contained index.html, no external resources.
Created `/workspace/index.html` — a self-contained, full-screen boids simulation with: - **Separation**, **alignment**, and **cohesion** rules with tunable weights - Smooth steering via per-boid acceleration and velocity limiting - Boids rendered as oriented triangles pointing in their direction of travel - Mouse-cursor repulsion/flee behavior (also works with touch) - Responsive, high-DPI canvas with no scrollbars - No external resources, CDNs, or network requests — everything is inline Open `index.html` in a browser and move your cursor to scatter the flock.
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.