class Boid { constructor(radius, color, x, y, direction, id, scene_width, scene_height) { // Boid properties this.radius = radius; this.color = color; this.x = x; this.y = y; this.direction = direction; this.id = id; // World properties this.scene_width = scene_width; this.scene_height = scene_height; // Boid "dynamic" properties this.rayLength = 60; this.wallBuffer = 40; this.turnStepAmount = 20; this.stepAmount = 2; this.fieldOfView = 180; this.boidBuffer = 20; } move(boids) { // this.direction += Math.random() * (2 * this.turnStepAmount) - this.turnStepAmount let direction = this.findNextRay(boids); if (!direction) { return false; } this.direction = direction; var vector = this.findPoint(this.x, this.y, this.stepAmount, this.direction); this.x = vector.x; this.y = vector.y; return true; } buildRays() { let rays = new Array(); let rayInteval = 5; let noOfSteps = this.fieldOfView / rayInteval; let is_clockwise = Math.random() >= 0.5; for (let i = 0; i < noOfSteps / 2; i++) { if (i == 0) { rays.push(rayInteval * i); continue; } if (is_clockwise) { rays.push(rayInteval * -i); } rays.push(rayInteval * i); if (!is_clockwise) { rays.push(rayInteval * -i); } } return rays; } findNextRay(boids) { let rays = this.buildRays(); let collision = false; for (let i = 0; i < rays.length; i++) { let rayAngle = this.direction + rays[i]; // Check against all other boids if (this.detectBoids(rayAngle, boids)) { if (i == 0) { collision = true; } continue; } // Check against scene boundaries if (this.detectBox(this.scene_width, this.scene_height, rayAngle)) { if (i == 0) { collision = true; } continue; } return rayAngle; } if (!collision) { return this.direction; } console.log('cannot find suitable ray'); return false; } detectBoids(direction, boids) { for (let i = 0; i < boids.length; i++) { if (this.detectBoid(direction, boids[i])) { return true; } } return false; } detectBoid(direction, boid) { // rule out ourselves if (this.id == boid.id) { return false; } // let cross = this.doPathsCross(direction, boid); // // if (cross) { // console.log('paths cross detected'); // } let intersect = this.boidsIntersect(boid); if (intersect) { console.log('boids intersect detected'); return true; } return this.willBoidsIntersect(boid, direction); } willBoidsIntersect(boid, direction) { let future_position = this.findPoint(this.x, this.y, this.rayLength, direction); return this.lineIntersectsCircle(this.x, future_position.x, this.y, future_position.y, boid.radius * 2, boid.x, boid.y); } boidsIntersect(boid) { return this.twoCirclesIntersect(this.radius, boid.radius, this.x, boid.x, this.y, boid.y); } twoCirclesIntersect(r1, r2, x1, x2, y1, y2) { // from https://planetcalc.com/8098/ (bottom of page) // https://mathworld.wolfram.com/Circle-CircleIntersection.html // d is the abs distance between the two circles center let d = this.distanceBetweenPoints(x1, x2, y1, y2); // a is distance to our x3 point ( I think ) let a = (Math.pow(r1, 2) - Math.pow(r2, 2) + Math.pow(d, 2)) / (2 * d); // h is some other segment of some other circle ( I dont really know ) let h = Math.sqrt(Math.pow(r1, 2) - Math.pow(a, 2)); // x3 and y3 represent the point between the two circle midpoints, perpindicular to which will be our intersects // ( maybe ) let x3 = x1 + ((a / d) * (x2 - x1)); let y3 = y1 + ((a / d) * (y2 - y1)); // x4 and y4 are one of the intersect points let x4 = x3 + ((h / d) * (y2 - y1)); let y4 = y3 - ((h / d) * (x2 - x1)); if (!x4 || !y4) { return false; } return { x: x4, y: y4 }; } lineIntersectsCircle(x1, x2, y1, y2, r, rx, ry) { // first r is at 0,0 so we must first move the line the same distance // Normalize points x1 -= rx; y1 -= ry; x2 -= rx; y2 -= ry; let dx = x2 - x1; let dy = y2 - y1; let dr = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); let dd = (x1 * y2) - (x2 * y1); let di = (Math.pow(r, 2) * Math.pow(dr, 2)) - Math.pow(dd, 2); if (di < 0) { return false; } else { return true; } } distanceBetweenPoints(x1, x2, y1, y2) { let dx = Math.abs(x1 - x2); let dy = Math.abs(y1 - y2); return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); } doPathsCross(direction, boid) { let thisFututrePosition = this.findPoint(this.x, this.y, this.boidBuffer, direction); let thisPath = { x1: this.x, y1: this.y, x2: thisFututrePosition.x, y2: thisFututrePosition.y }; let boidFuturePosition = this.findPoint(boid.x, boid.y, boid.boidBuffer, boid.direction); let boidPath = { x1: boid.x, y1: boid.y, x2: boidFuturePosition.x, y2: boidFuturePosition.y }; let thisIntersectsBoid = this.pathsIntersect( thisPath.x1, thisPath.y1, thisPath.x2, thisPath.y2, boidPath.x1, boidPath.y1, boidPath.x2, boidPath.y2 ); if (thisIntersectsBoid) { return true; } return false; } pathsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { // Check if none of the lines are of length 0 if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) { return false } let denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)) // // Lines are parallel // if (denominator === 0) { // return false // } let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator // is the intersection along the segments if (ua < 0 || ua > 1 || ub < 0 || ub > 1) { return false } // Return a object with the x and y coordinates of the intersection let x = x1 + ua * (x2 - x1) let y = y1 + ua * (y2 - y1) return { x, y } } detectBox(width, height, direction) { let point = this.findPoint(this.x, this.y, this.wallBuffer, direction); if (point.x - this.radius < 0 || point.y - this.radius < 0 || point.x + this.radius > width - 0 || point.y + this.radius > height - 0 ) { return true; } return false; } findPoint(x1, y1, length, angle) { angle *= Math.PI / 180; let x2 = x1 + length * Math.cos(angle); // must be rad let y2 = y1 + length * Math.sin(angle); // must be radrayLength return { x: parseFloat((Number(x2).toFixed(2))), y: parseFloat(Number(y2).toFixed(2)) }; } draw(context) { this.drawBoid(context); this.drawRay(context, this.x, this.y, this.rayLength, this.direction); } drawBoid(context) { context.beginPath(); context.fillStyle = "blue"; context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); context.stroke(); context.restore(); } drawRay(context, x, y, perceptionDistance, direction) { let point = this.findPoint(x, y, perceptionDistance, direction); context.lineWidth = 1; context.beginPath(); context.moveTo(x, y); context.lineTo(point.x, point.y); context.stroke(); context.restore(); } } class Flock { constructor() { this.boids = []; } run() { for (let i = 0; i < this.boids.length; i++) { this.boids[i].run(boids); } } addBoid(boid) { this.boids.push(boid); } } class GameArea { constructor(canvas_width, canvas_height) { this.canvas = document.createElement("canvas"); this.canvas_width = canvas_width; this.canvas_height = canvas_height; } init() { this.canvas.width = this.canvas_width; this.canvas.height = this.canvas_height; this.context = this.canvas.getContext("2d"); document.getElementById('container').appendChild(this.canvas); } clear() { this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); } } class Scene { // boid radius // no of boids // scene width // scene height constructor(boid_radius, no_of_boids) { this.boid_radius = boid_radius; this.no_of_boids = no_of_boids; this.boids = []; this.gameArea = {}; this.started = false; this.width = 600; this.height = 600; this.initGameArea(); this.initBoids(); } initBoids() { let boids = []; for (let i = 0; i < this.no_of_boids; i++) { boids.push(new Boid( this.boid_radius, "black", // 300, 300, 90, parseFloat(Number(Math.random() * (this.gameArea.canvas.width - 100) + 50).toFixed(1)), parseFloat(Number(Math.random() * (this.gameArea.canvas.height - 100) + 50).toFixed(1)), parseFloat(Number(Math.random() * 360).toFixed(2)), i, this.width, this.height )); } this.boids = boids; } initGameArea() { this.gameArea = new GameArea(this.width, this.height); this.gameArea.init() } start() { if (this.started) { return; } // Kinda annoying, setInterval is a piece of shite and is always run from global scope // Therefore "this.update" will be undefined unless we bind it const updateMe = this.update.bind(this); this.interval = setInterval(updateMe, 20); // updateMe(); this.started = true; } update() { if (!this.started) { return; } this.gameArea.clear(); for (let i = 0; i < this.boids.length; i++) { let result = this.boids[i].move(this.boids); this.boids[i].draw(this.gameArea.context); if (!result) { continue; } } return true; } stop() { if (!this.started) { return; } clearInterval(this.interval); this.interval = null; this.started = false; } reset() { if (this.started) { this.stop(); } this.boids = new Array; this.initBoids(); this.gameArea.clear(); this.started = false; } } let scene = new Scene(5, 10); function stop() { scene.stop(); } function start() { scene.start(); } function reset() { scene.reset(); }