Simulating forces and collisions in JavaScript

We're going to learn how to do funky things like this in vanilla JavaScript:

Hover over the demo above to attract the balls, or click to repel them. Note how they collide with the walls and each other. They also try to make their way 'home' based on their colour. Pretty cool, right?

About this guide

As a software developer and teacher, I created this interactive tutorial to help teach fun coding skills to my sixth form computer science students, but it's free for anyone to use. Those new to JavaScript and <canvas> should start with my learn JavaScript tutorial, at least if you get stuck.

Throughout the guide are various interactive demos/examples/animations. Most of these are displayed directly below some or all of the code behind them, and when that code is shown in a green box then you can edit that code to instantly see the results.

Demos can be restarted with their buttons, and stop running when they go off the screen, restarting when scrolled back into view.

The full code for each demo can be seen by clicking the buttons above them. From there, you'll be able to download a complete HTML page including the demo.

All demos use JavaScript without any frameworks or libraries. I'm a believer in reducing the amount of files and file sizes of web pages - it frustrates me greatly how horrendously bloated the web has become, and this is partly my attempt to reverse that trend.

This guide is strongly inspired by The Nature of Code. If you have more time, you should 100% check that out - it goes into far more detail and depth than this guide.

Let me know of any bugs, feedback or suggestions. I hope you enjoy your journey through this guide and that it helps you create amazing things!

Mr Jake Gordon

Setting up the canvas

All of the code in this tutorial involves drawing to a <canvas> element using its 2D context. You can follow along by making a simple HTML file - say index.html - and plonking this code in it:

... then opening it in a web browser. You should see this:

I'm going to assume you have at least the above code for the rest of this tutorial, and that you'll be adding extra code where it says your code here.

Drawing our first balls

We're just going to be drawing circles, which I'll call balls.

You can edit the code above and instantly see the results below. The first 3 arguments to ctx.arc() are x, y and radius. Try editing them to move the circle around and change its size.

Better to have an array to hold several balls, then have a loop that draws them all.

Try editing the code above to move the balls around, change their size or add more.

Notice how you can use canvas.width / 2 to get the horizontal centre of the canvas. Can you place one ball in each of the 4 corners of the canvas?

Animating balls

An animation is just a series of frames. On each frame, we first clear the canvas, then move objects by calculating their new positions, then draw them at those new positions.

We run drawLoop() on load, then setTimeout(drawLoop, 16) at the end of each loop ensures it runs again 16ms later. We could just use setInterval(drawLoop, 16) instead, but as we'll see later, using setTimeout() more closely matches our eventual usage of requestAnimationFrame(), which is a better way of syncing our loop with when your browser is ready to draw. In addition, by using setTimeout() instead of setInterval(), the first run of our loop will be straight away, rather than after the first interval.

The second argument to setTimeout() is the number of milliseconds between each frame. Change it to eg 500 above to run at 2fps (frames per second). Most screens run at 60fps (though some now run at twice this speed - 120fps), and 1000 ÷ 60 = 16.7, so 16 is a fair choice for now.

Try making the balls move faster or slower by changing ball.x += 1, and try to get them to move down, diagonally or to the left as well. Use the button to restart the animation if the balls have left the screen.

Colliding with walls

The previous animation is a bit boring after a few seconds, because the balls shoot off the right side of the canvas and never get seen again. Let's imagine a vertical wall down the middle of the canvas, and we need to keep the balls to the left of it.

Note that I've hidden the code for this one, but you can see it via the button below.

Each ball has a random speed between 0 and 5 ({ ... speed: Math.random() * 5 }). We're also using randomness to decide when to add new balls - only on 10% of frames, via if (Math.random() > 0.9). In our draw loop we add a ball's speed to its position with ball.x += ball.speed. After adding speed to position, we check to see if the ball has gone too far, and if it has, we multiply its speed by -1, which has the effect of reversing direction, as if hitting a wall:

