import {Vector} from './geometry.mjs'; // Outline of the shape of a boid when angled at 0 rad const boidShape = [ new Vector(1, 0), new Vector(-.5, .5), new Vector(-.5, -.5), new Vector(1, 0), ]; const boidShapeLength = boidShape.length; class Boid { constructor(params, center) { this.params = params; this.pos = center.clone(); this.vel = new Vector(Math.random() - 0.5, Math.random() - 0.5); this.register = new Vector(0, 0); } draw(ctx) { const transformed = this.register; const angle = this.vel.angle(); let isFirst = true; for (let j = 0; j < boidShapeLength; ++j) { transformed.x = boidShape[j].x; transformed.y = boidShape[j].y; transformed.rotate(angle).mul(this.params.radius).add(this.pos); if (isFirst) { ctx.moveTo(transformed.x, transformed.y); isFirst = false; } else { ctx.lineTo(transformed.x, transformed.y); } } } } class Boids { constructor(canvas, obstacles, 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', debug: false, }, params); // Current width and height of the canvas this.width = canvas.width; this.height = canvas.height; // Current center point of the canvas this.center = null; // Last time where the canvas was repainted this.lastTime = null; // List of active simulated boids this.boids = []; // List of obstacles this.obstacles = obstacles; // 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; this._afterResize(); } /** Change the canvas’ dimensions */ resize(width, height) { this.canvas.width = width; this.canvas.height = height; this.width = width; this.height = height; this._afterResize(); } _afterResize() { this.center = new Vector(this.width / 2, this.height / 2); this.ctx.resetTransform(); this.ctx.translate(this.center.x, this.center.y); } /** Introduce a new boid in the simulation. */ add(center) { this.boids.push(new Boid(this.params, center)); } /** 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 boidsLength = this.boids.length; const obstaclesLength = this.obstacles.length; for (let i = 0; i < boidsLength; ++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; // Compute mean flock position and velocity and compute // the repel force from other boids for (let j = 0; j < boidsLength; ++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); } } } } // Compute the repel force from obstacles for (let j = 0; j < obstaclesLength; ++j) { const obstacle = this.obstacles[j]; if (obstacle.distance(me.pos) < this.params.closeDist) { me.vel.add(me.pos).sub(obstacle.center); } } // 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 boidsLength = this.boids.length; const obstaclesLength = this.obstacles.length; this.ctx.clearRect( -this.width / 2, -this.height / 2, this.width, this.height ); // Draw obstacles if (this.params.debug) { this.ctx.fillStyle = '#dddddd'; this.ctx.beginPath(); for (let i = 0; i < obstaclesLength; ++i) { this.obstacles[i].draw(this.ctx); } this.ctx.fill(); } // Draw boids this.ctx.fillStyle = this.params.color; this.ctx.beginPath(); for (let i = 0; i < boidsLength; ++i) { this.boids[i].draw(this.ctx); } this.ctx.fill(); } } export default Boids;