images

Chapter 11

Billiard Ball Physics

What we'll cover in this chapter:

  • Mass
  • Momentum
  • Conservation of momentum

As you might expect in a technical book like this, things start off simple and gradually become more complex. With this chapter, you reach a pinnacle of complexity. Not that the rest of the chapters after this are all downhill, but this one requires that you don't skimp on the material that came earlier. That said, we'll walk through the concepts step by step, and if you've followed along reasonably well up to now, you should be fine.

Specifically, this chapter focuses on momentum: what happens to the momentum of two objects that collide, the conservation of momentum, and how to apply this conservation of momentum to the objects we draw.

As the objects used in these examples are all round, for simplicity's sake, this subject is often referred to as “billiard ball physics.” And you'll soon see that these examples do look like a bunch of different-sized billiard balls hitting each other.

As in previous chapters, the code examples will start in one dimension to keep things simpler and easier to understand. Then we move into two dimensions, at which point you'll need to jump into some coordinate rotation (the subject of the previous chapter). Essentially, you'll rotate the two-dimensional scene so it lies flat, which you can then ignore one axis and treat it as a one-dimensional scene. But all that is just to whet your appetite for what's coming up. Let's start with the concepts of mass and momentum.

Mass

The earlier chapters of the book covered several aspects of motion: velocity, acceleration, vectors, friction, bouncing, easing, springing, and gravity. Until now, we have ignored the concept of an object's mass when being moved. Now, scientifically speaking, mass should have been in the equation, but we've generally concentrated on doing things mostly correctly, and kept the emphasis on making sure it looks right. Most important, the final result must be efficient enough so that the web browser can run smoothly in the process. However, mass is so tied up in the subject of momentum that we can no longer ignore it.

So just what is mass? Here on Earth, we usually think of mass as how much something weighs. And that's pretty close, as weight is proportional to mass. The more mass something has, the more it weighs. In fact, we use the same terms to measure mass and weight: kilograms, pounds, and so on. But technically speaking, mass is the measurement of how much an object resists change in velocity. Thus, the more mass an object has, the harder it is to move that object or to change how that object moves (slow it down, speed it up, or change its direction).

This also relates to acceleration and force. The more mass something has, the more force you need to apply to it to produce a given acceleration. This is expressed in the equation:

F = m × a

For example, the engine in a compact car is designed to produce enough force to provide reasonable acceleration on the mass of a compact car; but it's not going to produce enough force to accelerate a large truck. The engine needs a lot more force, because the truck has a lot more mass.

Momentum

Now we move on to momentum, which is the product of an object's mass and velocity. In other words, mass times velocity. Momentum is indicated by the letter p, and mass by m, and is expressed as:

p = m × v

This means that an object with a small mass and high velocity can have similar momentum to an object with a large mass and low velocity. If the aforementioned truck moving at a mere 20 miles an hour, or a bullet with a tiny mass but a much higher velocity, collided with you, they'd both ruin your day. Here, you can see how two objects with a different mass and velocity can have an equal momentum (where m/s is meters per second):

4 kg × 15 m/s = 20 kg × 3 m/s = 60 kg m/s

Using the formula, a 4 kg ball rolling down a hill at 15 m/s has a momentum of 60 kg m/s. Because velocity is a vector (direction and magnitude), momentum must also be a vector. The direction of the momentum vector is the same as the direction of the velocity vector. Thus, to fully describe momentum, you express it like this:

4 kg × 15 m/s at 23 degrees

With this background knowledge, you see next how you can apply this to collisions.

Conservation of Momentum

By now you should be familiar with collisions. You've read an entire chapter on collision detection and even faked some collision reactions between two objects. Conservation of momentum is the exact principle you need to respond realistically to a collision.

Using the conservation of momentum, you can determine how objects react after a collision, so you can say: “This object moved at velocity A and that object moved at velocity B before the collision. Now, after the collision, this object moves at velocity C and that object moves at velocity D.” To break it down further, because velocity is just speed and direction, if you know the speed and direction of two objects just before they collide, you can figure out the speed and direction they will move in after the collision. This is a useful.

But there's a catch: You need to know each object's mass. So, what this means is that if you know the mass, speed, and direction of each object before the collision, you can figure out where and how fast the objects will go after they collide.

That's what conservation of momentum can do for you—but what is it? The Law of Conservation of Momentum is a fundamental concept of physics that says the total momentum for a system before a collision is equal to the total momentum after a collision. But what is this system the law refers to? This is just a collection of objects with momentum. Most discussions also specify that this is a closed system, which is a system with no other forces or influences acting on it. In other words, you can just ignore anything but the actual collision itself. For our purposes, we always consider just the reaction between two objects, so our system is always something like object A and object B.

The total momentum of the system is the combined momentum of all the objects in the system, so for us, this means the combined momentum of object A and object B. If you combine the momentums before the collision and combine the momentums afterward, the result should be the same.

Before we jump into the math, here's a suggestion. Don't worry too much about trying to figure out how to convert this to real code—we get to that soon enough. Just try to look at the next few formulas from a conceptual viewpoint, “This plus that equals that plus this.” It translates neatly into code by the end of this chapter.

If combined momentum before and after the collision is the same, and momentum is velocity times mass, then for two objects—object 0 and object 1—you can come up with something like this:

momentum0 + momentum1 = momentum0Final + momentum1Final

or

(m0 × v0) + (m1 × v1) = (m0 × v0Final) + (m1 × v1Final)

To find the final velocities for object 0 and object 1, they are v0Final and v1Final. The way to solve an equation with two unknowns is to find another equation that has the same two unknowns in it—and it just so happens there is such an equation floating around the halls of the world's physics departments. It has to do with kinetic energy. You don't have to know, or even care, what kinetic energy is about, you just borrow the formula to help you solve your own problem and be done with it. Here's the equation for kinetic energy:

