My favourite animation trick: exponential smoothing

There's a certain simple animation thing that I've been using almost since I've ever started doing anything related to graphics. I use it for rotating & moving the camera, for moving figures in a turn-based game, for moving UI elements, for smoothing volume changes in my audio lib, everywhere! So I decided I'll write about it. The trick itself is nothing new, – in fact, you've probably already heard about or even used it, – but I'll also show it in some examples and explain how it works mathematically :)


Toggle buttons

Speaking of UI, say you're making some UI component, maybe a toggle button. Something like this (click it!):

This simply computes the position of the switch as a function of its state:

position.x = turned_on ? max_x : min_x;

This works perfectly, but feels a bit lifeless. Adding some animation to it would be cool! Animations are not just a fancy visual thing, they help the user understand what's going on. Instead of teleporting the toggle indicator to its new position, let's move it smoothly:

The downside is that we need to run some updating animation now:

position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);

However, this still looks a bit clumsy due to having a constant speed (i.e. the position is a linear function of time). Let's add some easing function on top of that, like the classic cubic \( 3t^2-2t^3 \):

or a square root \( \sqrt t \):

The difference between these may be hard to see, so let's slow down the animation by a factor of 8:

Square root:

This time, instead of just updating the switch position, we have to keep track of some extra animation state:

t += (turned_on ? 1 : -1) * speed * dt;
t = clamp(t, 0, 1);
ease = (3 * t * t - 2 * t * t * t);
position.x = lerp(min_x, max_x, ease);

Here, I'm using the fact that smoothstep is symmetric in the following sense: 1 - f(t) = f(1 - t), meaning the forward and backward animations can use the same code. With sqrt things are a bit different: we have to explicitly use a different easing function depending on the animation's direction:

ease = turned_on ? sqrt(t) : 1 - sqrt(1 - t);

Whichever looks best is arguably a matter of taste, but of all these sqrt is my favourite: the switch starts moving really fast (this is because sqrt has infinite derivative at zero), but then slows down nicely as it reaches the destination (the cubic one is my second favourite, though). The downside of this version is that we need quite a lot of bookkeeping even in the simplest possible case of a two-state toggle button (later in the article I'll show how this becomes a nightmare in more complicated scenarios). Another downside is that it has a discontinuity: it jumps suddenly if the user clicks on it the middle of animation (try it!).

Thankfully there's a similar version which uses the minimal possible state and doesn't have the "jumping" problem:

I call it exponential smoothing (for reasons that will become clear later). I've also heard it being called approach, and I'm certain it has it's own name in every engine. Here it is slowed down 8x and compared to sqrt:

Square root:

Here's the code for the exponential version:

target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));

Intuitively, on each frame we nudge the current position towards its target position (which is determind by the on/off state). However, the amount of nudging (1 - exp(- dt * speed)) looks really weird, doesn't it? Before we see where it comes from, let's have a look at some more complicated animations.

Camera movement

Say we have some kind of map, and a camera scrolling/moving around.

Yes I've made a whole procedural map generator & renderer just for this example, and I have zero regrets.

Again, this begs us to add some animation. Let's interpolate it with constant speed:

Here's the code:

position.x += sign(target.x - position.x) * speed * dt;
            position.y += sign(target.y - position.y) * speed * dt;

See this jittering after the animation completes? That's because target.x - position.x keeps alternating between being positive and negative. Instead of sign(delta) we need some function that clamps the delta:

float update(float & value, float target, float max_delta)
    float delta = target - value;
    delta = min(delta,  max_delta);
    delta = max(delta, -max_delta);
    value += delta;

update(position.x, target.x, speed * dt);
update(position.y, target.y, speed * dt);

Quite a mouthful for such a simple thing! And here's the result:

Much better, although it's still a bit clumsy and also weird if we move the camera faster than the animation completes. We could, as before, add some easing function, like the cubic one:

although this time it gets really complicated: we have to maintain a queue of requested movement events, and animate them one by one (otherwise I have no idea how to slap the easing function here). This still looks a bit weird when moving the camera fast enough. We could just ignore user's input while the animation is active, but this is a deadly sin as it is infuriatingly frustrating from the user's perspective.

The perfect solution? Why, exponential smoothing of course! The code barely changes compared to the toggle button example:

position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
            position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));

and here's how it looks like:

Pretty nice, if you ask me! Notice how it speeds up naturally if you click fast enough.

Under the hood

Ok, so what's up with this 1 - exp(- speed * dt), what on Earth is that?

Let's start with a simplified version: we have some animation, it has a current position and the new position target which it must move towards with some speed. To make the movement faster when the difference between position and target is large, we make the speed proportional to this difference:

position += (target - position) * speed * dt;

Notice how it doesn't require maintaining any state other than the current and the target position! (speed is usually a constant.) It even doesn't need to keep track of time that elapsed since the start of the animation, and it adjusts automatically if the target suddenly changes.

Now this already works perfectly in many situations, but there's a small catch. Here's the toggle button again, with the above udpate code:

See the jittering? That's because I've set the speed value so high that speed * dt became larger than 1! Specifically, I used speed = 220 and dt = 1 / 125.

To understand what's happening, it is useful to rewrite the code above using lerp:

position = lerp(position, target, speed * dt);

You can check that this is ultimately the same formula. We can clearly see what's going on: the formula interpolates between the current value and the target value. The closer the interpolation parameter speed * dt to zero, the slower the interpolation. The closer it is to one, the faster the movement.

Now, what happens when speed * dt is larger than 1 is that the interpolation overshoots! The only reason it still works is that speed * dt is less than 2, so that the absolute delta between position and target still decreases with time. Here's an example with speed * dt = 248 / 125 < 2:

