Modeling Physics in Javascript: Gravity and Drag
This is the first post in the Modeling Physics in Javascript series. As such, we'll need to cover some introductory material. There are some of you who have real training in physics and calculus. You may be tempted to scream "that's not the whole story" at me when I explain some concepts, and my response to you is this: I wish I could cover all the amazing parts of physics and calculus in one blog post, but I can't. I'll therefore cover only the bits I need. Hopefully, the people reading this will accidentally learn physics while trying to pick up some cool JS tricks.
Yes, there's a JSFiddle to play with at the bottom of the page!
The Problem
Model a bouncy ball, under the influence of gravity, which can bounce off of horizontal or vertical walls. Oh, and it experiences air drag.Not too shabby for day 1! We'll first talk about Newton's 2nd Law, forces, gravity, and a touch of calculus before we can start coding.
Newton's 2nd Law
Newton's 2nd Law of Motion basically is physics. It describes how forces interact with macroscopic objects. If you've ever seen a ball fly through the air, or a car moving, or something rolling down a hill, or if you've ever felt anything, you know (indirectly) about Newton's 2nd Law.Newton's 2nd Law looks like this: F = ma.
Well, not quite. It looks more like this: Fnet = ma.
As you can imagine, physics has a manner of starting simple and then getting very complicated very quickly. The subscript "net" means that the "F" doesn't just refer to a single force, but the sum total of all forces on the object. The bold "F" and "a" mean that those variables (force and acceleration) are vectors -- meaning that they exist separately in 3 different directions (X, Y, and Z).
But that's not too bad. If you don't want to do vector math, you can simply rewrite Fnet = ma as the following three separate equations:
- Fx, net = max
- Fy, net = may
- Fz, net = maz
Let's just jump into a simple example. You have a ball with a mass of 2 kg, and there are three forces acting on it:
- 2 Newtons pulling right
- 4 N pulling left
- 1 N pulling down
And since there's only one force acting in the Y direction, the Fy, net is just -1 N. (Let's say "up" is positive and "down" is negative -- but only for now. We'll switch that up later. In general, you use positive and negative signs in a way that makes sense for your specific problem.)
Finally, since our ball weighs 2 kg, we can substitute our numbers into F = ma and get the following two equations:
- -2 N = 2 kg * ax
- -1 N = 2 kg * ay
- ax = -2 N / 2 kg
- ay = -1 N / 2 kg
- ax = -1 m / s2
- ay = -0.5 m / s2
So we had a situation where we know the mass of an object and the forces acting on it. We added up the forces (assigning negative signs to "left" and "down", causing those forces to subtract instead), and rearranged to solve for acceleration -- and we did this for both the X and the Y directions separately. In this example, the ball is accelerating down and to the left, but it's accelerating more to the left than it is downwards.
(More advanced: sometimes you don't know the X and Y components of a force. It's possible that you only know about one force that's angled at, say, 30 degrees up from right. In that case, you can use cosine and sine to figure out the X and Y pieces of that force respectively. We'll do that in a future article.)
Gravity
Gravity is one of the universal fundamental interactions, along with the electromagnetic, strong, and weak forces. Gravity causes a force to act on every single object from every other single object, but fortunately everything works out to simplify nicely if you're on the surface of a planet. On Earth, for instance, we don't even need to worry about the force that gravity causes, we can just look at the end result: every single object on Earth experiences an additional downward acceleration of 9.8 m / s2. That simple.If our ball from above (that we already solved for) is on Earth and experiencing gravity, then we just need to modify the ay equation slightly. We'll take the downward acceleration that the force is causing and combine it with the downward acceleration that gravity causes:
ay = -0.5 m / s2 - 9.8 m / s2
That leaves us with an acceleration of
ay = -10.3 m / s2
As you can see, the downward force combined with the acceleration from gravity shoots the ball downward even faster than either gravity or the force alone could.
So, gravity is simple (if you're on a planet). All you do is modify the ay acceleration to factor in the downward pull of gravity.
In a future article we'll look at how to model gravity if you're not on a planet (perhaps you are a planet), but that doesn't come until later...
Aerodynamic Drag
Our ball experiences gravity, but as I mentioned, we don't need to model that as a force; we just plug it directly into the acceleration result. Drag, however, is a force, and we'll have to model it.
The equation (one of the equations) for aerodynamic drag looks like this:
FD = -0.5 * CD * A * ρ * v2
It looks complicated, but when we break it down, it's pretty simple. First off, notice the bold characters. Like F = ma above, a bold variable means it's actually a vector -- so right off the bat we can split this into two equations:
- FD, x = -0.5 * CD * A * ρ * vx2
- FD, y = -0.5 * CD * A * ρ * vy2
And then we look at each one of those terms above:
- CD is the "coefficient of drag", which is influenced by the shape of the object (and a little bit by its material). For a ball, this is 0.47, and is a dimensionless quantity.
- A is the frontal area or frontal projection of the object. If you look at a silhouette of the object from the front, this is the area of that shape. For a ball, the frontal area is just the area of a circle, or π r2.
- ρ (Greek letter rho) is the density of the fluid the ball is in. If our ball's in air, this value is 1.22 (kg / m3)
- Velocity squared -- since we're looking at this in two directions separately, we use the X velocity and the Y velocity respectively.
- Note the -0.5 at the beginning. The negative sign, with the fact that the equation uses velocity, indicates that this force pushes in the opposite direction the ball is moving at all times. Because the velocity is squared it'll always be positive, which means the whole equation will always be negative, ie, opposite the velocity.
A Touch of Calculus
Calculus, like physics, is amazing and has a wonderful depth that I can't do justice in a blog post.
The "derivative" in calculus describes how something changes as something else is changing; often this will be called the "rate of change". When you drive a car at 30 MPH, your position is changing by 30 miles every hour. Your position changes as time changes. It can then be said that velocity is the "derivative of position with respect to time" or simply the "time derivative of position".
Then we can think about what happens when you speed up or slow down (accelerate). You might change your velocity by 5 MPH per hour (MPHPH?). In that sense, your velocity is changing with time, and you can say that acceleration is the time derivative of velocity.
So it starts with position. The derivative of position is velocity. And the derivative of velocity is acceleration. (The derivative of acceleration is called "jerk", and the derivative of jerk is called "jounce".)
Why is this relevant? Because if you know the acceleration of something (5 meters per second per second), and if you know how fast it's going when you start looking at it (let's say it's not moving at all), you can figure out where it will be at every moment in the future.
An example: your ball is accelerating at 2 m / s2 (meters per second per second). Let's say it's not moving at all when you start looking at it, and that the starting point is called x = 0;
Time | Accel | Velocity | Position |
---|---|---|---|
0 s | 2 m / s2 | 0 m / s | 0 m |
1 s | 2 m / s2 | 2 m / s | 0 m |
2 s | 2 m / s2 | 4 m / s | 2 m |
3 s | 2 m / s2 | 6 m / s | 6 m |
4 s | 2 m / s2 | 8 m / s | 12 m |
5 s | 2 m / s2 | 10 m / s | 20 m |
6 s | 2 m / s2 | 12 m / s | 30 m |
And so on. This is the approach we'll use when solving for the motion of our ball, except we'll do it not once per second but 40 times per second. All we're doing is using our knowledge of the forces to figure out the acceleration at every frame. Then we use the acceleration and current velocity to figure out the new velocity. And then we use the velocity and last position to find the current position.
Writing the Code
Time to dive in. I won't reproduce all the code in snippets, because there's some stuff that has nothing to do with physics. You'll be able to see the full script at the bottom of the page in the JSFiddle.
var frameRate = 1/40; // Seconds
var frameDelay = frameRate * 1000; // ms
var loopTimer = false;
var ball = {
position: {x: width/2, y: 0},
velocity: {x: 10, y: 0},
mass: 0.1, //kg
radius: 15, // 1px = 1cm
restitution: -0.7
};
var Cd = 0.47; // Dimensionless
var rho = 1.22; // kg / m^3
var A = Math.PI * ball.radius * ball.radius / (10000);
var ag = 9.81;
We set up the frame rate and plug in some physics values. We also create a ball object that stores the ball's position, velocity, mass, radius, and a number called "restitution". You'll see later that this value determines how bouncy the ball is.
Notice here that we've set the ball to be moving at the start of the simulation.
The best part of programming real physics is the fact that you can look up the density of water and replace the value for rho, and the ball will actually behave as if it's in water! If you program the physics correctly, then all you have to do is change the constants and the rest follows. Want the ball to be on the moon? Just change the acceleration due to gravity. Want the ball to swim through water? Just change the density rho. Want a light, floaty beach ball? Lower the mass and increase the radius.
Please play with these values in the JSFiddle below. Change rho and the radius and the mass, and see how physics affects the simulation!
var setup = function() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'red';
ctx.strokeStyle = '#000000';
loopTimer = setInterval(loop, frameDelay);
}
The setup function initializes the canvas and sets up a loop that executes every frameDelay milliseconds. We'll do all the physics and animation in the loop function.
In the loop:
// Do physics
// Drag force: Fd = -1/2 * Cd * A * rho * v * v
var Fx = -0.5 * Cd * A * rho * ball.velocity.x * ball.velocity.x * ball.velocity.x / Math.abs(ball.velocity.x);
var Fy = -0.5 * Cd * A * rho * ball.velocity.y * ball.velocity.y * ball.velocity.y / Math.abs(ball.velocity.y);
Fx = (isNaN(Fx) ? 0 : Fx);
Fy = (isNaN(Fy) ? 0 : Fy);
// Calculate acceleration ( F = ma )
var ax = Fx / ball.mass;
var ay = ag + (Fy / ball.mass);
// Integrate to get velocity
ball.velocity.x += ax*frameRate;
ball.velocity.y += ay*frameRate;
// Integrate to get position
ball.position.x += ball.velocity.x*frameRate*100;
ball.position.y += ball.velocity.y*frameRate*100;
First off, we calculate the drag forces on the ball. There's a little trick I used to get the direction of the velocity. Instead of using "if" statements to see if the velocity is positive or negative, I just do:
ball.velocity.y / Math.abs(ball.velocity.y)
at the end of the drag force statements. Dividing a number by its absolute value just leaves the sign. Other than that, the drag force lines are pretty straightforward. We're just calculating the forces.
After that, we calculate acceleration. Notice that the statement for "ay" is different from "ax". Gravity only works in the Y direction, so we add that in here. Also notice that in this problem, "downwards" is positive, unlike the example at the top of the page.
After that, we update the ball velocities with the acceleration times the frame rate. The reason we multiply by the frame rate is so: the acceleration is given in "meters per second-squared". But we're calling this loop 40 times a second (not once a second), so we need to divide by 40 (or multiply by 1/40 in this case) to adjust for the frame rate.
Finally, update the ball positions in a similar fashion. In this case we're also multiplying by 100. If you look at the ball object definition way above you'll see I commented that "1px = 1cm", so this *100 is just an adjustment to make everything work out in meters.
Then we handle collisions with the walls:
// Handle collisions
if (ball.position.y > height - ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = height - ball.radius;
}
if (ball.position.x > width - ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = width - ball.radius;
}
if (ball.position.x < ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = ball.radius;
}
We're just checking to see if the ball has ended up past the wall in this frame. If it has, then we multiply the velocity in that direction by the restitution coefficient from above. Since that number is always negative, it'll make the ball reverse direction. If you set the restitution to -1, it'll be perfectly bouncy, meaning it'll bounce up as high as it started falling from. If you set the restitution to 0, it'll flop dead on the ground with no bounce whatsoever. And if you set it to something like -2, it'll bounce even higher than it started. Play with it!
We also modify the position of the ball to just kiss the wall -- this way the ball won't get stuck "in" the wall. Keep in mind that the ball is moving in discrete motions, and so when it collides with the wall it's actually overlapping slightly.
Finally, since we want to be able to control the ball with the mouse, we'll add some handlers (not all code shown here):
var mouseDown = function(e) {
if (e.which == 1) {
getMousePosition(e);
mouse.isDown = true;
ball.position.x = mouse.x;
ball.position.y = mouse.y;
}
}
var mouseUp = function(e) {
if (e.which == 1) {
mouse.isDown = false;
ball.velocity.y = (ball.position.y - mouse.y) / 10;
ball.velocity.x = (ball.position.x - mouse.x) / 10;
}
}
If you click and drag the mouse, and let go, you'll create a kind of slingshot effect. This is achieved not by applying a force to the ball (which you could do), but rather by overriding the velocity of the ball based on how far you pulled the mouse. I like this approach better because it's easier to apply than a force. If you were to use a force to move the ball with the mouse, you'd have to apply the force over a period of time. The "initial velocity" technique above just lets you un-naturally override the velocity in an instant and let physics figure everything out again.
I hope you learned something! Please fork and play with the code in the fiddle below. Click and drag the mouse to slingshot the ball.
Note that certain configurations of variables will cause the simulation to become unstable. Try setting rho = 1000 but leaving the mass at 0.1. The ball should spaz out and blink around the screen. This isn't a problem with the physics, it's just that we're not running at a high enough frame rate for the very large drag forces. To solve that problem we would have to increase the frame rate to make the simulation stable again. Or we could use a different solver (we're using Euler's method here, a first-degree ODE solver) -- but we won't talk about solvers for a few weeks.