Using Damped Springs for animations

Humpf is a TypeScript and JavaScript library to get the position and velocity of a damped spring as a continuous function of time.

A what of what ?

The aim of this article is to explain the different terms in the sentence above to give you a better understanding of the library and how to use it.

Let's start by the most important part: what is a damped spring ?

What is a damped spring ?

A Damped Spring is an equation that simulate the movement of a spring where the amplitude of the spring (the bounce) is damped over time.

Here is an example of this movement

Before diving into how to configure and use such movement, let's see what a "continuous function of time" means.

Step-by-step vs Continuous function of time

There are two main ways to animate motion:

  • Using a step-by-step aproach
  • Using a continuous function of time

The step-by-step aproach

To animate the position of an object with a step-by-step approach, you first define the initial value, then on every frame you update this value:

let position = 0;
// on each frame move by 5 pixels
position = position + 5;

This aproach is quite powerful and can be used to simulate physics:

let acceleration = 0.1;
let velocity = -4;
let position = 0;
// on each frame
velocity = velocity + acceleration;
position = position + velocity;

This method is used by many frameworks and libraries out there but it's not the method used by Humpf.

With Humpf your motion is represented as a continuous function of time, let's see what it means.

Continuous function of time

A continuous function is a function where the output is continuous meaning it has no abrupt changes or hole.

In the case of Humpf, there are actually two outputs: position and velocity but we will only focus on position for now.

To illustrate this, here is a very simple continuous function of time that just return the time scaled by a factor y:

const y = 30;
function position(t: number): number {
return t * y;
}

As you can see, this motion is pretty boring and does not feal very realistic...

To make things a bit more interesting we can use easing functions.

Easing functions

Easing functions are small functions that take a parameter t (for time) and return a position. You've probably already seen or even used one like easeOutCubic, easeInOutQuad, or easeOutBounce

Here is what easeInOutCubic look like:

function position(t: number): number {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

In easing functions, time is expected to be between 0 and 1, same for the output.

As you can see, this motion is much nicer than the linear one !

But working with easing functions can be a bit tricky, you basically have to chose the one that best fit your need and roll with it.

What if instead of an arbitrary math function we could use a physic based equation to animate our ball ? Well that's where Damped Springs come into play !

Damped Springs

At this point you might be wondering:

How a spring could correctly model motion on a screen ? Most of the time I just want to move things from one position to another, not bounce around.

This is a good point, in fact simple spring equation would be pretty useless but here we are talking about damped springs and that make all the difference.

Damping Ratio

Unlike a simple spring, a damped spring depend on a damping ratio. This number express how hard it is for the object to move.

Now watch what how the curve evolve when the damping ratio approach 1

Damping Ratio: 0.20

As you can see, it looks less and less like a spring and more like what we had with the easeInOutCubic. The main difference is that this time the curve is define by our spring equation !

This is nice but a bit slow. Can we make it faster ?

Yes, and for that we need a new parameter: Angular Frequency

Angular Frequency

The angular frenquency is the frequency at which the spring goes back and forth. The easiest way to visualize this is with a spring with a damping ratio of 0.

Angular Frequency: 1.0

Note that here, because the damping ratio is 0, the spring will bounce forever.

Now let's combine the two !

Angular Frequency & Damping Ratio

Angular Frenquency: 1.00

Damping Ratio: 1.00

Updating a Spring

So far we have seen how a damped spring motion can be used to animate a nice realistic motion but in real cases motions are rarely just from A to B.

Take the example of a menu the user can show / hide with a button.

Let's animate this using a spring ! When it's closed we animate from closed to open, when it's open we animate from open to close.

Open in CodeSandbox

Now if you try to rapidly click multiple time on the button you will see a problem: we did not handle the case where the user click while the menu is moving.

We can fix this by using the current position of the spring when the user clicks:

// when the user click on the button
// we update the equilibrium but keep the current position
spring = Spring({
timeStart: Date.now(),
position: spring(Date.now()).position,
equilibrium: menuHiddenOffset
});

Take a look at the result:

Open in CodeSandbox

Smooth transitions with velocity

There is one last thing we have to take into account when updating a spring: velocity !

In the following example, we want the ball to be "attracted" to the cursor. To do so, everytime the cursor moves we update the spring's equilibrium to be the new position as well as position to make sure the ball starts from where it is.

At first look it might look like it prefectly work but look what happend when you move the cursor. Do you see how the ball seems to be stuck while the cursor is moving ?

This is because everytime we update the spring, we also reset the velocity (the speed) of the ball because the initial velocity of a spring is 0 by default.

But in the real world, the velocity of an object can't suddently go from a value to 0

To fix this we need to preserve velocity when whe update the spring:

// when the user click on the button
// we update the equilibrium but keep the current position and velocity
spring = Spring({
timeStart: Date.now(),
position: spring(Date.now()).position,
velocity: spring(Date.now()).velocity,
equilibrium: mouseEvent.clientX
});

And here the final result:

That's it

Congrats ! You now know how springs works. To go further take a look at the API.