Sticking just with horizontal movement for now, we can extend this concept to both vertical walls. The right-side wall is detected with if (ball.x > canvas.width) and the left-side wall with if (ball.x < 0). In both cases, we reverse direction by multiplying speed by -1.

Except... that doesn't quite work right does it? Particularly with the bigger balls, notice how they don't bounce until they're partly into the wall already. Our balls are positioned with their (x, y) coordinates at their centres, with radius r. We need to take that radius into account when calculating the collision with the walls:

But if you hit a few times, you may notice sometimes the balls start stuck on the walls. This is happening because some balls aren't starting fully on the screen, and thus every frame their speed is flipping between positive and negative. There are various ways we could fix this, one of which is to ensure all balls start fully on the screen, by taking their radius into account when randomising their start positions:

Each ball's x and y coordinates tell us where it's centre goes. Imagine a ball with x: 0 - its centre will be on the left edge of the canvas, with the left half of its body clipped off. To be fully on the canvas, the minimum x-ordinate is therefore the length of a ball's radius from the left edge, ie 0 + ball.r. On the right-side of the canvas, a ball positioned with x: canvas.width will have its right half clipped. This time we subtract a ball's radius to bring it fully inside - canvas.width - ball.r. So our random function needs to pick a number between ball.r and canvas.width - ball.r, which is achieved with Math.random() * (canvas.width - 2 * r) + r.

Note that we also randomised colours using hsla() which takes 4 arguments: hue, saturation, lightness and alpha/opacity. Hue is between 0 and 360 (degrees), while saturation and lightness are out of 100%, and alpha/opacity between 0 and 1. Try changing those values in the code below to see how the ball changes colour.

Notice that the previous animation can be a bit jerky? That's due to our use of setInterval(). Instead, we should use requestAnimationFrame() like this:

This way the code will run whenever the browser is ready to draw another frame, which may not always be exactly 60fps due to things like garbage collection or screens running at different refresh rates. Ideally we make use of dt, which is the delta or difference in time since the loop was last run, to ensure the animation runs smoothly and the same speed regardless of a screen's refresh rate.

