var vectorUtils = {
normalize: function(vec2){ var magnitude = Math.sqrt(vec2.x * vec2.x + vec2.y * vec2.y); if(magnitude == 0){ return {x: 0, y: 0}; } return {x: vec2.x / magnitude, y: vec2.y / magnitude}; }
}
class Boid{
// They will be initialized with a starting x and y position constructor(xPos, yPos, planeWidth, planeHeight, wrap, separationStrength, separationDistance, alignmentStrength, alignmentDistance, cohesionStrength, cohesionDistance){ // The mass of the boid will dictate how responsive it is to flocking forces this.mass = 1; this.maxSpeed = .5; this.position = {x: xPos, y: yPos}; this.velocity = {x: 0, y: 0}; this.acceleration = {x: 0, y: 0}; this.planeWidth = planeWidth; this.planeHeight = planeHeight; this.wrap = wrap; this.separationDistance = separationDistance; this.separationStrength = separationStrength; this.alignmentDistance = alignmentDistance; this.alignmentStrength = alignmentStrength; this.cohesionDistance = cohesionDistance; this.cohesionStrength = cohesionStrength; } // Heading is represented by a decimal value indicating the radians get heading() { return Math.atan2(this.velocity.y, this.velocity.x); } // This function will be called to guide the boid while flocking applyForce(force){ // Acceleration is force devided by mass this.acceleration.x += force.x / this.mass; this.acceleration.y += force.y / this.mass; } update(neighbors){ var separationForce = this.separation(neighbors); var cohesionForce = this.cohesion(neighbors); var alignmentForce = this.alignment(neighbors); this.applyForce(separationForce); this.applyForce(cohesionForce); this.applyForce(alignmentForce); this.updatePosition(); } separation(neighbors){ var separationForce = {x: 0, y: 0} var count = 0; for(var i = 0; i < neighbors.length; i++){ var distance = Math.pow(Math.pow(this.position.x - neighbors[i].position.x, 2) + Math.pow(this.position.y - neighbors[i].position.y, 2), .5); if(distance < this.separationDistance){ var offset = { x: this.position.x - neighbors[i].position.x, y: this.position.y - neighbors[i].position.y, } var normalizedOffset = vectorUtils.normalize(offset) var force = {x:normalizedOffset.x / (distance + .01), y:normalizedOffset.y / (distance + .01)};; separationForce.x += force.x; separationForce.y += force.y; count += 1; } } if(count > 0){ separationForce.x /= count; separationForce.y /= count; separationForce.x *= this.separationStrength; separationForce.y *= this.separationStrength; } return separationForce; } cohesion(neighbors){ var positionSum = {x: 0, y: 0}; var count = 0; for(var i = 0; i < neighbors.length; i++){ var distance = Math.pow(Math.pow(this.position.x - neighbors[i].position.x, 2) + Math.pow(this.position.y - neighbors[i].position.y, 2), .5); if(distance < this.cohesionDistance){ positionSum.x += neighbors[i].position.x; positionSum.y += neighbors[i].position.y; count++; } } var averagePosition; if(count > 0 ){ averagePosition = {x: positionSum.x / count, y: positionSum.y / count}; } else{ return {x: 0, y:0}; } var displacement = {x: -this.position.x + averagePosition.x, y: -this.position.y + averagePosition.y}; var distance = Math.pow(Math.pow(displacement.x, 2) + Math.pow(displacement.y, 2), .5); var normalizedDisplacement = vectorUtils.normalize(displacement); if(distance < 50){ normalizedDisplacement.x *= distance/50; normalizedDisplacement.y *= distance/50; } var cohesionForce = {x: normalizedDisplacement.x * this.cohesionStrength, y: normalizedDisplacement.y * this.cohesionStrength}; return cohesionForce; } alignment(neighbors){ // This is the average velocity of all neighbors var averageVelocity = {x: 0, y: 0}; // Tracks the number of boids within the cohesion distance var count = 0; for(var i = 0; i < neighbors.length; i++){ var distance = Math.pow(Math.pow(this.position.x - neighbors[i].position.x, 2) + Math.pow(this.position.y - neighbors[i].position.y, 2), .5); if(distance < this.alignmentDistance){ averageVelocity.x += neighbors[i].velocity.x; averageVelocity.y += neighbors[i].velocity.y; count++; } } if(count > 0){ averageVelocity.x /= count; averageVelocity.y /= count; } var alignmentForce = {x: averageVelocity.x * this.alignmentStrength, y: averageVelocity.y * this.alignmentStrength}; return alignmentForce } updatePosition(){ // Acceleration is change in velocity this.velocity.x += this.acceleration.x; this.velocity.y += this.acceleration.y; this.velocity.x = Math.abs(this.velocity.x) > this.maxSpeed ? Math.sign(this.velocity.x) * this.maxSpeed : this.velocity.x; this.velocity.y = Math.abs(this.velocity.y) > this.maxSpeed ? Math.sign(this.velocity.y) * this.maxSpeed : this.velocity.y; if(this.wrap){ this.position.x = (this.position.x + this.velocity.x) % this.planeWidth; this.position.y = (this.position.y + this.velocity.y) % this.planeHeight; if(this.position.x < 0){ this.position.x = this.planeWidth; } if(this.position.y < 0){ this.position.y = this.planeHeight; } } else{ this.position.x += this.velocity.x; this.position.y += this.velocity.y; } this.acceleration = {x: 0, y: 0}; }
}
// Defining the model for a flock class Flock{
constructor(flockSize, canvasWidth, canvasHeight, wrap, separationStrength, separationDistance, alignmentStrength, alignmentDistance, cohesionStrength, cohesionDistance){ this.boids = []; this.size = flockSize; this._separationStrength = separationStrength; this._separationDistance = separationDistance; this._alignmentStrength = alignmentStrength; this._alignmentDistance = alignmentDistance; this._cohesionStrength = cohesionStrength; this._cohesionDistance = cohesionDistance; this.populateFlock(canvasWidth, canvasHeight, wrap); this.neighborDistance = 100; this.neighborDistanceSquared = Math.pow(this.neighborDistance, 2); } set separationStrength(strength){ if(strength != this._separationStrength){ this._separationStrength = strength; for(var i = 0; i < this.size; i++){ this.boids[i].separationStrength = strength; } } } set separationDistance(distance){ if(distance != this._separationDistance){ this._separationDistance = distance; for(var i = 0; i < this.size; i++){ this.boids[i].separationDistance = distance; } } } set alignmentStrength(strength){ if(strength != this._alignmentStrength){ this._alignmentStrength = strength; for(var i = 0; i < this.size; i++){ this.boids[i].alignmentStrength = strength; } } } set alignmentDistance(distance){ if(distance != this._alignmentDistance){ this._alignmentDistance = distance; for(var i = 0; i < this.size; i++){ this.boids[i].alignmentDistance = distance; } } } set cohesionStrength(strength){ if(strength != this._cohesionStrength){ this._cohesionStrength = strength; for(var i = 0; i < this.size; i++){ this.boids[i].cohesionStrength = strength; } } } set cohesionDistance(distance){ if(distance != this._cohesionDistance){ this._cohesionDistance = distance; for(var i = 0; i < this.size; i++){ this.boids[i].cohesionDistance = distance; } } } populateFlock(canvasWidth, canvasHeight, wrap){ for(var n = 0; n < this.size; n++){ // The boids will be created at the center of the graph. this.boids.push(new Boid(canvasWidth / 2,canvasHeight / 2, canvasWidth, canvasHeight, wrap, this._separationStrength, this._separationDistance, this._alignmentStrength, this._alignmentDistance, this._cohesionStrength, this._cohesionDistance)); // The angle of the boids are evenly distributed in a circle var angle = (n / this.size) * 2 * Math.PI; // The velocity is set based on the calculated angle this.boids[n].velocity = {x: Math.cos(angle), y: Math.sin(angle)}; } } update(){ for(var i = 0; i < this.size; i++){ var neighbors = []; // Iterates through all other boids to find neighbors. for(var j = 0; j < this.size; j++){ if(j != i){ var squareDistance = Math.pow(this.boids[j].position.x - this.boids[i].position.x, 2) + Math.pow(this.boids[j].position.y - this.boids[i].position.y, 2); if(squareDistance < this.neighborDistanceSquared){ neighbors.push(this.boids[j]); } } } this.boids[i].update(neighbors); } }
}
var drawingUtils = {
drawTriangle: function(context, PosX, PosY, SideLength, Orientation) { context.setTransform(1,0,0,1,PosX,PosY); // Set position context.rotate(Orientation); // set rotation in radians context.beginPath(); var sides = 3; var a = ((Math.PI * 2) / sides); context.moveTo(SideLength,0); context.lineTo(SideLength * Math.cos(a*1), SideLength * Math.sin(a*1)); context.lineTo((SideLength + 3) * Math.cos(a*3), (SideLength + 3) * Math.sin(a*3)) context.lineTo(SideLength * Math.cos(a*2), SideLength * Math.sin(a*2)); context.closePath(); context.fill() context.setTransform(1,0,0,1,0,0);// reset the transform return true; }, renderFlock: function(flock, context){ for(var i = 0; i < flock.size; i++){ this.renderBoid(flock.boids[i], context); } }, renderBoid: function(boid, context){ // The drawTriangle function takes a position and a rotation as parameters this.drawTriangle(context, boid.position.x, boid.position.y, 5, boid.heading); }
}
var testRenderingDisplay = {
canvas: document.getElementById("rendering-test-display-canvas"), context: document.getElementById("rendering-test-display-canvas").getContext('2d'), element: document.getElementById("rendering-test-display"), restartButton: document.getElementById("rendering-test-display-restart"), flock: null, restart: function(){ testRenderingDisplay.flock = new Flock(20, testRenderingDisplay.canvas.width, testRenderingDisplay.canvas.height, false, 0, 0, 0, 0, 0, 0); }, loop: function (){ this.flock.update(); this.context.save(); // save the default state this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); drawingUtils.renderFlock(this.flock, this.context); this.context.restore(); }, initialize: function(){ this.restartButton.onclick = this.restart; this.restart(); }, deinitialize: function(){}
}
var testRenderingWrappedDisplay = {
canvas: document.getElementById("rendering-test-wrapped-display-canvas"), context: document.getElementById("rendering-test-wrapped-display-canvas").getContext('2d'), element: document.getElementById("rendering-test-wrapped-display"), restartButton: document.getElementById("rendering-test-wrapped-display-restart"), flock: null, restart: function(){ testRenderingWrappedDisplay.flock = new Flock(20, testRenderingWrappedDisplay.canvas.width, testRenderingWrappedDisplay.canvas.height, true, 0, 0, 0, 0, 0, 0); }, loop: function (){ this.flock.update(); this.context.save(); // save the default state this.context.clearRect(0, 0, this.canvas.width, this.canvas.height, false); drawingUtils.renderFlock(this.flock, this.context); this.context.restore(); }, initialize: function(){ this.restartButton.onclick = this.restart; this.restart(); }, deinitialize: function(){}
}
var separationDisplay = {
canvas: document.getElementById("separation-display-canvas"), context: document.getElementById("separation-display-canvas").getContext('2d'), element: document.getElementById("separation-display"), restartButton: document.getElementById("separation-display-restart"), distanceSlider: document.getElementById("separation-display-distance"), strengthSlider: document.getElementById("separation-display-strength"), flock: null, restart: function(){ separationDisplay.flock = new Flock(30, separationDisplay.canvas.width, separationDisplay.canvas.height, true, 0, 0, 0, 0, 0, 0); }, loop: function (){ this.flock.separationDistance = this.distanceSlider.value / 1.5; this.flock.separationStrength = this.strengthSlider.value / 50; this.flock.update(); this.context.save(); // save the default state this.context.clearRect(0, 0, this.canvas.width, this.canvas.height, false); drawingUtils.renderFlock(this.flock, this.context); this.context.restore(); }, initialize: function(){ this.restartButton.onclick = this.restart; this.restart(); }, deinitialize: function(){}
}
var cohesionDisplay = {
canvas: document.getElementById("cohesion-display-canvas"), context: document.getElementById("cohesion-display-canvas").getContext('2d'), element: document.getElementById("cohesion-display"), restartButton: document.getElementById("cohesion-display-restart"), distanceSlider: document.getElementById("cohesion-display-distance"), strengthSlider: document.getElementById("cohesion-display-strength"), flock: null, restart: function(){ cohesionDisplay.flock = new Flock(30, cohesionDisplay.canvas.width, cohesionDisplay.canvas.height, true, 0, 0, 0, 0, 0, 0); }, loop: function (){ this.flock.cohesionDistance = this.distanceSlider.value / 3; this.flock.cohesionStrength = this.strengthSlider.value / 1000; this.flock.update(); this.context.save(); // save the default state this.context.clearRect(0, 0, this.canvas.width, this.canvas.height, false); drawingUtils.renderFlock(this.flock, this.context); this.context.restore(); }, initialize: function(){ this.restartButton.onclick = this.restart; this.restart(); }, deinitialize: function(){}
} var cohesionSeparationDisplay = {
canvas: document.getElementById("cohesion-separation-display-canvas"), context: document.getElementById("cohesion-separation-display-canvas").getContext('2d'), element: document.getElementById("cohesion-separation-display"), restartButton: document.getElementById("cohesion-separation-display-restart"), cohesionDistanceSlider: document.getElementById("cohesion-separation-display-cohesiondistance"), cohesionStrengthSlider: document.getElementById("cohesion-separation-display-cohesionstrength"), separationDistanceSlider: document.getElementById("cohesion-separation-display-separationdistance"), separationStrengthSlider: document.getElementById("cohesion-separation-display-separationstrength"), flock: null, restart: function(){ cohesionSeparationDisplay.flock = new Flock(30, cohesionSeparationDisplay.canvas.width, cohesionSeparationDisplay.canvas.height, true, 0, 0, 0, 0, 0, 0); }, loop: function (){ this.flock.cohesionDistance = this.cohesionDistanceSlider.value; this.flock.cohesionStrength = this.cohesionStrengthSlider.value / 400; this.flock.separationDistance = this.separationDistanceSlider.value / 1.7; this.flock.separationStrength = this.separationStrengthSlider.value / 20; this.flock.update(); this.context.save(); // save the default state this.context.clearRect(0, 0, this.canvas.width, this.canvas.height, false); drawingUtils.renderFlock(this.flock, this.context); this.context.restore(); }, initialize: function(){ this.restartButton.onclick = this.restart; this.restart(); }, deinitialize: function(){}
} var finalDisplay = {
canvas: document.getElementById("final-display-canvas"), context: document.getElementById("final-display-canvas").getContext('2d'), element: document.getElementById("final-display"), restartButton: document.getElementById("final-display-restart"), cohesionDistanceSlider: document.getElementById("final-display-cohesiondistance"), cohesionStrengthSlider: document.getElementById("final-display-cohesionstrength"), separationDistanceSlider: document.getElementById("final-display-separationdistance"), separationStrengthSlider: document.getElementById("final-display-separationstrength"), alignmentDistanceSlider: document.getElementById("final-display-alignmentdistance"), alignmentStrengthSlider: document.getElementById("final-display-alignmentstrength"), flock: null, restart: function(){ finalDisplay.flock = new Flock(30, finalDisplay.canvas.width, finalDisplay.canvas.height, true, 0, 0, 0, 0, 0, 0); }, loop: function (){ this.flock.cohesionDistance = this.cohesionDistanceSlider.value; this.flock.cohesionStrength = this.cohesionStrengthSlider.value / 400; this.flock.separationDistance = this.separationDistanceSlider.value / 1.7; this.flock.separationStrength = this.separationStrengthSlider.value / 20; this.flock.alignmentDistance = this.alignmentDistanceSlider.value / 2.5; this.flock.alignmentStrength = this.alignmentStrengthSlider.value / 1700; this.flock.update(); this.context.save(); // save the default state this.context.clearRect(0, 0, this.canvas.width, this.canvas.height, false); drawingUtils.renderFlock(this.flock, this.context); this.context.restore(); }, initialize: function(){ this.restartButton.onclick = this.restart; this.restart(); }, deinitialize: function(){}
} codeDisplays.push(testRenderingDisplay); codeDisplays.push(testRenderingWrappedDisplay); codeDisplays.push(separationDisplay); codeDisplays.push(cohesionDisplay); codeDisplays.push(cohesionSeparationDisplay); codeDisplays.push(finalDisplay);