Spring Equations

Solving spring physics for animation, once and for all

Spring physics is the best way to animate things in UI (with certain exceptions). Animation feels natural, can be made interactive very easily, and only needs two parameters to create a wide variety of effects.

However, actually implementing spring animation can be kind of a hassle—especially regarding the timestep, picking coefficients, and so on. So, here’s a guide!

Physical Background

The basic concept behind spring physics is that of.... well, springs. Or if you want to sound fancy, “damped harmonic oscillators.”

Try dragging it!

A spring, in its essence, may be described by the following equation:

$$F = -kx - cv$$

Basically, this means that the force being exerted on the mass at any given time is proportional to the distance ($x$) to its target position, plus some friction/inertia (the $cv$ part).

In fact, since the force scales with the distance, this means that no matter where the spring starts, it will always take the same amount of time to get to its resting position.

Though the “archetypical” spring may be very… springy, for UI animation you’d often want something closer to a critically damped spring:

Try dragging this one as well!

The higher the $k$ coefficient is, the faster the spring will bounce, and the higher the $c$ coefficient is, the faster it will come to a rest ($c = 0$ will make it bounce forever).

A critically damped spring is a spring where the two coefficients $k$ and $c$ are balanced in such a way that it doesn’t bounce at all, but instead just moves smoothly to its destination.

If you increase $c$ a lot, you can also get an overdamped spring, which essentially just looks like a critically damped spring, but is slower.

Basic Implementation

So, actually, springs aren’t very complicated at all. You just take your couple of variables and update them every frame according to the equation as outlined above.

Though, you might also want to introduce a target variable to adjust the spring’s resting position, so that it doesn’t only target 0.

struct Spring {
    target: f64,
    value: f64,
    velocity: f64,
    k: f64,
    c: f64,
}

impl Spring {
    /// Updates the spring with delta time dt.
    fn update(&mut self, dt: f64) {
        // coefficients
        let k = self.k;
        let c = self.c;

        // displacement
        let x = self.value - self.target;
        // velocity
        let v = self.velocity;

        // apply the equation to get force
        let force = -k * x - c * v;
        
        // apply force to velocity!
        // because we assumed mass = 1, then
        // F = a = dv/dt, which results in
        // dv = a * dt = F * dt
        self.velocity += force * dt;
        // apply velocity to value
        self.value += self.velocity * dt;

        // NOTE: it’s very important that velocity is changed first!
        // Otherwise you’ll get weird issues where the spring suddenly
        // goes off to infinity.
    }
}
And... this will work fairly well for most basic cases!
let spring = Spring {
    // move from 0 to 1
    value: 0.,
    target: 1.,

    // initial velocity is 0
    velocity: 0.,

    // some example coefficients for a sort-of-critically-damped spring
    k: 113.,
    c: 21.,
};

// ...

fn update_animation(spring: &mut Spring, dt: f64) {
    spring.update(dt);

    draw_something(spring.value);
}

Note about animation duration

While some animation libraries will, somehow, pretend that spring animation has a fixed duration, this doesn’t really make sense because the spring never actually stops moving. Generally, if you need the animation to take a fixed amount of time to play and then stop completely, you might be better of using some other kind of easing.

What I’d recommend for springs, however, is to have some sort of threshold $\varepsilon$, and stop animating as soon as $|x| + |v| < \varepsilon$. When the animation gets into subpixel territory, no one will be able to tell the difference anyway.

If this is all you needed, then, well, I guess you’re done!!

More Ergonomic Controls

Though, let’s be honest. These “$k$” and “$c$” coefficients just really aren’t doing it. It’s really hard to figure out what results you’ll get from their values without having to just experiment a lot.

Fortunately, using $(k, c)$ is not the only way of describing springs. Another way is to use $\zeta$ and $T$.1

$\zeta$ is the damping ratio of the spring. $\zeta = 0$ will cause the spring to bounce around forever without losing any energy, while $\zeta = 1$ will create a critically damped spring. Basically, it can be thought of the “bounciness” or “smoothness.”

$T$ is the period of the spring. This is the time (e.g. in seconds) that it would take to complete one oscillation if it were to bounce around forever. However, most springs don’t bounce around forever, so it can be thought of as a sort of “animation duration,” though the animation will always be a bit longer than $T$.

Together, these two variables make controlling the spring much easier. Want kinda-bouncy, fast animation? $\zeta = 0.6, T = 0.3\,\text{s}$. Want slow and smooth animation? $\zeta = 1, T = 2\,\text{s}$.

I’ve got an online tool you can use to convert between $(\zeta, T)$ and $(k, c)$, but implementing it directly in code will be much more convenient in the long run.

