Vector math basics to animate a bouncing ball in JavaScript

Vector math is pretty much essential when you want to do any kind of physics simulation, be it as simple as a bouncing ball. While my goal originally was to implement a flocking simulation (like birds flying close to each other, but not too close), the lack of math skills led me to build a bouncing ball simulation first.

At the same time, I wanted to see what Khan Academy is all about. Turns out they have lessons on vector math, but there’s very little on vectors specifically. There are two lessons on vector basics, where this second one is a lot more practical. There are also two exercises, one for addition of vectors, one for scaling. Both are pretty easy and you should be done within a minute if you watched the video.

With that knowledge under your belt, let’s look at some practical application, the aforementioned bouncing ball. To start, take a look at the demo and play around with it, there are some instructions at the top.

You can find the source code for that demo on GitHub, here is the main file for the bouncing balls demo. I’m not going to discuss the Point and Vector classes that this uses, though you should take a look. They just implement adding and scaling of vectors, and calculating a vector based on two points. Let’s walk through the code:

var GRAVITY = new Vector(0, 9.81);
var FRICTION = 0.85;
var world = {
	x1: 0,
	y1: 0
};
$(window).resize(function() {
	world.x2 = $(window).width();
	world.y2 = $(window).height();
}).trigger("resize");

This defines two constants, GRAVITY and FRICTION, which we’ll use later to affect simulated objects. GRAVITY is a vector pointing downwards, where the second component represents the 9.81 meters per second, while the first component is zero. FRICTION is used in collisions later, and completely arbitrary.

The world object is also used in collision detection, and represents the dimensions of our 2d world. It starts at 0/0 in the left top corner, and ends in the right bottom corner. We bind a resize handler on window to update this calculation, that way collisions happen within the browser window, no matter how big it currently is.

Next up is the definition of our Ball class:

function Ball() {
	this.position = new Point(200, 200);
	this.output = $("<div>").addClass("dot").appendTo("body");
	this.velocity = new Vector(-5, 0);
}
Ball.prototype = {
	remove: function() {
		this.output.remove();
	},
	move: function() {
		// apply gravity
		this.velocity = this.velocity.add(GRAVITY.scale(0.1));

		// collision detection against world
		if (this.position.y > world.y2) {
			this.velocity.x2 = -this.velocity.x2 * FRICTION;
			this.position.y = world.y2;
		} else if (this.position.y < world.y1) {
			this.velocity.x2 = -this.velocity.x2 * FRICTION;
			this.position.y = world.y1;
		}
		if (this.position.x < world.x1) {
			this.velocity.x1 = -this.velocity.x1 * FRICTION;
			this.position.x = world.x1;
		} else {
			if (this.position.x > world.x2) {
				this.velocity.x1 = -this.velocity.x1 * FRICTION;
				this.position.x = world.x2;
			}
		}

		// update position
		this.position.x += this.velocity.x1;
		this.position.y += this.velocity.x2;

		// render
		this.output.css({
			left: this.position.x,
			top: this.position.y
		});
	}
};

This defines a Ball constructor, which initializes a new Ball at some arbitrary position, with some velocity to the left. It also creates a simple DOM element that we use for output.

The prototype of Ball has two methods. The remove method just removes the DOM element, which we use for cleanup. The move method is much more intersting: It gets called for each ‘tick’ of our animation loop, so we use it to update the current velocity, look for collisions, update the current position and render the result. Step by step:

this.velocity = this.velocity.add(GRAVITY.scale(0.1));

This adds GRAVITY to the balls velocity. While GRAVITY has a real world value, we need to adapt it to our pixel-based dimension. Doing this in every tick causes the ball to accelerate downwards, or when moving upwards, to deccelarate. With this alone our ball would start falling, but never stop. That’s where the next block comes in, the collision detection:

if (this.position.y > world.y2) {
	this.velocity.x2 = -this.velocity.x2 * FRICTION;
	this.position.y = world.y2;
} else if (this.position.y < world.y1) {
	this.velocity.x2 = -this.velocity.x2 * FRICTION;
	this.position.y = world.y1;
}
if (this.position.x < world.x1) {
	this.velocity.x1 = -this.velocity.x1 * FRICTION;
	this.position.x = world.x1;
} else {
	if (this.position.x > world.x2) {
		this.velocity.x1 = -this.velocity.x1 * FRICTION;
		this.position.x = world.x2;
	}
}

