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

Top: LinearBottom: Eased

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;
}
glsl

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);
}
glsl

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;
}
glsl

Interactive: Compare easing functions

Ease-in
Ease-out
Red: Ease-inGreen: Ease-out

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);
glsl

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);
glsl

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 seconds
glsl

The 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);
glsl

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 after
glsl

Combine with smoothstep for smooth transitions:

float fade = smoothstep(2.0, 2.5, u_time);  // fade in from t=2 to t=2.5
glsl

For 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;
glsl

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);
glsl

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)));
glsl

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);
glsl

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);
glsl

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*t for ease-in, t*(2-t) for ease-out
  • smoothstep(0, 1, t) is a built-in ease-in-out curve
  • Use fract or mod to create looping animations
  • Use step and smoothstep to trigger and sequence events
  • Stagger animations by position for wave-like effects