A demo animation of balls and springs showing realistic behavior
Before we dive into the code, take a look at the simulation running live in your browser. Try clicking and dragging near one of the green particles with your mouse.
The complete code for this browser-based animation is available in this GitHub repo. There are only two files,
chain.js, and they have no external dependencies. You can download them to your own computer, tinker with the the code, and see the results in your browser.
The Physical Model
In this sample program, the simulated world is two-dimensional. There are three kinds of entities in this world: balls, anchors, and springs.
A ball is a particle that has a mass m, position (x, y), and velocity (vx, vy). The mass is constant, but the position and velocity vectors are updated during each animation frame based on the total forces acting on the ball. The balls are depicted as green circles. There is also a gravitational acceleration g = 9.8 m/s² that pulls the balls toward the bottom of the screen.
An anchor is a special kind of ball that does not move. (Actually, you can move it around with the mouse, just like you can with any other ball. But other than that, an anchor stays put.) There is only one anchor in the simulation as presented, but you can change that by editing the code in
chain.js. Anchors look like red squares.
A spring is an elastic line segment that attaches to two balls at its endpoints. Every spring is connected to exactly two balls, but a ball may be connected to any number of springs. Each spring has two properties: a rest length and a spring constant. These are both positive real numbers that define how much force the spring applies to the two balls connected to its ends.
The rest length L is the length of a spring when it is neither stretched nor compressed. At this length, it will impart zero force to the balls it is attached to. L is expressed in meters.
The spring constant K tells how “strong” the spring is, meaning how much it resists being stretched or compressed away from its rest length. K is expressed in units of newtons per meter (N/m).
Now that we know the players, let’s explore how the simulator calculates their behavior in real time.
Animation in a Canvas
The animation itself occurs by drawing one animation frame at a time via the following steps:
- Perform physics calculations to update the model state.
- Erase the contents of the canvas.
- Draw all the balls and springs.
- Schedule another animation frame with a 10-millisecond timer callback.
Calculating the Forces
chain.js you will find a class
Simulation. Inside it is a member function
Update. Its parameter
dt, expressed in seconds, represents a small increment of time by which the simulation’s state is to be updated. Before reading the code in the
Update function, it will help to have a mental picture of the forces acting on a ball.
Every spring acts on both of the balls connected to it. Here, spring Sa acts on balls B1 and B2 with equal and opposite force Fa. Likewise, Sb acts on B1 and B3 with equal and opposite force Fb.
Let’s assume that the spring Sa at the depicted moment is stretched longer than its rest length. This is why the arrow for the force Fa is pointing away from the ball B1. At the same time, the same force Fa is pulling B2 in the opposite direction, toward B1.
On the other hand, suppose spring Sb is compressed shorter than its rest length. Therefore, Sb is trying to push B1 and B3 apart. That is why the force vector Fb has an arrow pointing toward B1.
There is also the ball’s weight mg pulling it downward, where m is the ball’s mass in kilograms, and g = 9.8 m/s² is acceleration due to gravity.
Update adds up all these contributory force vectors for every ball.
Update function calls the
AddForce function inside the
Spring class to add the forces of each spring in the simulation onto their connected balls. Here is that function:
Each spring has a force calculated by multiplying the spring constant by the the difference between the spring’s current length and its rest length. The force acts along a straight line between the two balls it connects.
Updating Position and Velocity Vectors
After tallying the forces acting on all the balls,
Update goes back and updates the positions and velocities for each ball based on those forces. The kinetic behavior of the balls is governed by the familiar Newtonian physics equation F=ma. This says that the total force vector F on a mass m causes the mass to accelerate with a vector a.
The simulation proceeds in discrete time steps Δt. In physics, acceleration is defined as the instantaneous rate of change of velocity with respect to time, or a=dv/dt in differential calculus notation. We approximate acceleration as a ratio of finite changes a=Δv/Δt. Putting this approximation together with F=ma, we can solve for the change in a ball’s velocity vector as Δv=(F/m)Δt. We divide the total force F acting on the ball in the x- and y-directions by the ball’s mass m, and multiply by the time increment Δt, to get the change in velocity in the x- and y-directions.
A Glitch in the Matrix
But this approximation for Δv causes a problem in the simulation. If you use that naive formula, you will discover that the simulation becomes unstable. Unlike the infinitely small time increment dt, a finite increment Δt causes slight imperfections in the total energy of the system (kinetic energy plus potential energy). This can cause the balls to move faster and faster over time and explode off the screen.
To temper the behavior, we multiply all the speed vectors by a friction factor that is slightly smaller than 1. Over time this dampens the movement of the balls and keeps the simulation stable. You can adjust this friction behavior by editing the value of the constant
Update function also updates the position of each ball by multiplying the velocity vector by the time Δt to figure out how much the position of the ball changed.
In addition to the friction trick mentioned above, the simulation stability is enhanced by performing 1000 incremental simulation updates for each displayed animation frame. This makes the Δt value 1/1000 the size of the time interval between screen updates. A smaller value of Δt makes the simulation more accurate.
In practice, my performance testing with this code revealed the actual bottleneck is not in the physics calculations at all. The most expensive part of the code is erasing the canvas each time the
Render function is called:
context.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
So if you are itching to make this code faster, I would recommend starting there!
I hope you enjoy tinkering with this code. I’m certain there are numerical simulation gurus out there who can tell me ways it can be improved, especially in better approximating the physics behavior with finite time increments. I’d love to hear from you! Feel free to leave comments so we can all learn.