KE = 0.5 × m × v2

Technically, kinetic energy is not a vector, so although you use the v for velocity, it deals with only the magnitude of the velocity. It doesn't care about the direction, but that won't hurt your calculations.

Now, it happens that the kinetic energy before and after a collision remains the same. So, you can do something like this:

KE0 + KE1 = KE0Final + KE1Final

or

(0.5 × m0 × v02) + (0.5 × m1 × v12) = (0.5 × m0 × v0Final2) + (0.5 × m1 × v1Final2)

You can then factor out the 0.5 values to get this:

(m0 × v02) + (m1 × v12) = (m0 × v0Final2) + (m1 × v1Final2)

Notice that you have a different equation with the same two unknown variables: v0Final and v1Final. You can now factor these out and come up with a single equation for each unknown. These are the formulas that you end up with when all is done:

          (m0 − m1) × v0 + 2 × m1 × v1
v0Final = ----------------------------
                 m0 + m1

          (m1 − m0) × v1 + 2 × m0 × v0
v1Final = ----------------------------
                 m0 + m1

Now you can see why you have reached a pinnacle of complexity in this book. Actually, you haven't quite reached it yet. You're about to apply this to one axis, and after that, you're going to dive in and add coordinate rotation to it when you move to two axes. Hold on!

Conservation of Momentum on One Axis

Now that you've got the formulas, you can start animating with them. For this first example, you'll again use the Ball class, but we've added a mass property to it. Here is the new code (ball.js):

function Ball (radius, color) {
  if (radius === undefined) { radius = 40; }
  if (color === undefined) { color = "#ff0000"; }
  this.x = 0;
  this.y = 0;
  this.radius = radius;
  this.vx = 0;
  this.vy = 0;
  this.mass = 1;
  this.rotation = 0;
  this.scaleX = 1;
  this.scaleY = 1;
  this.color = utils.parseColor(color);
  this.lineWidth = 1;
}
Ball.prototype.draw = function (context) {
  context.save();
  context.translate(this.x, this.y);
  context.rotate(this.rotation);
  context.scale(this.scaleX, this.scaleY);
  context.lineWidth = this.lineWidth;
  context.fillStyle = this.color;
  context.beginPath();
  context.arc(0, 0, this.radius, 0, (Math.PI * 2), true);
  context.closePath();
  context.fill();
  if (this.lineWidth > 0) {
    context.stroke();
  }
  context.restore();
};

Ball.prototype.getBounds = function () {
  return {
    x: this.x - this.radius,
    y: this.y - this.radius,
    width: this.radius * 2,
    height: this.radius * 2
  };
};

In the next example (document 01-billiard-1.html), you create two different balls with different sizes, positions, and masses. Ignore the y axis this time around, so the balls have the same vertical position. When you load the example in your browser, the setup looks something like Figure 11-1.

images

Figure 11-1. The anticipation builds! Setting up objects for conservation of momentum on one axis.

The balls are created and positioned at the beginning of the script. In the drawFrame animation loop, we set up some basic motion code for one-axis velocity and simple distance-based collision detection; we'll add the reaction code in a moment:

<!doctype html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Billiard 1</title>
  <link rel="stylesheet" href="style.css">
 </head>
 <body>
  <canvas id="canvas" width="400" height="400"></canvas>
  <script src="utils.js"></script>
  <script src="ball.js"></script>
  <script>
  window.onload = function () {
    var canvas = document.getElementById('canvas'),
        context = canvas.getContext('2d'),
        ball0 = new Ball(),
        ball1 = new Ball();

    ball0.mass = 2;
    ball0.x = 50;
    ball0.y = canvas.height / 2;
    ball0.vx = 1;

    ball1.mass = 1;
    ball1.x = 300;
    ball1.y = canvas.height / 2;
    ball1.vx = -1;

    (function drawFrame () {
      window.requestAnimationFrame(drawFrame, canvas);
      context.clearRect(0, 0, canvas.width, canvas.height);

      ball0.x += ball0.vx;
      ball1.x += ball1.vx;
      var dist = ball1.x − ball0.x;

      if (Math.abs(dist) < ball0.radius + ball1.radius) {
        //reaction will go here
      }

      ball0.draw(context);
      ball1.draw(context);
    }());
  };
  </script>
 </body>
</html>

With this basic setup in place, we'll now look at how to program the reaction. Taking ball0 first, and considering that ball0 is object 0 and ball1 is object 1, you need to apply the following formula:

          (m0 − m1) × v0 + 2 × m1 × v1
v0Final = ----------------------------
                 m0 + m1

In JavaScript, this becomes the following code:

var vx0Final = ((ball0.mass - ball1.mass) * ball0.vx + 2 * ball1.mass * ball1.vx) /
               (ball0.mass + ball1.mass);

It shouldn't be too hard to see where that came from. You can then do the same thing with ball1, so this:

          (m1 − m0) × v1 + 2 × m0 × v0
v1Final = ----------------------------
                 m0 + m1

becomes this:

var vx1Final = ((ball1.mass - ball0.mass) * ball1.vx + 2 * ball0.mass * ball0.vx) /
               (ball0.mass + ball1.mass);

After adding the reaction code, the complete drawFrame function ends up like this:

(function drawFrame () {
  window.requestAnimationFrame(drawFrame, canvas);
  context.clearRect(0, 0, canvas.width, canvas.height);

  ball0.x += ball0.vx;
  ball1.x += ball1.vx;
  var dist = ball1.x − ball0.x;

  if (Math.abs(dist) < ball0.radius + ball1.radius) {
    var vx0Final = ((ball0.mass - ball1.mass) * ball0.vx + 2 * ball1.mass * ball1.vx) /
                   (ball0.mass + ball1.mass),
        vx1Final = ((ball1.mass - ball0.mass) * ball1.vx + 2 * ball0.mass * ball0.vx) /
                   (ball0.mass + ball1.mass);
    ball0.vx = vx0Final;
    ball1.vx = vx1Final;

    ball0.x += ball0.vx;
    ball1.y += ball1.vx;
  }

  ball0.draw(context);
  ball1.draw(context);
}());

