Animating with Ease In/Out: Deriving the calculations for smooth animations using no calculus.
The problem with Linear Interpolation
Animating an item directly from point A to B is simple when using “linear interpolation”. One simply moves the item at a constant rate for the duration of the animation:
The “problem” with this, shown above, are the changes in the velocity of the object. Linear interpolation produces instantaneous, infinite acceleration and deceleration. Physical objects that humans interact with in the real world do not work that way. Objects always spend some time coming up to speed and—unless there’s a brick wall involved—some time slowing down at their destination.
Computer interfaces are more pleasing to humans when the velocities change. So let’s figure out how to do that. And let’s do so in a way that makes sense, and is easy to re-derive when you are stranded on a deserted island and need to come up with the answer.
Plotting Velocities
First, let’s describe our problem in a way that enumerates what we have to work with.
- We want to specify how long it takes the animation to take place.
- We want to specify how much time is spent accelerating, and decelerating.
- We want to specify where the object is moving to (and possibly where it’s moving from).
- During the animation, at any given time, we know what time it is and want to figure out where the object should be.
Let’s draw a diagram showing the velocities we want:
The items in black are what we have control over. The items in green we may need to work out.
Calculating the Distance from Velocity
Now, I said that there wouldn’t be any calculus involved. That’s mostly true, except that we’re going to use one helpful fact often taught in calculus class. The area under a velocity curve is equal to the distance traveled. At any time if we can figure out how much hashed area there is to the left, we know how far along we should be in our animation.
With that one tool in our belt we can revert to geometry to write the equations needed for our program. Let’s start at the beginning of our animation and work out what we need along the way.
Instead of deriving equations that work with specific start and end values, I want a single solution that given ta
, tg
, td
, and the elapsed time tn
can tell us what percent through the animation we should be. Then we can use that percentage to lerp between any start and end values. In addition to being a nicely generic solution, this is particularly useful when animating more than one property, such as a 3D position comprised of X, Y, and Z values. We perform the calculation to find the completion percent once, and then use that value to lerp each position value between the start and end points.
Given this desire, we want the total area under the velocity graph to be 1.0
, corresponding a value of 100% complete at the end. The area is the sum of the area of the middle rectangle and end triangles:
area = da + dg + dd 1.0 = ½tav + tgv + ½tdv
Rearranging, we can find the maximum velocity for any given triplet of timings:
1.0 = (½ta + tg + ½td)v v = 1.0 / (½ta + tg + ½td) v = 1.0 / (tg + ½(ta + td))
Now, given any tn
, how far through the animation should we be? Let’s break it down into the three sections:
- If we’re still accelerating—if
tn ≤ ta
—then our percent complete is just the area of the triangle shown in the graphic at that start with a base oftn
and height ofvn
. - If we are gliding—if
ta < tn ≤ ta+tg
—then our percent complete is the area of the accelerating triangle, plus a small rectangle whose base istn-ta
and whose height isv
. - If we are decelerating—if
ta+tg < tn ≤ t
—then our percent complete is most simply computed as 100% complete minus the small triangle of animation still to come, whose base ist - tn
and whose height is somevn
.
In the accelerating and decelerating zones, while we can compute the current vn
, it is easier to recognize that the smaller triangle is proportional to the full sized triangle, and that the ratio of the areas is thus the square of the ratio of the bases. That might be confusing in prose, but in math we can just say (when looking at the acceleration-so-far portion):
tn² ½tnvn ——— = ——————— ta² ½tav
We can then rearrange this to find just the area of the smaller triangle,
expressed without needing to know vn
:
½tn²tav tn²v ½tnvn = ——————— = —————— ta² 2ta
We can find the area of the deceleration-remaining triangle similarly.
Armed with this knowledge, we can write code for finding the percent complete.
// elapsed - how much time has passed since the animation started // accel - how much time should be spent accelerating // glide - how much time should be spent at maximum speed // decel - how much time should be spent decelerating function percentComplete(elapsed, accel, glide, decel) { // The following two values could be precomputed, // assuming unchanging values for accel, glide, and decel const totalTime = accel + glide + decel const maxVelocity = 1 / (glide + (accel + decel)/2) if (elapsed <= accel) { return maxVelocity * elapsed ** 2 / (2 * accel) } else if (elapsed <= (accel + glide)) { return maxVelocity * (accel / 2 + (elapsed - accel)) } else if (elapsed < totalTime) { return 1.0 - maxVelocity * (totalTime - elapsed)**2 / (2 * decel) } else { return 1.0 } }