Last active
June 3, 2025 18:30
-
-
Save joshuabradley012/bd2bc96bbe1909ca8555a792d6a36e04 to your computer and use it in GitHub Desktop.
Revisions
-
joshuabradley012 revised this gist
Oct 13, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -216,7 +216,7 @@ const random = (max = 9, min = 0) => { const colors = ['red', 'green', 'blue', 'purple', 'orange']; const collidingBalls = ({ width = 400, height = 400, parent = document.body, count = 50 } = {}) => { const display = new Canvas(parent, width, height); const balls = []; for (let i = 0; i < count; i++) { -
joshuabradley012 revised this gist
Oct 12, 2020 . No changes.There are no files selected for viewing
-
joshuabradley012 revised this gist
Jul 26, 2020 . 1 changed file with 14 additions and 14 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -116,20 +116,6 @@ class Ball { this.collisions = this.collisions.slice(this.collisions.length - 3); } /** * this is the most stable solution to avoid overlap * but it is slightly inaccurate @@ -154,6 +140,20 @@ class Ball { actor.collisions.push(this.id + updateId); } } // setting bounds on the canvas prevents balls from overlapping on update const upperLimit = new Vector(state.display.canvas.width - this.radius, state.display.canvas.height - this.radius); const lowerLimit = new Vector(0 + this.radius, 0 + this.radius); // check if hitting left or right of container if (this.position.x >= upperLimit.x || this.position.x <= lowerLimit.x) { this.velocity = new Vector(-this.velocity.x, this.velocity.y); } // check if hitting top or bottom of container if (this.position.y >= upperLimit.y || this.position.y <= lowerLimit.y) { this.velocity = new Vector(this.velocity.x, -this.velocity.y); } const newX = Math.max(Math.min(this.position.x + this.velocity.x, upperLimit.x), lowerLimit.x); const newY = Math.max(Math.min(this.position.y + this.velocity.y, upperLimit.y), lowerLimit.y); -
joshuabradley012 created this gist
Jul 26, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,237 @@ class State { constructor(display, actors) { this.display = display; this.actors = actors; } update(time) { /** * provide an update ID to let actors update other actors only once * used with collision detection */ const updateId = Math.floor(Math.random() * 1000000); const actors = this.actors.map(actor => { return actor.update(this, time, updateId); }); return new State(this.display, actors); } } class Vector { constructor(x, y) { this.x = x; this.y = y; } add(vector) { return new Vector(this.x + vector.x, this.y + vector.y); } subtract(vector) { return new Vector(this.x - vector.x, this.y - vector.y); } multiply(scalar) { return new Vector(this.x * scalar, this.y * scalar); } dotProduct(vector) { return this.x * vector.x + this.y * vector.y; } get magnitude() { return Math.sqrt(this.x ** 2 + this.y ** 2); } get direction() { return Math.atan2(this.x, this.y); } } class Canvas { constructor(parent = document.body, width = 400, height = 400) { this.canvas = document.createElement('canvas'); this.canvas.width = width; this.canvas.height = height; parent.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); } sync(state) { this.clearDisplay(); this.drawActors(state.actors); } clearDisplay() { // opacity controls the trail effect set to 1 to remove this.ctx.fillStyle = 'rgba(255, 255, 255, .4)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.strokeStyle = 'black'; this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height); } drawActors(actors) { for (let actor of actors) { if (actor.type === 'circle') { this.drawCircle(actor); } } } drawCircle(actor) { this.ctx.beginPath(); this.ctx.arc(actor.position.x, actor.position.y, actor.radius, 0, Math.PI * 2); this.ctx.closePath(); this.ctx.fillStyle = actor.color; this.ctx.fill(); } } class Ball { constructor(config) { Object.assign(this, { id: Math.floor(Math.random() * 1000000), type: 'circle', position: new Vector(100, 100), velocity: new Vector(5, 3), radius: 25, color: 'blue', collisions: [], }, config ); } update(state, time, updateId) { /** * if slice occurs on too many elements, it starts to lag * collisions is an array to allow multiple collisions at once */ if (this.collisions.length > 10) { this.collisions = this.collisions.slice(this.collisions.length - 3); } // setting bounds on the canvas prevents balls from overlapping on update const upperLimit = new Vector(state.display.canvas.width - this.radius, state.display.canvas.height - this.radius); const lowerLimit = new Vector(0 + this.radius, 0 + this.radius); // check if hitting left or right of container if (this.position.x >= upperLimit.x || this.position.x <= lowerLimit.x) { this.velocity = new Vector(-this.velocity.x, this.velocity.y); } // check if hitting top or bottom of container if (this.position.y >= upperLimit.y || this.position.y <= lowerLimit.y) { this.velocity = new Vector(this.velocity.x, -this.velocity.y); } /** * this is the most stable solution to avoid overlap * but it is slightly inaccurate */ for (let actor of state.actors) { if (this === actor || this.collisions.includes(actor.id + updateId)) { continue; } /** * check if actors collide in the next frame and update now if they do * innaccurate, but it is the easiest solution to the sticky collision bug */ const distance = this.position.add(this.velocity).subtract(actor.position.add(actor.velocity)).magnitude; if (distance <= this.radius + actor.radius) { const v1 = collisionVector(this, actor); const v2 = collisionVector(actor, this); this.velocity = v1; actor.velocity = v2; this.collisions.push(actor.id + updateId); actor.collisions.push(this.id + updateId); } } const newX = Math.max(Math.min(this.position.x + this.velocity.x, upperLimit.x), lowerLimit.x); const newY = Math.max(Math.min(this.position.y + this.velocity.y, upperLimit.y), lowerLimit.y); return new Ball({ ...this, position: new Vector(newX, newY), }); } get area() { return Math.PI * this.radius ** 2; } get sphereArea() { return 4 * Math.PI * this.radius ** 2; } } // see elastic collision: https://en.wikipedia.org/wiki/Elastic_collision const collisionVector = (particle1, particle2) => { return particle1.velocity .subtract(particle1.position .subtract(particle2.position) .multiply(particle1.velocity .subtract(particle2.velocity) .dotProduct(particle1.position.subtract(particle2.position)) / particle1.position.subtract(particle2.position).magnitude ** 2 ) // add mass to the system .multiply((2 * particle2.sphereArea) / (particle1.sphereArea + particle2.sphereArea)) ); }; const isMovingTowards = (particle1, particle2) => { return particle2.position.subtract(particle1.position).dotProduct(particle1.velocity) > 0; }; const runAnimation = animation => { let lastTime = null; const frame = time => { if (lastTime !== null) { const timeStep = Math.min(100, time - lastTime) / 1000; // return false from animation to stop if (animation(timeStep) === false) { return; } } lastTime = time; requestAnimationFrame(frame); }; requestAnimationFrame(frame); }; const random = (max = 9, min = 0) => { return Math.floor(Math.random() * (max - min + 1) + min); }; const colors = ['red', 'green', 'blue', 'purple', 'orange']; const collidingBalls = ({ width = 400, height = 400, parent = document.body, count = 50 }) => { const display = new Canvas(parent, width, height); const balls = []; for (let i = 0; i < count; i++) { balls.push(new Ball({ radius: random(8, 3) + Math.random(), color: colors[random(colors.length - 1)], position: new Vector(random(width - 10, 10), random(height - 10, 10)), velocity: new Vector(random(3, -3), random(3, -3)), })); } let state = new State(display, balls); runAnimation(time => { state = state.update(time); display.sync(state); }); }; collidingBalls();