To customise the animation speed (rather than an individual ball's speed), see the line ball.x += ball.speed * dt / 16 and change the 16 to something else. Note that it won't actually change the number of frames being rendered though - that is controlled by your screen's framerate via requestAnimationFrame(). You should always use dt in animation code, but to make code shorter and clearer I'll leave that detail out for now.

Speed vs velocity

So far we've got our balls moving in 1 dimension (right and left, along the x-ordinate). It's time to get them moving in 2 dimensions by adding some vertical motion. We also need to rethink what we mean by speed when we're working in 2D.

Say we want a ball to move to the centre of any of the surrounding 8 boxes. The time taken will depend on whether that box is on a diagonal, as the diagonal boxes are further away. Alternatively, they could arrive at their destination at the same time (as shown above) by the ones travelling diagonally travelling faster.

Consider just the 3 blue balls above which arrive at their destinations simultaneously. Each is travelling at the same horizontal speed. But in addition, the balls moving diagonally are also travelling with a vertical component. The one just going right has zero vertical speed.

When working in 2D, instead of speed we need to think about velocity, which is a vector with both magnitude and direction. The 4 balls following the yellow lines all have the same speed, but each one has a different velocity. Likewise, the 4 balls following the pink lines all have the same speed but different velocities too. By adding a velocity vector to a starting position, we arrive at a new position - in this case, 8 different positions for 8 different velocities.

Although we describe vectors as having magnitude and direction, they're usually stored as horizontal (x) and vertical (y) components, which can be converted via maths (trigonometry) to a magnitude and direction/angle if required. When working in 3D, we simply add a third component (z) to our vectors.

Our ball's position is also a vector - it can be seen as a displacement from the origin (0, 0), which in computer graphics is located in the top-right.

From now on, instead of storing each ball's x and y position separately, we're going to place them together under an attribute we'll call pos (for position), and speed velocity under vel. Both will have x and y components, which can be converted to magnitude/length and direction/angle if needed. Some languages come with in-built vector classes, but JavaScript doesn't, and we won't complicate things by adding one now.

Notice above how we now have to reference the x position of a ball with ball.pos.x instead of just ball.x, and the same for y as well. We also have to use the components of velocity with eg ball.vel.x when updating the position of the ball each frame.

Let's apply the same idea to multiple balls, and add collisions with horizontal walls.

Our code is getting a bit messy. We'll add a function drawBall() which deals with updating the position of a ball then drawing it, and another function setup() where we add all the balls at the beginning. It's important to define any variables we want to use in the global scope (outside functions), such as let balls = []. Refactoring the above in this way gives us:

Actually, we can do better. drawBall() isn't a very good name for a function that updates the position of a ball and then draws it. Better to split that into two functions, updateBall() and drawBall(), which we'll consistently use from now on:

Gravity and constant acceleration

To calculate the new position of a ball, we add its velocity vector to its position vector.

At the moment our velocity is constant, but what if we want it to speed up or slow down? For that, we introduce another vector, acceleration (which we'll name acc), again with x and y components. Before we add velocity to position, we first add acceleration to velocity. As we'll see later, this is a simplification of the real-world, and is know as the Euler method (or Euler integration).

Here we'll simulate gravity by applying a constant acceleration downwards, with a positive y component.

I've slowed the animation down and added a readout of position, velocity and acceleration so you can track how they change each frame.

Oops! The floor doesn't seem quite as solid as we first thought! We can fix that by clamping our y position so it never falls off the screen.

Remove or comment out the bottom line above to see how it fixed the problem of balls falling through the floor.

The animation may look a bit unrealistic, as the balls always bounce back up to where they started. In real life, there's friction and drag which slows things down. We can add some friction to the collision with the floor by multiplying velocity by a bit less than -1 each time:

Try tweaking the values above to change how much energy/speed is lost on a collision with the floor.

Unfortunately, there's a slight problem with our earlier attempt to stop balls from sneaking through the floor. Remember that this is the code we added within updateBall():

Below I've made the balls smaller and have them slightly offset to illustrate the problem - hit a few times to see what's happening. Notice they start off in a nice continuous line, but then after they bounce off the floor they get grouped with 2 or 3 other balls, which then continue to travel in lock-step.

The balls in each group would have passed through the floor during the same frame as each other. With Math.min() all have had their y position altered, but to the same value, regardless of how far each would have travelled through the floor (or should have bounced) in that frame.

Below I naively try to fix that by instead using this code:

Again, hit a few times to see what's happening.

My idea was that however much each ball would have travelled through the floor, that's how much it should have bounced up instead. But as you can see... we get something quite different.

In the real-world, acceleration caused by gravity is continuously applied to velocity. But in our animation, acceleration is applied in steps, once for each frame - that's the simplification given to us by the Euler method. But between one step and the next (or one frame and the next) the floor may have been hit, and as a result acceleration caused by gravity hasn't been applied correctly - it will have been acting in the wrong direction (up rather than down) for the portion of the motion after the collision.

We need to calculate exactly when in the frame the collision took place, so that we can apply the acceleration caused by gravity in the correct direction both before and after the collision.

We can use the equations of motion to help. These are also known as the 'suvat' equations, due to their usage of the letters: s for displacement; u for initial velocity; final velocity; acceleration and time. In particular, we can rearrange the equation s=u t + 12at2 to give us a quadratic which can then be solved with the quadratic formula to give us the time of the collision. Or, more specifically, the fraction of the frame which has elapsed at the moment of collision.

It works! And with some random balls, stronger gravity and less bouncy balls:

Adding the equations of motion made the code quite a bit more complex. For now, we'll ignore it, sticking to the Euler method of adding acceleration to velocity then velocity to position.

Other accelerations

So far we've only dealt with the constant 1 dimensional acceleration caused by gravity, which has a downwards direction. Like position (technically 'displacement') and velocity, acceleration is also a vector, and has both x and y components to give it direction and magnitude.

Here, in every frame we're randomising the acceleration of each ball. Math.random() gives a value between 0 and 1, and if we use that for our random acceleration it will always be positive (ie down and to the right). We instead choose a random value centred on zero by subtracting 0.5:

Instead, we might want to randomise each ball's acceleration once during setup, then apply that same acceleration each frame:

Because the same acceleration is applied every frame, balls just keep getting faster and faster! We could avoid that by clamping velocity, in the same way that air resistance limits the effects of gravity to a maximum terminal velocity. But for now, we'll instead consider the causes of accelerations - forces.

Windy forces

Gravity is a force that causes acceleration, just like other forces. Objects with no forces acting on them should have zero acceleration and continue with their previous velocity.

It actually doesn't make sense for a ball to have a persistent acceleration property. Put another way, its acceleration should be reset to zero every frame. Once reset, we can then apply forces which might be acting on it, and those forces give it its instantaneous acceleration for that frame. Each frame in our updateBall() function, we'll now do things like this:

So to apply gravity, we would add eg ball.acc.y += 1 after cancelling the previous acceleration, and at the same time we would apply other forces too but adding to the ball's acceleration vector. So instead of code like ball.acc.x = 2 we'll use ball.acc.x += 2.

Although wind is technically caused by a force, we'll imagine wind as a force in itself that we want to model. On a windy day you'll notice that wind isn't constant - it's continuously changing in both direction and magnitude. While gravity acts downwards, here we'll assume that wind only acts horizontally, both right and left (positive and negative in the x direction) with varying strength.

Above, we randomly pick a new value between -1 and 1 for wind each frame.

It's not a very convincing wind is it?! It's very jerky, all the balls move in lock-step, and if left to run a while, the balls can end up moving incredibly fast. Let's deal with each of those issues to create a more realistic wind.

The jerkiness is caused by the random number generator. Each frame, we're setting wind to be a completely random number between -1 and 1, which is being visualised with a 'wind vane' (click above for its code) at the centre of the canvas. That means one frame it could be -1 (strongly to the left), and the next it's +1 (strongly to the right). That doesn't really happen in the real world - it will be more gradual, with one frame's wind strength only varying a small amount from the last frame's.

Below, we compare the randomForce we used above to a betterForce which only changes from the previous frame by a small amount each frame. We also clamp it to stop it getting too strong.

The 2 wind vanes show the completely random force from the last example above, and our better force below.

An even better way to model the gradual random changes in wind would be to use something like 1D Perlin noise, which is beyond the scope of this guide.

Let's now apply our better, less jerky wind force to the balls.

Much better! But they're still all moving in lock-step, and they can still get very very fast if the random wind happens to be in the same direction for too long.

Think about 2 objects in the wind, one big and one small. Which is more affected by the wind? The smaller one. This is why the balls moving in lock-step looks a bit odd - we would expect the smaller ones to be more affected by the wind. We could achieve that by dividing the wind force by the ball's radius when adding it to acceleration. We're also going to clamp velocity below to stop the balls getting too fast.

You may want to tweak some of the numbers, such as maxSpeed or the magic 5 I'm multiplying the windForce by to compensate for the division by the ball's radius.

It works, but it would be nice to have something a bit more accurate than just dividing by a ball's radius. Newton's 2nd law of motion gives us the formula F=ma, which rearranged shows us that the acceleration caused by a force can be calculated by dividing the force by the mass. Assuming our balls represent spheres of equal density, we can give them mass using the formula for the volume of a sphere V=43πr3.

I'm happy enough with that wind now. You'll notice the magic number we're multiplying windForce by has now become 10,000 instead of 5 as our mass is much greater than the radius we were compensating for earlier. Why 10,000? Because it felt just aobut right to me! The real-world has SI units like metres and kilograms, but I haven't decided how many metres wide our canvas is (it's 600 pixels), or the density of the balls, so we'll simply continue using these magic numbers for now.

Blow your own wind

It's time to add some interactivity. Instead of random wind, how about if you could control the wind somehow with your mouse (or touch)? For now we'll assume just a single pointer (though with touchscreens you can have many touch points at once), which we'll track in a variable pointer, with both x and y for its position. Every time our mouse or finger moves on the screen, we'll update its position.

We also need a function that draws the pointer at its current position, and we add a call to that function within our drawLoop().

I'd like our wind to blow with a strength equal to its horizontal displacement from the horizontal centre of the screen, but in the opposite direction. So if our pointer is on the far left of the screen, I want that to signify a very strong wind to the right. Light breezes come from a pointer close to the horizontal centre.

And let's see that with the balls!

Note that I've tweaked the strength of the windForce by multiplying it by 10 when applying it to scale it better, and also doubled the maxSpeed. Again, you may want to tweak these yourself.

We may want our wind to stop when our pointer leaves the canvas. We can do this by listening for pointerout events.

Well, the wind stops blowing but the balls keep on moving. That's because when no forces act on an object, it continues with its previous velocity (Newton's first law of motion). This is fine for rockets in the vacuum space, but on Earth we're used to a bit of air resistance or drag which slows everything down.

We can calculate drag as a fraction of an object's velocity, in the opposite direction.

Have a play with the formulae for applying the wind force, drag and the speed clamp to see how it affects things.

We've probably had enough fun with wind for now. Next we'll look at attractive and repulsive forces working in 2 dimensions.

2D repulsion and attraction

Let's model a repulsive force that acts outwards from a point in all directions. We can think of it as pushing everything away from it.

Gravity acted downwards on all balls equally, and our wind acted horizontally on all balls with an intensity depending on their size. With our new force, we need to do some calculations to work out the direction the force will act on each ball individually. We can think of a different wind vane acting on each ball, depending on its position in relation to the repulsive force.

Except what we have above is a stronger force the further away from the point we go, while we want the reverse of that - the closer an object is, the greater the force we want acting on it. First, we'll normalise the magnitude of these vectors (by finding the hypotenuse using Pythagoras' theorem) so they're all the same length.

Next we scale those normalised vectors to be longer the closer they are to the point. More specifically, we might expect the intensity of the force to follow the inverse-square law, ie where intensity is inversely proportional to the square of the distance.

Now we'll attempt to use these vector forces to accelerate the balls. We'll also turn collisions with walls back on, otherwise we would need to add logic to wrap the force around the walls too.

As with wind earlier, it takes a far stronger force to accelerate larger objects (think about lifting up a heavy vs a light box - it's much harder to lift the heavy box), so we'll see smaller objects repelled with a faster acceleration.

We only have to add a - to negate our acceleration from the force to reverse it into an attractive force instead.

Although the force is attractive, it has a strange effect doesn't it? Balls slowly approach it, but then violently shoot away. As they approach the point their acceleration approaches infinity, which then slingshots them over to the other side. Of course with solid/rigid objects this wouldn't happen (they would crash into the other body instead!), but we haven't introduced them yet.

We can fix this problem by clamping the acceleration caused by the force.

That seems a bit more gentle now, and the balls tend to generally stick around the attractor.

I guess you might want to model the idea that each ball is attracted as if on an elastic band instead, where the further they are away the greater the attraction. I've also removed collisions with the walls for this example, and increased drag.

It's fun to play around with point forces to create different effects.

Planetary orbits and gravity

Earlier we modelled gravity as a force accelerating object downwards, for example towards Earth. But really gravity is a force between two bodies, pulling them towards each other depending on their masses and the distance separating them. There's no drag in the vacuum of space, so we can remove that, and we'll reduce our demo down to just 2 planetary bodies for now, such as the Earth and its moon.

We can use the formula for gravity to calculate the force vector acting between 2 planets.

If we calculated and applied gravity within updateBall() we would be inefficiently calculating twice for each pair of planets. Instead, we call applyGravity() just once.

We have to be a little careful here. Before, in updateBall() we first cancelled out acceleration then added to it from various forces. But because we're now applying gravity before updateBall() is called, we instead need to cancel out acceleration at the end rather than the beginning. So it's now structured like this:

When calculating the acceleration resulting from gravity on a particular planet, we must first divide the force by that planet's mass - the bigger the mass, the smaller the resulting acceleration. So while the gravitational force between the Earth and Moon is equal, the acceleration it causes to the moon is greater as the Moon has a smaller mass than the Earth.

Below we have 3 initially-stationary planetary bodies dancing with each other, each one attracted to the other 2.

Or we could have lots, each attracted to each other.

Or we could add our wall collisions back and invert gravity, creating a kind of anti-gravity.

Object collisions

When solid objects come into contact they don't simply pass through each other as though the other wasn't there. Instead, something happens depending on the properties of the objects colliding. Maybe they smash into tiny pieces, or perhaps they bounce off each other like snooker balls. We're going to try to model the latter.

When we're creating code for collisions, we need to consider both collision detection and collision resolution. When we decided the walls were solid, we detected the collisions by eg checking for the right-wall with ball.x + ball.r > canvas.width. We resolved the collision by inverting the x component of the ball's velocity - ball.vel.x *= .1.

Luckily, our balls are round and its relatively easy to detect their collisions. If we had more complex-shaped objects, our collision detection algorithm would need to be more sophisticated. What we'll find though, is that collision resolution is harder than collision detection.

Collision detection

For 2 circles, we can detect when they collide by simply comparing the distance between their centres with the sum of their radii. We can also give balls unique ids to avoid detecting collisions with themselves.

Harder is deciding when and how to do this check in an efficient way. Say we wanted to simply change their colour when they collide, we may try something like this:

It works! But it's terribly inefficient, as we can see when we add lots of balls. Click the demo below to see it running for 1 second. It's so inefficient it makes everything else on the page slower too (unless you have a particularly fast CPU - in which case, increase the numBalls by ~500 at a time until you see the slow down).

Why's it so slow? Well, for every ball we're checking for collisions with every other ball. In fact, if we consider just 3 balls (a, b and c), we check a-b, a-c, b-a, b-c, c-a and c-b. There's a relatively simple improvement we can make to halve the number of checks we do, so that eg the a-b check isn't duplicated with b-a. First, we'll add a collided property to each ball, then we'll use an efficient loop within our main drawLoop() to check each collision once only, setting the ball.collided property to true if a collision is detected.

That's better! Each drawLoop() now takes around half as long as it did before, as it has half as many checks to make. Remember we get into difficulty if every frame it takes us more time to do all the calculations than the time available to the frame - 16ms for a 60fps screen.

But there's more we can do to improve the efficiency of our algorithm! At the moment, we're still checking each ball against every other ball (but only once now instead of twice!).

The sort and sweep algorithm (also known as sweep and prune) can massively reduce the number of checks by first sorting all balls - for example by their x position - then sweeping through these sorted balls, keeping track of which ones are currently 'open', and only doing a full collision check when multiple balls are open at once. Algorithms like this can be thought of in terms of a broad phase (seeing if balls might collide based on their x position) and a narrow phase (in our case, checking radii vs distance between them).

As you can see, the code is a lot more complex now! But the improvement is dramatic. Even better, this algorithm scales better - while at 1,000 balls it is 4x faster on my computer, at 4,000 balls it is nearly 10x faster! When we have an algorithm which scales better, we say it has better time complexity (expressible in Big-O notation). There are various further improvements we could make, but they're beyond the scope of this guide.

Collision resolution

Now for the harder part. We've detected a collision between two balls, but how do we deal with that? When we collide with the right-wall, we reverse the horizontal component of velocity and also clamp the x position to ensure it doesn't penetrate the wall. Let's try something similar for 2 balls colliding with each other, and we'll start of working in just one dimension, dealing just with the horizontal component.

1D collision resolution

We'll take our same collision detection funcion from the previous example, and add a line to resolveCollision(a, b) if a collision is detected between a and b.

Here's our very simple collision resolution function for now:

I'm sure you can see various problems with this. The first pair of balls seem fine, as they're the same size and moving with equal but opposite velocity towards each other. But for the middle pair one ball is larger than the other - we would thus expect the larger ball to somehow be less influenced by the collision. Lastly, in the bottom pair the right-side ball should surely be affected by the colllision too, but because it has zero for horizontal velocity, if we stick a negative sign in front of that we still have zero.

Simply reversing the horizontal velocities of our colliding balls clearly isn't good enough. For elastic collisions that we're trying to model here, momentum (and kinetic energy) must be conserved.

An object's momentum is its mass multiplied by its velocity. For two balls moving at the same speed, one big and one small, the big ball has more momentum - that is, it takes a greater force to slow it down. To conserve momentum, the combined momentum of both balls before and after the collision need to be equal. The new velocity of ball a can be found using this expression: ma-mbma+mbva+2mbma+mbvb. Rearranging to simplify slightly gives va(ma-mb)+2mbvbma+mb, which we can then implement in resolveCollision().

Perfect! We have elastic collisions in one dimension, which also preserve kinetic energy (the sum of kinetic energy, given by 12mv2, is equal before and after collision). In inelastic collisions, such as 2 cars in a head-on collision, kinetic energy isn't preserved as energy is lost to heat and sound as the cars violently smash into each other and come to rest after a few seconds. For our model, they'll be no destructive smashing, just a peaceful continuation of balls on new trajectories with no loss of energy - an elastic collision. If we introduce a bit of friction (eg by multiplying new velocities by 0.9), we no longer have a fully elastic collision, as we're modelling a bit of energy being lost to the heat of friction instead.

2D collision resolution

Our maths is about to get a bit more spicy as we add the vertical dimension. We'll start with a naive approach, treating our x and y components of velocity completely independently of each other.

At first glance, it may seem that everything's working correctly. But look closer, and you'll start to see some suspicious behaviour. Specifically, when 2 balls collide off-centre, they don't continue in the direction you'd expect them to. Let's look closely at just one collision:

When the moving orange ball hits the stationary green ball, currently the green ball moves only horizontally, but we would expect it to move in a slightly downwards direction as well, because it's being hit on its top-half. Here's what we're hoping to see instead:

Let's just have a look at the moments of collision, and consider what should be happening.

I've added 2 lines to each collision: the line of impact and the tangent to the collision. To the left of the collision, I've also added a diagram which shows the horizontal and vertical components of the velocity vectors for the 2 balls drawn end-to-end.

In the collision above, notice that the stationary green ball simply follows the line of impact, while the orange ball continues in a more complex direction. Before the collision we have only a horizontal component of velocity, as the green ball is stationary and the orange ball is moving only to the right. After the collision, the combined velocity vector remains the same, as shown by the end-to-end vectors diagram still ending at the same point. This is important - for 2 equal-mass balls, the combined velocity before the collision must be equal to the combined velocity after (and for different-mass balls, the combined momentum must be the same). As a result, because the green ball is travelling with a vertical component of velocity (in order to follow the line of impact), the orange ball must also travel with a vertical component of velocity, of equal magnitude but in the opposite direction. Similarly, because the green ball must travel along the line of impact, it must have a horizontal component of its velocity, and so the orange ball will need to lose some horizontal velocity (so that the combined horizontal velocity after collision is equal to the combined horizontal velocity before collision).

But how do we calculate what fractions of the initial horizontal and vertical velocity is given to each ball? Let's think in terms of forces acting in the collision. Any forces at play will be along the line of impact, so we'll start by redrawing our end-to-end velocity vectors in a rotated coordinate system aligned with the line of impact. In this coordinate system, our orange ball no longer just has horizontal velocity, but a mixture of vertical and horizontal velocity.

TODO: more work needed in here!

But there's a far simpler way to do all this without trigonometry! TODO: finish this.

Further reading

Here are some of the resources I read (or watched) to help me understand the concepts presented in this guide. I particularly recommend working through the entirety of The Nature of Code, and watching the Coding Train videos on YouTube by the same person.

TODO: very much not finishet yet, lots more still to come...