Easing & tweening
Turn time into a 0–1 progress, bend it with an easing curve, and build staged, staggered and back-and-forth animations from it.
How it works
A tween moves a value from one state to another over a set time: a wipe from left to right, a flash fading out, a
panel sliding in. In a shader it is three steps. Turn time into a progress from 0 to 1, bend the progress with an
easingA curve that bends a 0–1 progress so motion speeds up or slows down instead of moving at a constant rate. Easing A curve that bends a 0–1 progress so motion speeds up or slows down instead of moving at a constant rate. Linear interpolation (mix) Blends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.ease-in-out: e = p² (3 − 2p) curve, and mixBlends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.mix(a, b, t) = a (1 − t) + b t
float p = clamp((time - startTime) / duration, 0.0, 1.0); // progress: 0 before, 1 after
float e = p * p * (3.0 - 2.0 * p); // eased (smoothstep's curve)
float x = mix(-0.2, 1.2, e); // the animated value
Keeping the three apart is the point: the easing curve can change without touching the timing, and the same progress can drive several values at once.
Easing curves
Motion at constant speed looks mechanical, because real objects speed up and slow down. Easing curves are shaping curves that keep 0 at 0 and 1 at 1, named by where they are gentle:
float easeInQuad(float p) { return p * p; } // starts slow
float easeOutQuad(float p) { return 1.0 - (1.0 - p) * (1.0 - p); } // ends slow
float easeInOutQuad(float p) { // slow at both ends
return p < 0.5 ? 2.0 * p * p : 1.0 - 2.0 * (1.0 - p) * (1.0 - p);
}
float easeOutCubic(float p) { float q = 1.0 - p; return 1.0 - q * q * q; }
Higher powers make the slow part longer and the fast part sharper. Ease-out suits things arriving on screen, since they settle into place; ease-in suits things leaving, since they gather speed; ease-in-out suits moves from one rest position to another.
Two families go past the ends on purpose. A “back” ease overshoots and returns, like a panel that springs slightly past its place:
float easeOutBack(float p) {
const float k = 1.70158; // the classic overshoot, about 10%
float q = p - 1.0;
return 1.0 + (k + 1.0) * q * q * q + k * q * q;
}
An elastic ease overshoots too, with a decaying sine on top. Bounce, a series of falling arcs, stays inside 0–1 but hits the end several times. Curves that overshoot produce values above 1 or below 0, so check that what they drive can take them: a color might need a clamp, while a position can simply pass its target.
CSS describes most easing as cubic-bezier() curves. A shader can’t evaluate those cheaply, because finding the value
at a given time means solving a cubic equation, so shaders write the curve directly as a polynomial like the ones above.
Staged and staggered animation
Many animations have phases: a mask grows, then a color shifts, then everything fades. Give each phase its own window of the overall progress by remapping it:
float stage(float p, float a, float b) { return clamp((p - a) / (b - a), 0.0, 1.0); }
float grow = easeOutCubic(stage(p, 0.0, 0.5)); // first half
float shift = stage(p, 0.3, 0.8); // overlaps the end of the growth
float fade = easeInQuad(stage(p, 0.7, 1.0)); // the last 30%
Windows can overlap, which usually reads as smoother than strictly one-after-the-other. Staggering is the same remap
with a delay per element: for a row of tiles, stage(p, i * 0.05, i * 0.05 + 0.4) starts tile i a moment after the
one before, the cascade used in transitions and UI reveals.
Back and forth, and loops
A tween that should repeat needs a progress that repeats. fract(time / period) restarts at 0 every period, which
jumps back. For back and forth, fold it into a triangle that goes up and then down:
float loop = fract(time / 2.0); // 0..1 every 2 seconds
float pingPong = 1.0 - abs(loop * 2.0 - 1.0); // 0..1..0 every 2 seconds
float e = easeInOutQuad(pingPong); // eased both ways
To hold at each end before turning, remap the folded value: stage(pingPong, 0.1, 0.9) rests at 0 and at 1 for a
moment, a fifth of each half, before moving on.