Easing and Timing
Making motion feel natural
Linear motion looks mechanical. Objects in the real world accelerate and decelerate. A ball rolling to a stop slows down gradually. A door swinging open speeds up as gravity takes hold, then slows as the hinges resist. Easing functions capture this natural quality of motion.
Why Linear Feels Wrong
When something moves at a constant speed, our brains register it as artificial. We have evolved to expect acceleration. Watch anything move in the physical world and you will see it speed up from rest, maintain velocity, then slow to a stop.
Linear motion vs eased motion
The top ball moves at constant speed. The bottom ball eases in and out. Even without physics knowledge, the eased version feels more natural and pleasing to watch.
The Easing Functions
Easing functions take a normalized time value from 0 to 1 and return a new value, also typically 0 to 1, but with the rate of change shaped differently. The input is linear time, the output is curved time.
The three fundamental easing types are:
- Ease-in: Starts slow, accelerates toward the end
- Ease-out: Starts fast, decelerates toward the end
- Ease-in-out: Slow at both ends, fast in the middle
The three fundamental easing curves
Horizontal axis: time (0-1) | Vertical axis: animated value (0-1)
The steeper the curve at any point, the faster the motion appears at that moment. Ease-in is shallow at the start (slow) and steep at the end (fast). Ease-out is the opposite.
Implementing Easing
The simplest easing functions use powers. Squaring the input creates ease-in:
float easeInQuad(float t) {
return t * t;
}For ease-out, we flip the curve. Think of it as ease-in played in reverse:
float easeOutQuad(float t) {
return t * (2.0 - t);
}Ease-in-out combines both, using ease-in for the first half and ease-out for the second:
float easeInOutQuad(float t) {
return t < 0.5
? 2.0 * t * t
: -1.0 + (4.0 - 2.0 * t) * t;
}Interactive: Compare easing functions
Higher powers create more dramatic easing. t * t * t (cubic) has more pronounced acceleration than t * t (quadratic). pow(t, 5.0) is even more extreme.
Using Smoothstep for Easing
The built-in smoothstep function is itself an ease-in-out curve. It uses a cubic polynomial that has zero slope at both ends:
float eased = smoothstep(0.0, 1.0, t);This is mathematically equivalent to 3t^2 - 2t^3, a standard Hermite interpolation. It is smooth, continuous, and often exactly what you need.
For more control, you can use smoothstep on remapped values:
float t = fract(u_time);
float eased = smoothstep(0.0, 1.0, t);Looping with Mod and Fract
To create repeating animations, use mod or fract to wrap time into a cycle:
float cycle = fract(u_time); // 0 to 1 every second
float cycle = fract(u_time * 0.5); // 0 to 1 every 2 seconds
float cycle = mod(u_time, 3.0) / 3.0; // 0 to 1 every 3 secondsThe result is linear sawtooth motion. Apply easing to make it feel natural:
float t = fract(u_time);
float eased = smoothstep(0.0, 1.0, t);Interactive: A bouncing ball with easing
Sequencing with Step
To trigger events at specific times, use step:
float started = step(2.0, u_time); // 0 before 2 seconds, 1 afterCombine with smoothstep for smooth transitions:
float fade = smoothstep(2.0, 2.5, u_time); // fade in from t=2 to t=2.5For sequenced animations, calculate how far into each phase you are:
float duration = 2.0;
float phase = mod(u_time, duration * 2.0);
float forward = smoothstep(0.0, duration, phase);
float backward = smoothstep(duration, duration * 2.0, phase);
float position = forward - backward;Sequenced animation with phases
Four phases: right, up, left, down (each with easing)
Staggered Animations
One of the most visually appealing techniques is staggering, where elements animate with slight time offsets based on their position.
float delay = uv.x * 0.5; // elements on the right start later
float t = max(0.0, fract(u_time) - delay);
float eased = smoothstep(0.0, 0.5, t);Staggered animation by position
Each cell starts its animation based on its position
The delay creates a wave-like effect as the animation ripples across the screen. You can stagger by x, y, distance from center, or any other spatial property.
Common Patterns
Here are some combinations that appear frequently:
Breathe effect: Gentle, continuous pulsing
float breathe = smoothstep(0.0, 1.0, abs(sin(u_time)));Pop in: Quick appearance with overshoot
float t = clamp(u_time - startTime, 0.0, 1.0);
float pop = 1.0 + sin(t * 3.14159) * 0.2 * (1.0 - t);Heartbeat: Two quick pulses, then rest
float beat = mod(u_time, 1.0);
float pulse = smoothstep(0.0, 0.1, beat) * smoothstep(0.3, 0.2, beat);
pulse += smoothstep(0.15, 0.25, beat) * smoothstep(0.45, 0.35, beat);Key Takeaways
- Linear motion feels mechanical; easing makes motion feel natural
- Ease-in accelerates, ease-out decelerates, ease-in-out does both
- Simple easing uses powers:
t*tfor ease-in,t*(2-t)for ease-out smoothstep(0, 1, t)is a built-in ease-in-out curve- Use
fractormodto create looping animations - Use
stepandsmoothstepto trigger and sequence events - Stagger animations by position for wave-like effects