and here's one with speed * dt = 252 / 125 > 2:

The last one doesn't do anything useful at all.

To solve this, we could simply clamp the value by 1:

position = lerp(position, target, min(1, speed * dt));

However, this doesn't seem like the right thing to do in all scenarios. Consider why speed * dt might actually happen to be so large?

One reason is that your speed value is too large because you want a really quick animation. However, as we've seen with the above toggle buttons, this is actually way too quick for any reasonable user – the actual animation is impossible to notice. So, our speed value is usually not that high.

The other reason is that dt is too large. Maybe because your code runs too slow, and your framerate is dropping. Maybe because the user moved to a different tab/window and your code was sleeping, and now it got woken up with a dt of many seconds.

When applying such a dt to something like physics, you certainly want to clamp it, or subdivide into several updates, etc. With animations, however, wouldn't it be cool if everything worked perfectly even in this case? Even if your physics might lag, at least the camera & buttons would still work nicely – as a user, I would really appreciate such care.

Differential equations (oh no)

Ok, we want to solve the problem, but how? Here's the two-step recipe:

  1. Realize that what we're doing is numerically solving a certain differential equation
  2. Solve the equation symbolically and use the result directly

Time-dependent update that works for small dt but breaks for large dt is pretty typical for numerical solvers of differential equations. What equation does position += (target - position) * speed * dt solve? Whenever you see A += B * dt, this corresponds to an equation \[ \frac{d}{dt}A=B \] In our case, the equation is \[ \frac{d}{dt}\text{position} = (\text{target} - \text{position})\cdot\text{speed} \]

I will die if I keep typing these formulas with all words spelled out, so let's make a few variable changes: call \( x = \text{position} \), \( a = \text{target} \), and \( c = \text{speed} \):

\[ \frac{d}{dt}x = (a-x)\cdot c \] Solving this needs just a few tricks: \[ \frac{d}{dt}(x-a) = \frac{d}{dt}x = (a-x)\cdot c = -(x-a)\cdot c \] \[ x-a = (x_0-a)\cdot\exp{-c\cdot t} \] \[ x = a - (a-x_0)\cdot\exp{-c\cdot t} \] \[ x = x_0 + (a-x_0)\cdot\left(1-\exp{-c\cdot t}\right) \]

Btw, a similar exponent appears in e.g. volumetric rendering for pretty similar reasons.

It's not important to understand exactly where this all comes from. The point is that if we believe that position += (target - position) * speed * dt is the right formula for small dt, then the formula position += (target - position) * (1 - exp(- speed * dt)) is the right formula to use for any dt. This is further supported by expanding the latter equation in terms of Taylor series for the exponent: \( \exp(x) \approx 1 + x \), so that

\[ 1 - \exp(- \text{speed} \cdot \text{dt}) \approx 1 - (1 - \text{speed} \cdot \text{dt}) = \text{speed} \cdot \text{dt} \]

i.e. we get exactly the former equation.

The cool thing is that it doesn't care about old values: if you have your previous value \( x_0 \) and you know how much time has passed between the previous and the current iteration, you can compute the new value. (This is a direct consequence of being a first-order differential equation.)

So, the TL;DR is that

position += (target - position) * (1 - exp(- speed * dt))

is the right formula that works for any speed and dt. Even if the product speed * dt is too large, exp(- speed * dt) handles it nicely, since exp of a large negative number is just something close to zero, so 1 - exp will be close to one.

We can, as before, rewrite this using lerp: position = lerp(position, target, 1 - exp(- speed * dt)) or even position = lerp(target, position, exp(- speed * dt)). There are many ways to rewrite this equtaion.

Choosing the speed

Usually, we think of animation in terms of its duration. Like, the toggle button should move to the new place in 0.125 seconds (the actual value used in the examples in the beginning of the post), after that it stops moving. With this exponential formula, however, the animation technically takes infinite time to complete! exp(- speed * time) gets smaller with time, but it never equals zero, so that position technically never equals target (provided they were different to start with).

However, in practice we have a ton of limitations. If position is a floating-point number, it quickly reaches the precision limit, and it becomes equal to target in practice. If it is, say, the camera position, the user probably won't notice that the animation is still going since the delta target - position gets ridiculously small even before it hits floating-point precision limits.

So, what does the speed parameter mean, exactly? It means the following: 1 / speed is the time in which position becomes closer to target by a factor of e = 2.71828... exactly. Do whatever you want with this information.

I usually set the speed to something in the range 5..50. For a linear/cubic animation of a certain speed, I usually set the exponential version speed to be 2 * speed, this feels about right (again, this is what was used in the examples above).

Exponential smoothing

If you google "exponential smoothing" (or "exponential moving average"), you might find the wiki article on something completely unrelated which, nevertheless, features some pretty similar formulas. It actually is the discrete analogue of what we were talking about in this post!

Suppose that our dt is always the same; also suppose that target changes as often as every iteration. Then, indexing the values with the iteration number, we compute something like position[i] = (target[i] - position[i - 1]) * factor, where factor = 1 - exp(- speed * dt). In this case, one typically sets factor directly to some value between 0 and 1 instead of deriving it from other values (although the aforementioned wiki article does explain what this factor actually means).

People use it in signal processing for the same reasons I do for animations: it doesn't require maintaining previous values or any other obscure state, just the current averaged value. They also use it in digital audio, where you typically have a fixed dt of 1 / freq the inverse sampling frequency (e.g. 1/44100 or 1/48000).

Last paragraph title

I had the idea of this post for many months, glad to finally get it done :)

As always, thanks for reading!