import Vector from './vector.mjs'; class Boids { constructor(canvas, params = {}) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.params = Object.assign({ centerAccel: 0.075, repelAccel: 0.8, matchAccel: 0.15, boundsAccel: 0.01, maxSpeed: 300, closeDist: 20, visibleDist: 60, radius: 10, color: 'black', }, params); // Current width and height of the canvas this.width = canvas.width; this.height = canvas.height; // Current center point of the canvas this.center = new Vector(canvas.width / 2, canvas.height / 2); // Last time where the canvas was repainted this.lastTime = null; // List of active simulated boids this.boids = []; // Vector registers used for holding temporary values this.registers = [ new Vector(0, 0), new Vector(0, 0), new Vector(0, 0) ]; // When the simulation is running, the ID of the next rAF request, // otherwise, null this.animationId = null; // Outline of the shape of a boid when angled at 0 rad this.boidShape = [ new Vector(1, 0), new Vector(-.5, .5), new Vector(-.5, -.5), new Vector(1, 0), ]; this.boidShapeLength = this.boidShape.length; } /** Change the canvas’ dimensions */ resize(width, height) { this.canvas.width = width; this.canvas.height = height; this.width = width; this.height = height; this.center = new Vector(width / 2, height / 2); } /** Introduce a new boid in the simulation. */ add(center) { this.boids.push({ pos: center.clone(), vel: new Vector(Math.random() - 0.5, Math.random() - 0.5), }); } /** Start the simulation. */ start() { if (this.animationId !== null) { return; } this._step = this._step.bind(this); this.animationId = requestAnimationFrame(this._step); } /** Pause the simulation. */ pause() { cancelAnimationFrame(this.animationId); this.animationId = null; this.lastTime = null; } /** * @private * Perform a time step of the simulation and update the canvas. */ _step(time) { if (!this.lastTime) { this.lastTime = time; } const delta = (time - this.lastTime) / 1000; this.lastTime = time; this._update(delta); this._draw(); if (this.animationId !== null) { this.animationId = requestAnimationFrame(this._step); } } /** * @private * Advance the simulation. */ _update(delta) { const length = this.boids.length; for (let i = 0; i < length; ++i) { const me = this.boids[i]; const meanPos = this.registers[0].reset(); const meanVel = this.registers[1].reset(); const repelForce = this.registers[2].reset(); let visibles = 0; for (let j = 0; j < length; ++j) { if (i != j) { const you = this.boids[j]; const dist = Vector.distSquared(me.pos, you.pos); if (dist < this.params.visibleDist * this.params.visibleDist) { meanPos.add(you.pos); meanVel.add(you.vel); visibles += 1; if (dist < this.params.closeDist * this.params.closeDist) { repelForce.add(me.pos).sub(you.pos); } } } } // Attract towards center of visible flock if (visibles >= 1) { me.vel.add(meanPos.div(visibles) .sub(me.pos) .mul(this.params.centerAccel)); } // Attract toward center of screen if out of bounds if (me.pos.x < this.width * -.4 || me.pos.x > this.width * .4 || me.pos.y < this.height * -.4 || me.pos.y > this.height * .4) { me.vel.addMul(me.pos, -this.params.boundsAccel); } // Repel away from close boids me.vel.add(repelForce.mul(this.params.repelAccel)); // Match other boids’ velocity if (visibles >= 1) { me.vel.add(meanVel.div(visibles) .sub(me.vel) .mul(this.params.matchAccel)); } // Do not surpass maximum speed const speed = me.vel.normSquared(); if (speed > this.params.maxSpeed * this.params.maxSpeed) { me.vel.div(Math.sqrt(speed)).mul(this.params.maxSpeed); } // Integrate speed me.pos.addMul(me.vel, delta); } } /** * @private * Redraw the boids. */ _draw() { const length = this.boids.length; const transformed = this.registers[0]; this.ctx.clearRect(0, 0, this.width, this.height); // Draw each boid’s head following the angle of its course this.ctx.beginPath(); this.ctx.fillStyle = this.params.color; for (let i = 0; i < length; ++i) { const boid = this.boids[i]; const angle = boid.vel.angle(); let isFirst = true; for (let j = 0; j < this.boidShapeLength; ++j) { transformed.x = this.boidShape[j].x; transformed.y = this.boidShape[j].y; transformed .rotate(angle) .mul(this.params.radius) .add(this.center) .add(boid.pos); if (isFirst) { this.ctx.moveTo(transformed.x, transformed.y); isFirst = false; } else { this.ctx.lineTo(transformed.x, transformed.y); } } } this.ctx.fill(); } } export default Boids;