Because the momentum calculations reference the velocity of each ball, you need to store their results in the temporary variables vx0Final and vx1Final, rather than assign them directly to the ball0.vx and ball1.vx properties.

Placing the Objects

The last two lines of the reaction code we just added deserve some explanation. After you figure out the new velocities for each ball, you add them back to the ball's position. If you recall the bouncing examples in Chapter 6, you needed to move the ball so that it touched the edge of the wall and not get stuck. Otherwise, it's possible that the ball can miss the edge and on subsequent frames, it can bounce back and forth along the boundary. It's the same problem in this example, but now there are two moving objects that you don't want embedded in each other.

You can place one of the balls just on the edge of the other one, but which one should you move? Whichever one you moved would appear to “jump” into its new position unnaturally, which would be especially noticeable at low speeds.

There are probably a number of ways to determine the correct placement of the balls, ranging from simple to complex and accurate to totally faked. The simple solution we used for this first example is to add the new velocity back to the objects, moving them apart again. This is realistic, quite simple, and accomplished in two lines of code. You can also see that the total system momentum is still the same after the collision, with -1/3 as the final velocity value for vx0Final and 5/3 for vx1Final:

(ball0.mass * ball0.vx) + (ball1.mass * ball1.v) = (2 * 1) + (1 * -1) = (2 * -1/3) + (1 * 5/3)
= 1

Later, in the “Solving a Potential Problem” section, you'll see a problem that can crop up with this method and we'll work on a solution that's a little more robust.

Go ahead and load the 01-billiard-1.html document in your browser. Change the masses and velocities of each ball until you see what's going on, and then change the sizes of each. You'll see that the size doesn't have anything to do with the reaction. In most cases, the larger object has a higher mass and you can probably figure out the relative area of the two balls and set some realistic masses for their sizes. But usually, it's more effective to just mess around with numbers for mass until things look and feel right. We're creating visual programs, so, sometimes, the eye is the best judge.

Optimizing the Code

The worst part of this solution is that huge equation right in the middle of the code, and that we have almost exactly the same equation in there twice. We should get rid of one of them.

But it's going to take a bit more math and algebra. You need to find the relative velocity of the two objects before the condition; this is their combined total velocity. Then, after you get the final velocity of one object, you can find the difference between it and the total velocity to get the final velocity for the other object.

You find the total velocity by subtracting the velocities of the two objects. That might seem strange, but think of it from the viewpoint of the system. Let's imagine the system has two cars going the same direction on a highway. One is moving at 50 mph and the other at 60 mph. Depending on which car you're in, you can see the other car going at 10 mph or -10 mph. In other words, it's either slowly moving ahead of you or falling behind you.