Here we compare the current position of our ball to the dimensions of the world. For each direction, there’s a check if the call is beyond the limit, if so, it inverts the velocity for that direction, while applying FRICTION. This causes the ball to bounce back slightly slower then it was before, simulating very primitive friction. To avoid glitches, where the ball goes beyond the world dimensions and doesn’t come back, the position gets updated to move it back inside the defined limits.

Now that we’ve updated the velocity (and fixed the position in case of a collision), we can update the resulting position and output it:

// update position
this.position.x += this.velocity.x1;
this.position.y += this.velocity.x2;

// render
this.output.css({
	left: this.position.x,
	top: this.position.y
});

This adds the velocity components to the position of the ball, then uses inline styles to update the position in the DOM.

Next we’ll look at the setup and animation loop:

var balls = [];
balls.push(new Ball());

// animation loop
setInterval(function() {
	balls.forEach(function(ball) {
		ball.move();
	});
}, 25);

Here we create an array of balls and add one initial Ball. Then start an interval to at 25ms, which should give us about 40 frames per second (fps). To get more smooth 60fps, we’d have to go down to 16.5ms, which would also be even more CPU intensive then this becomes with lots of balls.

Inside the interval, we just loop through all balls and call the move method for each. In a proper game engine, this loop would separate the position updates from the rendering to ensure that, when frames get dropped, the game itself doesn’t slow down.

Up next, we’ve got the code to add new balls, with user controlled initial velocity:

var start;
$(document).mousedown(function(event) {
	start = new Point(event.pageX, event.pageY);
}).mouseup(function(event) {
	var end = new Point(event.pageX, event.pageY);
	var ball = new Ball();
	ball.position = end;
	ball.velocity = start.relative(end).scale(0.2);
	ball.move();
	balls.push(ball);
});

Here we bind mousedown and mouseup events, each time creating a Point object from the pageX and pageY event properties. In the mouseup handler, we then use the end point as the starting position for the new Ball object. Using Point’s relative method, we calculate a vector between those two points, scale it down and use it as the velocity for the new ball. That way, you can just click anywhere to add a new ball, or click, drag and let go to create one with intial velocity based on the drag. To get the ball animated along with the others, its added to the balls array.

With that we’re almost at the end. The last piece just clears all balls when pressing Escape:

$(document).keyup(function(event) {
	if (event.keyCode === 27) {
		balls.forEach(function(ball) {
			ball.remove();
		});
		balls.splice(0, balls.length);
	}
});

And that’s it! Thanks to Vector, we’ve got a pretty sane implementation, and a good starting point for further improvements. And there’s lots of potential:

  • Better friction simulation: Currently balls keep bouncing pretty often, they don’t slow down as much as they should after loosing some height.
  • More collision detection: Detecting collisions with other balls, the mouse or other objects would make the whole thing a lot more interesting.
  • Better collision detection: Currently collision detection just happens against a fixed position, not the actual ball’s dimensions. Taking the (rounded) borders into account would make things quite a bit more complicated, but also more realistic.
  • More moving objects with other shapes: Currently there’s just pixels bouncing around, even though they’re rendering as balls. Adding square objects both animated and static could make things a lot more interesting.
  • 3D: Moving from a 2D to a 3D simulation involves adding another component to both the Vecotor and Point class, and would add that third dimensions to each calculation, making especially the collision detection, already the most complex part, even more complex.

With this, I’ll get back to working on the basics for my flocking simulation.

-Jörn

No more comments.
  1. Morgan 'ARR!' Allen

    Turns out circular collision detection is quite easy, probably the easiest form of collision detection. The basic formula is

    c1.radius+c2.radius < sqrt(sq(c2.x – c1.x) + sq(c2.y – c1.y)) === collided

    This basically says. If the sum of the distance from each circles center to edge is less then the distance between their center points they have collided.

    I did nearly the exact experiments several months ago while learning canvas. As I said, circular collision was easy. Squares are much more difficult because you have to determine which edges are going to collide. I actually ended up using circular collision detection first to determine if the squares where even close enough to bother with.

  2. Morgan 'ARR!' Allen

    I’ve realized that is backwards. Should be greater than (or equal to) not less than. Combined radii remains the same but the distance between center points shrinks until collision.

  3. Great stuff.
    I’ve been looking for a decent js physics tutorial.

  4. @Morgan: Oh nice, thanks for that info. I’ll give that a try.