$$k = \left(\frac{2\pi}{T}\right)^2 \Longleftrightarrow T = \frac{2\pi}{\sqrt{k}}$$
$$c = 2\zeta\sqrt{k} = \frac{4\pi\zeta}{T} \Longleftrightarrow \zeta = \frac{c}{2\sqrt{k}}$$
impl Spring {
    fn new(damping_ratio: f64, period: f64) -> Spring {
        let sqrt_k = 2. * f64::consts::PI / period;

        Spring {
            value: 0.,
            target: 0.,
            velocity: 0.,

            k: sqrt_k * sqrt_k,
            c: 2. * damping_ratio * sqrt_k,
        }
    }
}

Problems with Iteration

While this solution works pretty well for most situations, sometimes, it kind of... stops making sense:

Conservation of energy is a hoax, apparently

Especially if your spring is being very bouncy and your timestep $\Delta t$ is large or inconsistent, the spring might decide to leave the mortal realm.

This is quite a contrived example, but I’ve seen such issues crop up in the wild—especially with interactive springs. It’s certainly always a good idea to fix your timestep.2

In fact, this whole problem is an aliasing issue. Since this approach samples and updates the spring at discrete time intervals, these issues are unavoidable in the general case. You might also notice that the solution actually seems to converge to some sort of ground truth as the timestep gets smaller and smaller:

While the accuracy at 60 updates per second is pretty decent in Example 1, it’s really bad in Example 2, and lower framerates certainly seem to make it diverge quite a bit.

We could try adjusting the timestep to be a really small value and iterating several times per frame instead of only once, but the fact that it converges hints at a better solution.

This whole “convergence as $\Delta t$ gets really small” business certainly feels suspiciously familiar.

Yep, it’s calculus!

Solving the Differential Equation

We need to go deeper.

Going back to the original equation, you can write $F$ and $v$ in terms of $x$ (still assuming mass to be 1):

$$\frac{d^2x}{dt^2} = -kx - c\frac{dx}{dt}$$

Oh no, a differential equation.

But if you solved it, you’d have exactly the explicit equation you’re looking for. Because damped harmonic oscillation isn’t that hard of a problem, the solution actually exists, too.

I tried my hand at solving it myself for a bit, but I’m not really any good at differential equations. Thankfully, the internet exists, and other people have already succeeded!

An article3 I found outlines several solutions of the differential equation:

Additional parameters:

Alternate forms

This shouldn’t actually be very hard to implement. Simply obtain the initial state, compute all parameters, and then, when updating, just increase a time variable instead of doing an iterative time step.

However, there is another issue to consider: parameters may need to change halfway through the animation, especially if the animation is interactive. Obviously, you’d want to preserve the current velocity and position of the spring.

The simplest solution is to just read those out and use them for the new initial state $(x_0, v_0)$. This means we also need velocity in addition to displacement, so we’ll need the derivatives too:

Finally, I split my implementation into a SpringSolver that handles only a single solution, and a Spring that presents the same interface as the Spring from the code earlier.

struct Spring {
    damping_ratio: f64,
    period: f64,
    time: f64,
    target: f64,
    solver: SpringSolver,
}

impl Spring {
    fn update(&mut self, dt: f64) {
        // not much to do here except advance time!
        self.time += dt;
    }

    fn value(&self) -> f64 {
        // SpringSolver doesn’t deal with targets and only computes displacement from 0,
        // so we need to add the target to get the actual value
        self.target + self.solver.displacement(self.time)
    }

    fn target(&self) -> f64 {
        self.target
    }

    fn velocity(&self) -> f64 {
        self.solver.velocity(self.time)
    }

    fn damping_coefficient(&self) -> f64 {
        4. * self.damping_ratio * f64::consts::PI / self.period
    }

    fn set_value(&mut self, value: f64) {
        // preserve velocity
        let velocity = self.velocity();
        let displacement = value - self.target;
        self.time = 0;
        self.solver = SpringSolver::new(self.damping_ratio, self.damping_coefficient(), displacement, velocity);
    }

    fn set_target(&mut self, target: f64) {
        // preserve value and velocity
        let value = self.value();
        let velocity = self.velocity();
        let displacement = value - target;
        self.time = 0;
        self.solver = SpringSolver::new(self.damping_ratio, self.damping_coefficient(), displacement, velocity);
        self.target = target;
    }

    fn set_velocity(&mut self, velocity: f64) {
        // preserve displacement
        let displacement = self.solver.displacement(self.time);
        self.time = 0;
        self.solver = SpringSolver::new(self.damping_ratio, self.damping_coefficient(), displacement, velocity);
    }
}

/// SpringSolver handles the solution for a single state, with the only free variable being time.
enum SpringSolver {
    Underdamped {
        // c
        damp_coef: f64,
        // phi
        phase: f64,
        // A
        amp_fac: f64,
        // omega D
        ang_freq_d: f64,
    },
    Critical {
        // x 0
        init_disp: f64,
        // v 0
        init_vel: f64,
        damp_coef: f64,
    },
    Overdamped {
        damp_coef: f64,
        // c D
        damp_coef_d: f64,
        // A 1 and A 2
        amp_fac_1: f64,
        amp_fac_2: f64,
    },
}

