Problem Description
I'm currently creating a Canvas based app that simulates balls and gravity. After the balls collide, their position is resolved and their velocity is calculated depending upon the angle they collide. However, once the balls begin to settle, they become "twitchy" and won't stop moving. How can I solve this?
The Relevant code
bindToScreen(s) {
if (this.position.y + this.radius > s.height) {
this.position.y = s.height - this.radius;
this.velocity.y *= -this.bounce;
}
if (this.position.y - this.radius < 0) {
this.position.y = this.radius;
this.velocity.y *= -1;
}
if (this.position.x + this.radius > s.width) {
this.position.x = s.width - this.radius;
this.velocity.x *= -1;
}
if (this.position.x - this.radius < 0) {
this.position.x = this.radius;
this.velocity.x *= -1;
}
}
handleCollision(p2, s) {
let d = this.position.distanceVector(p2);
let angle = Math.atan2(d.y, d.x),
spread = (this.radius + p2.radius) - this.position.distanceBetween(p2) + 1,
ax = spread * Math.cos(angle),
ay = spread * Math.sin(angle);
// Separate
this.position.x -= ax * 2;
this.position.y -= ay * 2;
// Bounce
this.velocity.x -= Math.cos(angle);
this.velocity.y -= Math.sin(angle);
p2.velocity.x += Math.cos(angle);
p2.velocity.y += Math.sin(angle);
if (this.position.x + this.radius + this.velocity.x > s.width) {
this.position.x = s.width - this.radius;
}
if (this.position.y + this.radius + this.velocity.y > s.height) {
this.position.y = s.height - this.radius;
}
this.bindToScreen(s);
}
Resources I've used to put this code together
Update
As per AlbertoSinigaglia's suggestion, I tried to null the velocity if it was sufficiently small after a collision. However, that results in even weirder behaviour like floaty balls after a couple of collisions. Fiddle showing this here. I thought this may have been the gravity continuously increasing the y
velocity, so I tried setting that to 0 too. Same problem.
// If velocity is very small, set it to 0
if(this.velocity.y > -0.1 && this.velocity.y < 0.1){
this.velocity.y = 0;
this.gravity = 0;
}
if(this.velocity.x > -1 && this.velocity.x < 1){
this.velocity.x = 0;
}
A full snippet
The code is unfortunately pretty long, so here's a hidden snippet.
class Canvas{
constructor(w, h){
this.width = w;
this.height = h;
this.element = document.getElementById('canvas');
this.ctx = this.element.getContext('2d');
this.element.width = this.width;
this.element.height = this.height;
}
clear(){
this.element.width = this.width;
}
drawCircle(c){
this.ctx.beginPath();
this.ctx.arc(c.x, c.y, c.radius, 0, 2 * Math.PI);
this.ctx.fillStyle = c.color;
this.ctx.fill();
this.ctx.closePath();
}
}
class Vector2D{
constructor(x = 0, y = 0){
this.x = x;
this.y = y;
}
add(v){
this.x += v.x;
this.y += v.y;
}
sub(v){
this.x -= v.x;
this.y -= v.y;
}
mul(v){
this.x *= v.x;
this.y *= v.y;
}
div(v){
this.x /= v.x;
this.y /= v.y;
}
distanceVector(v2){
return new Vector2D(v2.x-this.x, v2.y-this.y);
}
distanceBetween(v2){
let dx = v2.x - this.x;
let dy = v2.y - this.y;
return Math.sqrt(dx*dx+dy*dy);
}
}
class Circle{
constructor(x, y, r, c){
this.position = new Vector2D(x, y);
this.radius = r;
this.color = c;
}
get x(){
return this.position.x;
}
get y(){
return this.position.y;
}
collision(c2){
return this.position.distanceBetween(c2) < this.radius + c2.radius;
}
render(s){
s.drawCircle(this);
}
}
class Particle extends Circle {
constructor(x, y, r, c, v) {
super(x, y, r, c);
this.velocity = new Vector2D(v.x, v.y);
this.mass = this.radius;
this.gravity = 0.8;
this.bounce = 0.8;
this.resistance = 0.5;
}
bindToScreen(s) {
if (this.position.y + this.radius > s.height) {
this.position.y = s.height - this.radius;
this.velocity.y *= -this.bounce;
}
if (this.position.y - this.radius < 0) {
this.position.y = this.radius;
this.velocity.y *= -1;
}
if (this.position.x + this.radius > s.width) {
this.position.x = s.width - this.radius;
this.velocity.x *= -1;
}
if (this.position.x - this.radius < 0) {
this.position.x = this.radius;
this.velocity.x *= -1;
}
}
checkCollisions(p, s) {
p.forEach(p2 => {
if (p2 !== this) {
if (this.collision(p2)) {
this.handleCollision(p2, s);
}
}
});
}
handleCollision(p2, s) {
let d = this.position.distanceVector(p2);
let angle = Math.atan2(d.y, d.x),
spread = (this.radius + p2.radius) - this.position.distanceBetween(p2) + 1,
ax = spread * Math.cos(angle),
ay = spread * Math.sin(angle);
// Separate
this.position.x -= ax * 2;
this.position.y -= ay * 2;
// Bounce
this.velocity.x -= Math.cos(angle);
this.velocity.y -= Math.sin(angle);
p2.velocity.x += Math.cos(angle);
p2.velocity.y += Math.sin(angle);
if (this.position.x + this.radius + this.velocity.x > s.width) {
this.position.x = s.width - this.radius;
}
if (this.position.y + this.radius + this.velocity.y > s.height) {
this.position.y = s.height - this.radius;
}
this.bindToScreen(s);
}
update(s) {
this.velocity.y += this.gravity;
this.velocity.x *= -this.resistance;
this.position.add(this.velocity);
this.bindToScreen(s);
}
}
class Game{
constructor(){
this.screen = new Canvas(800, 600);
this.particles = [];
this.particleCount = 10;
this.particleSize = 20;
this.delay = 25;
}
run(){
for(let i = 0; i < this.particleCount; i++){
let _this = this;
setTimeout(() => {
_this.particles.push(new Particle(this.particleSize, this.particleSize, this.particleSize, 'red', {x: 7, y: 7}));
}, _this.delay * i);
}
this.loop();
};
update(){
this.particles.forEach(particle => particle.update(this.screen));
};
checkCollisions(){
this.particles.forEach(particle => particle.checkCollisions(this.particles, this.screen));
}
render(){
this.screen.clear();
this.particles.forEach(particle => particle.render(this.screen));
};
loop(){
this.update();
this.checkCollisions();
this.render();
requestAnimationFrame(this.loop.bind(this));
}
}
let game = new Game();
game.run();
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
html, body{
background-color: #efefef;
}
canvas{
width: 800px;
height : 600px;
background-color: rgba(255, 255, 255, 0.75);
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
margin: auto;
box-shadow: 0 0 40px #dddddd;
}
<canvas id="canvas" width="800" height="600"></canvas>