Before you do anything with collisions, you need to find out the total velocity (from ball1's viewpoint) by subtracting ball1.vx from ball0.vx:

var vxTotal = ball0.vx - ball1.vx;

Then, after calculating vx0Final, add that to vxTotal, and you'll have vx1Final. This might not be the most intuitive formula, but you'll see it works:

vx1Final = vxTotal + vx0Final;

Now that's better than that horrible double formula. Also, since the formula for ball1.vx doesn't reference ball0.vx anymore, you can get rid of the temporary variables. Here's the revised drawFrame function (from document 02-billiard-2.html):

(function drawFrame () {
  window.requestAnimationFrame(drawFrame, canvas);
  context.clearRect(0, 0, canvas.width, canvas.height);

  ball0.x += ball0.vx;
  ball1.x += ball1.vx;
  var dist = ball1.x − ball0.x;

  if (Math.abs(dist) < ball0.radius + ball1.radius) {
    var vxTotal = ball0.vx − ball1.vx;
    ball0.vx = ((ball0.mass - ball1.mass) * ball0.vx + 2 * ball1.mass * ball1.vx) /
               (ball0.mass + ball1.mass);
    ball1.vx = vxTotal + ball0.vx;

    ball0.x += ball0.vx;
    ball1.y += ball1.vx;
  }

  ball0.draw(context);
  ball1.draw(context);
}());

We've gotten rid of quite a few math operations and still have the same result—not bad.

This isn't one of those formulas that you're necessarily going to understand inside out. You might not memorize it, but at least you know you can always find the formula here. Whenever you need it for your own programs, just pull this book out and copy it!

Conservation of Momentum on Two Axes

Take a deep breath, because you're going to the next level. So far, you've applied a long-winded formula, but it's pretty much plug-and-play. You take the mass and the velocity of the two objects, plug them into the formula, and get your result.

Now we throw one more layer of complexity into it—another dimension. You use coordinate rotation to do it, but let's take a look at why.

Understanding the Theory and Strategy

Figure 11-2 illustrates the example you just saw: collision in one dimension.

images

Figure 11-2. A one-dimensional collision

As you can see, the objects have different sizes, different masses, and different velocities. The velocities are represented by arrows coming out from the center of each ball—these are vectors. A velocity vector points in the direction of the motion and its length indicates the speed.

The one-dimensional example is simple because both velocity vectors were along the x axis, so you can add and subtract their magnitudes directly. But, take a look at Figure 11-3, which shows two balls colliding in two dimensions.

images

Figure 11-3. A two-dimensional collision

Because the velocities are in completely different directions, you can't just plug the velocities into the momentum-conservation formula. So, how do you solve this?

You start by making the second diagram look a bit more like the first by rotating it. First, figure out the angle formed by the positions of the two balls and rotate the entire scene—positions and velocities—counterclockwise by that amount. For example, if the angle is 30 degrees, rotate everything by -30. This is exactly the same thing you did in Chapter 10 to bounce off an angled surface. The resulting diagram looks like Figure 11-4.

images

Figure 11-4. A two-dimensional collision, rotated

That angle between the two balls is important; that's the angle of collision. It's the only part of the ball's velocities that you care about—the portion of the velocity that lies on that angle.

Take a look at the diagram in Figure 11-5. Here, we've added vector lines for the vx and vy for both velocities. The vx for both balls lies exactly along the angle of collision.

images

Figure 11-5. Draw in the x and y velocities.

Because the only portion of the velocity you care about is the part that lies on the angle of collision—which is now your vx—you can just forget all about vy. And, as you can see in Figure 11-6, it's been taken out of the diagram.

images

Figure 11-6. All you care about is the x velocity.

This should look familiar, because it's the first diagram! You can easily solve this using the plug-and-play momentum formula. When you apply the formula, you wind up with two new vx values. Remember that the vy values never change, but the alteration of the vx alone changed the overall velocity to look something Figure 11-7.

images

Figure 11-7. New x velocities and the same y velocities with the result of a new overall velocity

Now you just rotate everything back again, as shown in Figure 11-8, and you have the final real vx and vy for each ball.

images

Figure 11-8. Everything rotated back

That's what the process looks like as a diagram, but now we have to convert all this into code.

Writing the Code

To begin, you create a base document that enables two balls to move at any angle and eventually hit each other. Starting off with the same setup as before, you have two Ball instances: ball0 and ball1. Let's make them a little larger now, as shown in Figure 11-9, so there's a good chance of them bumping into each other.

images

Figure 11-9. Setting the objects for the conservation of momentum on two axes

Here's the example, 03-billiard-3.html, though don't test it yet, because we still need to define the checkCollision function, which we do in a moment:

<!doctype html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Billiard 3</title>
  <link rel="stylesheet" href="style.css">
 </head>
 <body>
  <canvas id="canvas" width="400" height="400"></canvas>
  <script src="utils.js"></script>
  <script src="ball.js"></script>
  <script>
  window.onload = function () {
    var canvas = document.getElementById('canvas'),
        context = canvas.getContext('2d'),
        ball0 = new Ball(80),
        ball1 = new Ball(40),
        bounce = -1.0;
        ball0.mass = 2;

    ball0.x = canvas.width − 200;
    ball0.y = canvas.height − 200;
    ball0.vx = Math.random() * 10 − 5;
    ball0.vy = Math.random() * 10 − 5;

    ball1.mass = 1;
    ball1.x = 100;
    ball1.y = 100;
    ball1.vx = Math.random() * 10 − 5;
    ball1.vy = Math.random() * 10 − 5;

    function checkCollision (ball0, ball1) {
      //not defined yet...
    }

    function checkWalls (ball) {
      if (ball.x + ball.radius > canvas.width) {
        ball.x = canvas.width − ball.radius;
        ball.vx *= bounce;
      } else if (ball.x - ball.radius < 0) {
        ball.x = ball.radius;
        ball.vx *= bounce;
      }
      if (ball.y + ball.radius > canvas.height) {
        ball.y = canvas.height − ball.radius;
        ball.vy *= bounce;
      } else if (ball.y - ball.radius < 0) {
        ball.y = ball.radius;
        ball.vy *= bounce;
      }
    }

    (function drawFrame () {
      window.requestAnimationFrame(drawFrame, canvas);
      context.clearRect(0, 0, canvas.width, canvas.height);

      ball0.x += ball0.vx;
      ball0.y += ball0.vy;
      ball1.x += ball1.vx;
      ball1.y += ball1.vy;

      checkCollision(ball0, ball1);
      checkWalls(ball0);
      checkWalls(ball1);

      ball0.draw(context);
      ball1.draw(context);
    }());
  };
  </script>
 </body>
</html>

In this exercise, you set the boundaries, set some random velocities, throw in some mass, move each ball according to its velocity, and check the boundaries. You notice that the boundary-checking code has been moved into its own function, checkWalls, and we still need to write the function for collision-checking.

The beginning of the checkCollision function is simple; it's just a distance-based collision detection setup:

function checkCollision (ball0, ball1) {
  var dx = ball1.x − ball0.x,
      dy = ball1.y − ball0.y,
      dist = Math.sqrt(dx * dx + dy * dy);

  if (dist < ball0.radius + ball1.radius) {
    //collision handling code here
  }
}

The first thing that the collision-handling code needs to do is figure out the angle between the two balls, which you'll recall from the trigonometry in Chapter 3, you can do with Math.atan2(dy, dx). You'll store the cosine and sine calculations, as you'll be using them over and over:

//calculate angle, sine, and cosine
var angle = Math.atan2(dy, dx),
    sin = Math.sin(angle),
    cos = Math.cos(angle);

Then, you need to perform the coordinate rotation for the velocity and position of both balls. Let's call the rotated positions x0, y0, x1, and y1 and the rotated velocities vx0, vy0, vx1, and vy1.

Because you are use ball0 as the “pivot point,” its coordinates are 0, 0. That won't change even after rotation, so you can just write this:

//rotate ball0's position
var x0 = 0,
    y0 = 0;

Next, ball1's position is in relation to ball0's position. This corresponds to the distance values you've already figured out, dx and dy. So, you can rotate those to get ball1's rotated position:

//rotate ball1's position
var x1 = dx * cos + dy * sin,
    y1 = dy * cos - dx * sin;

Now, rotate all the velocities. You should see a pattern forming:

//rotate ball0's velocity
var vx0 = ball0.vx * cos + ball0.vy * sin,
    vy0 = ball0.vy * cos - ball0.vx * sin;

//rotate ball1's velocity
var vx1 = ball1.vx * cos + ball1.vy * sin,
    vy1 = ball1.vy * cos - ball1.vx * sin;

And here's all the rotation code in place:

function checkCollision (ball0, ball1) {
  var dx = ball1.x − ball0.x,
      dy = ball1.y − ball0.y,
      dist = Math.sqrt(dx * dx + dy * dy);

if (dist < ball0.radius + ball1.radius) {
  //calculate angle, sine, and cosine
  var angle = Math.atan2(dy, dx),
      sin = Math.sin(angle),
      cos = Math.cos(angle),

      //rotate ball0's position
      x0 = 0,
      y0 = 0,
      //rotate ball1's position
      x1 = dx * cos + dy * sin,
      y1 = dy * cos - dx * sin,

      //rotate ball0's velocity
      vx0 = ball0.vx * cos + ball0.vy * sin,
      vy0 = ball0.vy * cos - ball0.vx * sin,

      //rotate ball1's velocity
      vx1 = ball1.vx * cos + ball1.vy * sin,
      vy1 = ball1.vy * cos - ball1.vx * sin;
  }
}

Now perform a simple one-dimensional collision reaction with vx0 and ball0.mass and with vx1 and ball1.mass. From the one-dimensional example presented earlier, you had the following code:

var vxTotal = ball0.vx − ball1.vx;
ball0.vx = ((ball0.mass - ball1.mass) * ball0.vx + 2 * ball1.mass * ball1.vx) /
         (ball0.mass + ball1.mass);
ball1.vx = vxTotal + ball0.vx;

You can rewrite that as follows:

var vxTotal = vx0 − vx1;
vx0 = ((ball0.mass - ball1.mass) * vx0 + 2 * ball1.mass * vx1) /
       (ball0.mass + ball1.mass);
vx1 = vxTotal + vx0;

All you did was replace the ball0.vx and ball1.vx with the rotated versions vx0 and vx1. Let's plug the new version into the function definition:

function checkCollision (ball0, ball1) {
  var dx = ball1.x − ball0.x,
      dy = ball1.y − ball0.y,
      dist = Math.sqrt(dx * dx + dy * dy);

  if (dist < ball0.radius + ball1.radius) {
    //calculate angle, sine, and cosine
    var angle = Math.atan2(dy, dx),
        sin = Math.sin(angle),
        cos = Math.cos(angle),

        //rotate ball0's position
        x0 = 0,
        y0 = 0,

        //rotate ball1's position
        x1 = dx * cos + dy * sin,
        y1 = dy * cos - dx * sin,

        //rotate ball0's velocity
        vx0 = ball0.vx * cos + ball0.vy * sin,
        vy0 = ball0.vy * cos - ball0.vx * sin,
        //rotate ball1's velocity
        vx1 = ball1.vx * cos + ball1.vy * sin,
        vy1 = ball1.vy * cos - ball1.vx * sin,

        //collision reaction
        vxTotal = vx0 − vx1;
    vx0 = ((ball0.mass - ball1.mass) * vx0 + 2 * ball1.mass * vx1) /
          (ball0.mass + ball1.mass);
    vx1 = vxTotal + vx0;
    x0 += vx0;
    x1 += vx1;
  }
}

This code also adds the new x velocities to the x positions, to move them apart, as in the one-dimensional example.

Now that you have updated post-collision positions and velocities, rotate everything back. Start by getting the unrotated, final positions:

//rotate positions back
var x0Final = x0 * cos - y0 * sin,
    y0Final = y0 * cos + x0 * sin,
    x1Final = x1 * cos - y1 * sin,
    y1Final = y1 * cos + x1 * sin;

Remember to reverse the + and - in the rotation equations, as you are going in the other direction now. These “final” positions are actually not quite final. They are in relation to the pivot point of the system, which is ball0's original position. You need to add all of these to ball0's position to get the actual coordinate positions. Let's do ball1 first, so that it uses ball0's original position, not the updated one:

//adjust positions to actual screen positions
ball1.x = ball0.x + x1Final;
ball1.y = ball0.y + y1Final;
ball0.x = ball0.x + x0Final;
ball0.y = ball0.y + y0Final;

Last, but not least, rotate back the velocities. These can be applied directly to the balls' vx and vy properties:

//rotate velocities back
ball0.vx = vx0 * cos - vy0 * sin;
ball0.vy = vy0 * cos + vx0 * sin;
ball1.vx = vx1 * cos - vy1 * sin;
ball1.vy = vy1 * cos + vx1 * sin;

Finally, take a look at the entire completed function:

function checkCollision (ball0, ball1) {
  var dx = ball1.x − ball0.x,
      dy = ball1.y − ball0.y,
      dist = Math.sqrt(dx * dx + dy * dy);
  if (dist < ball0.radius + ball1.radius) {
    //calculate angle, sine, and cosine
    var angle = Math.atan2(dy, dx),
        sin = Math.sin(angle),
        cos = Math.cos(angle),

        //rotate ball0's position
        x0 = 0,
        y0 = 0,

        //rotate ball1's position
        x1 = dx * cos + dy * sin,
        y1 = dy * cos - dx * sin,

        //rotate ball0's velocity
        vx0 = ball0.vx * cos + ball0.vy * sin,
        vy0 = ball0.vy * cos - ball0.vx * sin,

        //rotate ball1's velocity
        vx1 = ball1.vx * cos + ball1.vy * sin,
        vy1 = ball1.vy * cos - ball1.vx * sin,

        //collision reaction
        vxTotal = vx0 − vx1;
    vx0 = ((ball0.mass - ball1.mass) * vx0 + 2 * ball1.mass * vx1) /
          (ball0.mass + ball1.mass);
    vx1 = vxTotal + vx0;
    x0 += vx0;
    x1 += vx1;

    //rotate positions back
    var x0Final = x0 * cos - y0 * sin,
        y0Final = y0 * cos + x0 * sin,
        x1Final = x1 * cos - y1 * sin,
        y1Final = y1 * cos + x1 * sin;

    //adjust positions to actual screen positions
    ball1.x = ball0.x + x1Final;
    ball1.y = ball0.y + y1Final;
    ball0.x = ball0.x + x0Final;
    ball0.y = ball0.y + y0Final;

    //rotate velocities back
    ball0.vx = vx0 * cos - vy0 * sin;
    ball0.vy = vy0 * cos + vx0 * sin;
    ball1.vx = vx1 * cos - vy1 * sin;
    ball1.vy = vy1 * cos + vx1 * sin;
  }
}

Play around with this example. Change the size of the Ball instances, the initial velocities, masses, and so on. Become convinced that it works well.

As for that checkCollision function, it's difficult. But if you read the comments, you see it's broken up into (relatively) simple chunks. We optimized it a bit to remove some duplication that enables us to reuse parts of code, which can make it easier to maintain. You can see the final result in 04-billiard-4.html:

<!doctype html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Billiard 4</title>
  <link rel="stylesheet" href="style.css">
 </head>
 <body>
  <canvas id="canvas" width="400" height="400"></canvas>
  <script src="utils.js"></script>
  <script src="ball.js"></script>
  <script>
  window.onload = function () {
    var canvas = document.getElementById('canvas'),
        context = canvas.getContext('2d'),
        ball0 = new Ball(80),
        ball1 = new Ball(40),
        bounce = -1.0;

    ball0.mass = 2;
    ball0.x = canvas.width − 200;
    ball0.y = canvas.height − 200;
    ball0.vx = Math.random() * 10 − 5;
    ball0.vy = Math.random() * 10 − 5;

    ball1.mass = 1;
    ball1.x = 100;
    ball1.y = 100;
    ball1.vx = Math.random() * 10 − 5;
    ball1.vy = Math.random() * 10 − 5;

    function rotate (x, y, sin, cos, reverse) {
      return {
        x: (reverse) ? (x * cos + y * sin) : (x * cos - y * sin),
        y: (reverse) ? (y * cos - x * sin) : (y * cos + x * sin)
      };
    }

    function checkCollision (ball0, ball1) {
      var dx = ball1.x − ball0.x,
          dy = ball1.y − ball0.y,
          dist = Math.sqrt(dx * dx + dy * dy);

      //collision handling code here
      if (dist < ball0.radius + ball1.radius) {
        //calculate angle, sine, and cosine
        var angle = Math.atan2(dy, dx),
            sin = Math.sin(angle),
            cos = Math.cos(angle),
            //rotate ball0's position
            pos0 = {x: 0, y: 0}, //point

            //rotate ball1's position
            pos1 = rotate(dx, dy, sin, cos, true),

            //rotate ball0's velocity
            vel0 = rotate(ball0.vx, ball0.vy, sin, cos, true),

            //rotate ball1's velocity
            vel1 = rotate(ball1.vx, ball1.vy, sin, cos, true),

            //collision reaction
            vxTotal = vel0.x − vel1.x;
        vel0.x = ((ball0.mass - ball1.mass) * vel0.x + 2 * ball1.mass * vel1.x) /
                 (ball0.mass + ball1.mass);
        vel1.x = vxTotal + vel0.x;

        //update position
        pos0.x += vel0.x;
        pos1.x += vel1.x;

        //rotate positions back
        var pos0F = rotate(pos0.x, pos0.y, sin, cos, false),
            pos1F = rotate(pos1.x, pos1.y, sin, cos, false);

        //adjust positions to actual screen positions
        ball1.x = ball0.x + pos1F.x;
        ball1.y = ball0.y + pos1F.y;
        ball0.x = ball0.x + pos0F.x;
        ball0.y = ball0.y + pos0F.y;

        //rotate velocities back
        var vel0F = rotate(vel0.x, vel0.y, sin, cos, false),
            vel1F = rotate(vel1.x, vel1.y, sin, cos, false);
        ball0.vx = vel0F.x;
        ball0.vy = vel0F.y;
        ball1.vx = vel1F.x;
        ball1.vy = vel1F.y;
      }
    }

    function checkWalls (ball) {
      if (ball.x + ball.radius > canvas.width) {
        ball.x = canvas.width − ball.radius;
        ball.vx *= bounce;
      } else if (ball.x - ball.radius < 0) {
        ball.x = ball.radius;
        ball.vx *= bounce;
      }
      if (ball.y + ball.radius > canvas.height) {
        ball.y = canvas.height − ball.radius;
        ball.vy *= bounce;
      } else if (ball.y - ball.radius < 0) {
        ball.y = ball.radius;
        ball.vy *= bounce;
      }
    }

    (function drawFrame () {
      window.requestAnimationFrame(drawFrame, canvas);
      context.clearRect(0, 0, canvas.width, canvas.height);

      ball0.x += ball0.vx;
      ball0.y += ball0.vy;
      ball1.x += ball1.vx;
      ball1.y += ball1.vy;

      checkCollision(ball0, ball1);
      checkWalls(ball0);
      checkWalls(ball1);

      ball0.draw(context);
      ball1.draw(context);
    }());
  };
  </script>
 </body>
</html>

In this example, we created a rotate function that accepts a few parameters and returns an object representing a rotated point with x and y properties. This version isn't quite as easy to read when you're learning the principles involved, but it results in less duplicated code.

Adding More Objects

Making two balls collide and react is no easy task, but you made it. Now let's add a few more colliding objects—say eight. It seems as if it's going to be four times more complex, but it's not. The function you have now checks two balls at a time, but that's all you need anyway. You add more objects, move them around, and check each one against all the others. You've already did that in the collision-detection examples in Chapter 9. All you need to do is to plug in the checkCollision function where you would normally do the collision detection.

For this example (05-multi-billiard-1.html), start with eight balls in an array. The checkCollision and checkWalls functions aren't listed in their entirety, but you can use the exact same ones from the previous example:

<!doctype html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Multi Billiard 1</title>
  <link rel="stylesheet" href="style.css">
 </head>
 <body>
  <canvas id="canvas" width="400" height="400"></canvas>
  <script src="utils.js"></script>
  <script src="ball.js"></script>
  <script>
  window.onload = function () {
    var canvas = document.getElementById('canvas'),
        context = canvas.getContext('2d'),
        balls = [],
        numBalls = 8,
        bounce = -1.0;

    for (var radius, ball, i = 0; i < numBalls; i++) {
      radius = Math.random() * 20 + 15;
      ball = new Ball(radius);
      ball.mass = radius;
      ball.x = i * 100;
      ball.y = i * 50;
      ball.vx = Math.random() * 10 − 5;
      ball.vy = Math.random() * 10 − 5;
      balls.push(ball);
    }

    function rotate (x, y, sin, cos, reverse) {
      return {
        x: (reverse) ? (x * cos + y * sin) : (x * cos - y * sin),
        y: (reverse) ? (y * cos - x * sin) : (y * cos + x * sin)
      };
    }

    function checkCollision (ball0, ball1) {
      //not listed, same as previous example...
    }

    function checkWalls (ball) {
      //not listed, same as previous example...
    }

    function move (ball) {
      ball.x += ball.vx;
      ball.y += ball.vy;
      checkWalls(ball);
    }

    function draw (ball) {
      ball.draw(context);
    }

    (function drawFrame () {
      window.requestAnimationFrame(drawFrame, canvas);
      context.clearRect(0, 0, canvas.width, canvas.height);

      balls.forEach(move);
      for (var ballA, i = 0, len = numBalls - 1; i < len; i++) {
        ballA = balls[i];
        for (var ballB, j = i + 1; j < numBalls; j++) {
          ballB = balls[j];
          checkCollision(ballA, ballB);
        }
      }
      balls.forEach(draw);
    }());
  };
  </script>
 </body>
</html>

The balls are spaced out to ensure that they do not touch each other to start with. If so, they can get stuck together.

The drawFrame function is surprisingly simple; it iterates over the balls three times: one for basic movement, one for collision detection, and of course, one to draw. The first forEach iteration passes each ball to the move function, which updates the ball's position, moving it and bouncing it off the walls. Then, the double-nested for loop performs the collision detection. Here, you get a reference to two balls at a time and pass them to the checkCollision function. The checkWalls and checkCollision functions are the same as before, so just add them to this file from the previous example.

To add more balls, just update the numBalls variable to the new total and make sure they do not touch at the start of the animation.

Solving a Potential Problem

One word of warning regarding the setup described in this chapter: It's still possible for a pair of objects to get stuck together. This usually happens in a crowded environment with many objects bouncing off each other, and is worse when they are moving at high speeds. You can also occasionally see this behavior if two or three balls collide in a corner.

If there are three balls on the screen—ball0, ball1, and ball2— and they all happen to be close together, here's what happens:

  • The code moves all three according to their velocities.
  • The code checks ball0 versus. ball1 and ball0 versus ball2. It finds no collision.
  • The code checks ball1 versus ball2. These two happen to be hitting, so it does all the calculations for their new velocities and updates their positions so that they are no longer touching. This inadvertently puts ball1 in contact with ball0. However, this particular combination has already been checked for a collision, so it now goes unnoticed.
  • On the next loop, the code again moves each according to its velocity. This potentially drives ball0 and ball1 even further together.
  • Now the code does notice that ball0 and ball1 hit. It calculates their new velocities and adds the new velocities to the positions, to move them apart. But, because they were already touching, this might not be enough to actually separate them. They become stuck.

Again, this mostly occurs when you have a lot of objects in a small space, moving at higher speeds. It also happens if the objects already touch at the start of the animation. As you might have already seen when testing the example, this issue crops up now and then, so it's good to know where the problem is. The exact point is in the checkCollision function; specifically, it occurs in the following lines:

//update position
pos0.x += vel0.x;
pos1.x += vel1.x;

This assumes that the collision occurs due to only the two ball's own velocities, and that adding back on the new velocities separates them. Most of the time, this is true. But the situations just described are exceptions. If you run into this problem, you need something more stringent to ensure the objects are definitely separated before moving on. Try using the following method:

//update position
var absV = Math.abs(vel0.x) + Math.abs(vel1.x),
    overlap = (ball0.radius + ball1.radius) - Math.abs(pos0.x − pos1.x);
pos0.x += vel0.x / absV * overlap;
pos1.x += vel1.x / absV * overlap;

This might not be the most mathematically accurate solution, but it seems to work well. It first determines the absolute velocity; this is the sum of the absolute values of both velocities. For instance, if one velocity is -5 and the other is 10, the absolute values are 5 and 10, and the total is 5 + 10, or 15.

Next, it determines how much the balls actually overlap. It does this by getting their total radii and subtracting their distance.

Then, it moves each ball a portion of the overlap, according to their percent of the absolute velocity. The result is that the balls should be exactly touching each other, with no overlap. It's a bit more complex than the earlier version, but it clears up the bugs.

In fact, in the next version, 06-multi-billiard-2.html, we created 15 balls, made them a bit larger, and randomly placed them on the canvas. The ones that overlap still freak out for a few frames, but eventually, because of this new code, they settle down. Here's the complete code listing for the example:

<!doctype html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Multi Billiard 2</title>
  <link rel="stylesheet" href="style.css">
 </head>
 <body>
  <canvas id="canvas" width="400" height="400"></canvas>
  <script src="utils.js"></script>
  <script src="ball.js"></script>
  <script>
  window.onload = function () {
    var canvas = document.getElementById('canvas'),
        context = canvas.getContext('2d'),
        balls = [],
        numBalls = 15,
        bounce = -1.0;

    for (var radius, ball, i = 0; i < numBalls; i++) {
      radius = Math.random() * 20 + 15;
      ball = new Ball(radius, Math.random() * 0xffffff);
      ball.mass = radius;
      ball.x = Math.random() * canvas.width;
      ball.y = Math.random() * canvas.height;
      ball.vx = Math.random() * 10 - 5;
      ball.vy = Math.random() * 10 - 5;
      balls.push(ball);
    }

    function rotate (x, y, sin, cos, reverse) {
      return {
        x: (reverse) ? (x * cos + y * sin) : (x * cos - y * sin),
        y: (reverse) ? (y * cos - x * sin) : (y * cos + x * sin)
      };
    }

    function checkCollision (ball0, ball1) {
      var dx = ball1.x - ball0.x,
          dy = ball1.y - ball0.y,
          dist = Math.sqrt(dx * dx + dy * dy);

      //collision handling code here
      if (dist < ball0.radius + ball1.radius) {
        //calculate angle, sine, and cosine
        var angle = Math.atan2(dy, dx),
            sin = Math.sin(angle),
            cos = Math.cos(angle),

            //rotate ball0's position
            pos0 = {x: 0, y: 0}, //point

            //rotate ball1's position
            pos1 = rotate(dx, dy, sin, cos, true),

            //rotate ball0's velocity
            vel0 = rotate(ball0.vx, ball0.vy, sin, cos, true),

            //rotate ball1's velocity
            vel1 = rotate(ball1.vx, ball1.vy, sin, cos, true),

            //collision reaction
            vxTotal = vel0.x - vel1.x;
        vel0.x = ((ball0.mass - ball1.mass) * vel0.x + 2 * ball1.mass * vel1.x) /
                 (ball0.mass + ball1.mass);
        vel1.x = vxTotal + vel0.x;

        //update position - to avoid objects becoming stuck together
        var absV = Math.abs(vel0.x) + Math.abs(vel1.x),
            overlap = (ball0.radius + ball1.radius) - Math.abs(pos0.x - pos1.x);
        pos0.x += vel0.x / absV * overlap;
        pos1.x += vel1.x / absV * overlap;

        //rotate positions back
        var pos0F = rotate(pos0.x, pos0.y, sin, cos, false),
            pos1F = rotate(pos1.x, pos1.y, sin, cos, false);

        //adjust positions to actual screen positions
        ball1.x = ball0.x + pos1F.x;
        ball1.y = ball0.y + pos1F.y;
        ball0.x = ball0.x + pos0F.x;
        ball0.y = ball0.y + pos0F.y;

        //rotate velocities back
        var vel0F = rotate(vel0.x, vel0.y, sin, cos, false),
            vel1F = rotate(vel1.x, vel1.y, sin, cos, false);
        ball0.vx = vel0F.x;
        ball0.vy = vel0F.y;
        ball1.vx = vel1F.x;
        ball1.vy = vel1F.y;
      }
    }

    function checkWalls (ball) {
      if (ball.x + ball.radius > canvas.width) {
        ball.x = canvas.width - ball.radius;
        ball.vx *= bounce;
      } else if (ball.x - ball.radius < 0) {
        ball.x = ball.radius;
        ball.vx *= bounce;
      }
      if (ball.y + ball.radius > canvas.height) {
        ball.y = canvas.height - ball.radius;
        ball.vy *= bounce;
      } else if (ball.y - ball.radius < 0) {
        ball.y = ball.radius;
        ball.vy *= bounce;
      }
    }

    function move (ball) {
      ball.x += ball.vx;
      ball.y += ball.vy;
      checkWalls(ball);
    }

    function draw (ball) {
      ball.draw(context);
    }

    (function drawFrame () {
      window.requestAnimationFrame(drawFrame, canvas);
      context.clearRect(0, 0, canvas.width, canvas.height);

      balls.forEach(move);
      for (var ballA, i = 0, len = numBalls - 1; i < len; i++) {
        ballA = balls[i];
        for (var ballB, j = i + 1; j < numBalls; j++) {
          ballB = balls[j];
          checkCollision(ballA, ballB);
        }
      }
      balls.forEach(draw);
    }());
  };
  </script>
 </body>
</html>

Of course, you're free to investigate your own solutions to the problem, and if you come up with something that is simpler, more efficient, and more accurate, please share!

Important Formulas in this Chapter

The important formula in this chapter is the one for conservation of momentum.

Conservation of Momentum, in Straight Mathematical Terms

          (m0 − m1) × v0 + 2 × m1 × v1
v0Final = ----------------------------
                 m0 + m1

          (m1 − m0) × v1 + 2 × m0 × v0
v1Final = ----------------------------
                 m0 + m1

Conservation of Momentum in JavaScript, with a Shortcut

var vxTotal = vx0 − vx1;
vx0 = ((ball0.mass - ball1.mass) * vx0 + 2 * ball1.mass * vx1) /
      (ball0.mass + ball1.mass);
vx1 = vxTotal + vx0;

Summary

Congratulations! You've made it through the heaviest math in the book and you now have in your repertoire the methods for handling accurate collision reactions. One thing we've ignored in these examples, just to keep them simple, is the concept of friction. You can try to add that into the system because you certainly know enough at this point to do so. Be sure to check out Chapter 19, where you'll see a little trick to use in the case that both objects have the same mass.

In the next chapter, we tone things down a bit and look into particle attraction, though adding in some billiard ball physics to the examples there would be quite fitting.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.239.41