impl SpringSolver {
    /// Creates a new spring solver for the given parameters.
    fn new(
        damping_ratio: f64, // zeta
        damp_coef: f64,     // c
        init_disp: f64,     // x 0
        init_vel: f64,      // v 0
    ) -> SpringSolver {
        if damping_ratio < 1. {
            // underdamped

            // omega 0
            let ang_freq = damp_coef / damping_ratio / 2.;
            // omega D
            let ang_freq_d = (ang_freq * ang_freq - damp_coef * damp_coef / 4.).sqrt();
            // A
            let amp_fac = (
                init_disp * init_disp
                + ((2. * init_vel + damp_coef * init_disp) / (2. * ang_freq_d)).powi(2)
            ).sqrt();
            // phi
            let phase = (2. * init_vel + damp_coef * init_disp)
                .atan2(2. * init_disp * ang_freq_d);

            SpringSolver::Underdamped { damp_coef, amp_fac, phase, ang_freq_d }
        } else if damping_ratio == 1. {
            // critically damped
            SpringSolver::Critical { init_disp, init_vel, damp_coef }
        } else {
            // overdamped

            // omega 0
            let ang_freq = damp_coef / damping_ratio / 2.;
            // c D
            let damp_coef_d = 2 * ((damp_coef / 2.).powi(2) - ang_freq * ang_freq).sqrt();
            // A 1
            let amp_fac_1 = (-2. * init_vel + init_disp * (-damp_coef + damp_coef_d)) / (2. * damp_coef_d);
            let amp_fac_2 = (2. * init_vel + init_disp * (damp_coef + damp_coef_d)) / (2. * damp_coef_d);

            SpringSolver::Overdamped { damp_coef, damp_coef_d, amp_fac_1, amp_fac_2 }
        }
    }

    // Computes displacement at time t.
    fn displacement(&self, t: f64) -> f64 {
        match self {
            SpringSolver::Undamped { damp_coef, amp_fac, phase, ang_freq_d } => {
                amp_fac * (-t * damp_coef / 2.).exp() * (ang_freq_d * t - phase).cos()
            }
            SpringSolver::Critical { init_disp, init_vel, damp_coef } => {
                (-t * damp_coef / 2.).exp() * (init_disp + (t * (2. * init_vel + damp_coef * init_disp)) / 2.)
            }
            SpringSolver::Overdamped { damp_coef, damp_coef_d, amp_fac_1, amp_fac_2 } => {
                amp_fac_1 * (t * -(damp_coef + damp_coef_d) / 2.).exp() + amp_fac_2 * (t * -(damp_coef - damp_coef_d) / 2.).exp()
            }
        }
    }

    /// Computes velocity at time t.
    fn velocity(&self, t: f64) -> f64 {
        match self {
            SpringSolver::Undamped { damp_coef, amp_fac, phase, ang_freq_d } => {
                -amp_fac * (-t * damp_coef / 2.).exp() * (
                    damp_coef / 2. * (ang_freq_d * t - phase).cos()
                    + ang_freq_d * (ang_freq_d * t - phase).sin()
                )
            }
            SpringSolver::Critical { init_disp, init_vel, damp_coef } => {
                -(-t * damp_coef / 2.).exp() * (t * damp_coef * damp_coef * init_disp + 2. * init_vel * (damp_coef * t - 2.)) / 4.
            }
            SpringSolver::Overdamped { damp_coef, damp_coef_d, amp_fac_1, amp_fac_2 } => {
                -amp_fac_1 * (damp_coef + damp_coef_d) / 2. * (-t * (damp_coef + damp_coef_d) / 2.).exp()
                    - amp_fac_2 * (damp_coef - damp_coef_d) / 2. * (-t * (damp_coef - damp_coef_d) / 2.).exp()
            }
        }
    }
}
The iterative solution is shown in the background. The difference between the two approaches is especially apparent when T is small.

The precision and robustness of this method compared to the iterative solution is really great!!

Plus, having an explicit function means that you can now also use springs for animation that’s on a separate timeline and not necessarily real-time.

I’ve used a variant of this code for a long time now, and usually I just copy-paste it verbatim from project to project, because unlike big libraries and applications, math doesn’t really need that much maintenance! (Though, there were a few floating-point precision issues here and there.) This article presents a cleaned-up version of that code with fewer questionable decisions.

I hope it helped! That’s all!

Footnotes and References

  1. I was originally introduced to this idea in the Apple WWDC talk “Designing Fluid Interfaces.”
  2. Gaffer On Games: Fix Your Timestep!
  3. DampedHarmonicOscillator.nb by Glenn Agnolet (HTTP Link)