Animation

Game loops, requestAnimationFrame, smooth motion, physics, easing, and practical animation techniques.

Lesson 6 of 10

What Is Animation?

Animation is an illusion. Nothing actually “moves” on your screen. Instead, you show a series of still images so quickly that your brain perceives motion. It is exactly the same trick as a flipbook: each page has a slightly different drawing, and when you flip through them fast enough, things appear to move.

The flipbook analogy:

Frame 1       Frame 2       Frame 3       Frame 4
┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
│        │   │        │   │        │   │        │
│ ●      │   │  ●     │   │   ●    │   │    ●   │
│        │   │        │   │        │   │        │
└────────┘   └────────┘   └────────┘   └────────┘

Show these fast enough → your brain sees a ball moving right!

Canvas animation:
1. Clear the canvas        (erase the page)
2. Update positions        (draw the next frame slightly different)
3. Draw everything         (show the new page)
4. Repeat 60 times/second  (flip fast!)

On a Canvas, we do this by writing a function that clears the screen, updates some numbers (positions, sizes, colors), draws everything fresh, and then asks the browser to call that function again for the next frame. That cycle (clear, update, draw, repeat) is called the game loop (even if you are not making a game).

Why 60 Frames Per Second?

Most monitors refresh their display 60 times per second. That is 60 frames per second, or 60fps. Movies use 24fps, which is enough for film but looks choppy for interactive graphics. At 60fps, each frame lasts about 16.7 milliseconds. If your drawing code takes longer than that, frames get dropped and animation looks janky.

Frame timing at different rates:

Framerate   Time per frame   Feels like
─────────   ──────────────   ──────────
 10 fps       100.0 ms       slideshow, painful
 24 fps        41.7 ms       cinema film
 30 fps        33.3 ms       okay for video
 60 fps        16.7 ms       smooth for interaction ← target
120 fps         8.3 ms       butter-smooth (gaming monitors)

Rule of thumb: keep your update+draw under 16ms and
the browser handles the rest.

requestAnimationFrame vs setInterval

You might think: “I will just use setInterval(draw, 16) to run my loop 60 times per second.” That works, but it has serious problems. setInterval is a general-purpose timer; it does not know anything about the screen. It will keep firing even when the browser tab is hidden (wasting battery), it drifts out of sync with the monitor, and it cannot adapt to different refresh rates.

requestAnimationFrame(callback) was built specifically for animation. It syncs with the display refresh, pauses automatically when the tab is hidden, and lets the browser batch your drawing with other visual updates for maximum efficiency.

setInterval vs requestAnimationFrame:

setInterval(draw, 16):
├─ fires every ~16ms regardless of monitor refresh
├─ keeps firing when tab is hidden (wastes CPU/battery)
├─ can drift: 16ms timer ≠ exactly 60fps
└─ no guarantee drawing happens before next screen paint

requestAnimationFrame(draw):
├─ fires exactly once before each screen repaint
├─ pauses automatically when tab is hidden
├─ adapts to any refresh rate (60hz, 120hz, 144hz)
└─ browser optimizes paint timing

Always use requestAnimationFrame for canvas animation.
Your First Animation paused

Try this: Change x += 2 to x += 5 for faster movement, or x += 0.5 for slow motion. Then add a var y = 50; variable and increment it each frame to make the ball move diagonally.

Notice the pattern: clearRect wipes the slate, we move x a bit, draw the circle at the new x, and call requestAnimationFrame(loop) to schedule the next frame. The variable __raf stores the frame ID so the demo harness can cancel the loop when you hit Reset or navigate away.

Stopping an animation: When building your own pages, call cancelAnimationFrame(id) with the ID returned by requestAnimationFrame to stop the loop. Without this, animation loops keep running forever, even after removing the canvas from the page, wasting CPU and causing memory leaks.

The Timestamp Parameter

When the browser calls your animation function, it passes one argument: a timestamp in milliseconds. This is a high-precision number from performance.now() that tells you exactly when this frame started. You will use it to calculate how much time has passed between frames.

function loop(timestamp) {
  // timestamp = 16834.5  (milliseconds since page load)
  // ...draw stuff...
  __raf = requestAnimationFrame(loop);
}
__raf = requestAnimationFrame(loop);
//                              ↑
// The browser calls loop(timestamp) for you.
// You never pass the timestamp yourself: the browser does.

Delta Time: Frame-Rate Independent Motion

In the first demo, we moved the ball by a fixed 2 pixels every frame. That means on a 60fps monitor the ball moves 120 pixels per second, but on a 120fps monitor it moves 240 pixels per second, twice as fast! This is a bug, not a feature.

The fix is delta time (often written dt). Instead of moving a fixed amount per frame, you decide a speed in pixels per second, then multiply by how many seconds actually passed since the last frame.

The delta time formula:

speed = 150 pixels per second

Frame at 60fps (dt = 0.0167s):
  movement = 150 * 0.0167 = 2.5 pixels this frame

Frame at 120fps (dt = 0.0083s):
  movement = 150 * 0.0083 = 1.25 pixels this frame

Both rates → 150 pixels per second. Consistent speed!

Formula:  distance = speed * deltaTime
          where deltaTime = (now - lastTime) / 1000
Frame-Rate Independent Motion paused

On a standard 60fps display both balls look similar, but the green one (delta time) will maintain the same speed on any device. The pink one will be twice as fast on a 120Hz gaming monitor. Always use delta time.

The Game Loop Cycle

Let’s zoom out and see the full picture of what happens each frame. This is the heartbeat of every game, animation, and interactive canvas app.

The game loop: what happens 60 times per second:

        ┌──────────────────────┐
        │  Browser calls your  │
        │  loop(timestamp)     │
        └──────────┬───────────┘
                   │
                   ▼
        ┌──────────────────────┐
        │  1. Calculate dt     │  dt = (now - last) / 1000
        │     (delta time)     │  last = now
        └──────────┬───────────┘
                   │
                   ▼
        ┌──────────────────────┐
        │  2. Clear canvas     │  ctx.clearRect(0, 0, w, h)
        └──────────┬───────────┘
                   │
                   ▼
        ┌──────────────────────┐
        │  3. Update state     │  position += velocity * dt
        │     (physics, AI,    │  check collisions
        │      input, logic)   │  handle user input
        └──────────┬───────────┘
                   │
                   ▼
        ┌──────────────────────┐
        │  4. Draw everything  │  ctx.fillRect, arc, etc.
        └──────────┬───────────┘
                   │
                   ▼
        ┌──────────────────────┐
        │  5. Request next     │  __raf = requestAnimation-
        │     frame            │    Frame(loop)
        └──────────┬───────────┘
                   │
                   └──── repeats forever ───┘

Physics Basics: Position, Velocity, Acceleration

Real-world objects do not teleport; they accelerate, cruise, and slow down. To make canvas animation feel physical and natural, we model three properties:

The three layers of motion:

Position     = where the object is right now (x, y)
Velocity     = how fast it's moving and in what direction (vx, vy)
Acceleration = how velocity is changing (ax, ay)

Each frame:
  velocity += acceleration * dt
  position += velocity * dt

Example: a ball falling due to gravity:
  ay = 500 (pixels/sec², pointing down)
  vy += 500 * dt    → velocity increases each frame
  y  += vy  * dt    → position changes by velocity

This is Euler integration: simple and good enough for games.
Position, Velocity, and Acceleration paused

The green ball starts slowly and speeds up. That is acceleration at work. This single idea (velocity changes position, acceleration changes velocity) is the foundation of all physics-based animation: gravity, bouncing, springs, friction, and more.

Gravity Simulation: The Bouncing Ball

Gravity is just acceleration pointing downward. Each frame, gravity adds to the ball’s downward velocity, making it fall faster and faster. When the ball hits the floor, we reverse its vertical velocity (bounce) and multiply by a number less than 1 so it loses energy each bounce, just like a real ball. This demo also uses ctx.shadowColor and ctx.shadowBlur to add a glow effect around the ball; setting shadowBlur back to 0 disables it for subsequent draws.

Gravity simulation each frame:
  vy += gravity * dt        // gravity pulls the ball down
  y  += vy * dt             // ball moves according to velocity

When ball hits floor (y + radius > floorY):
  y  = floorY - radius      // snap to floor surface
  vy = vy * -0.7            // reverse direction, lose 30% energy
                             //  -0.7 = bounce + energy loss

The ball bounces lower each time until it "settles" on the floor.
Gravity and Bouncing Ball paused

Try this: Once the ball settles on the floor, click Run to drop it again.

Friction and Damping

In real life, things slow down. A ball rolling on the ground loses speed to friction. A swinging pendulum gradually stops. In code, we simulate this by multiplying velocity by a number slightly less than 1 each frame. This is called damping.

Damping (simple friction):
  velocity *= 0.99          // lose 1% speed each frame

Problem: this is frame-rate dependent! At 120fps you
apply 0.99 twice as often → things slow down faster.

Frame-rate independent damping:
  velocity *= Math.pow(damping, dt)

Example with damping = 0.01 (meaning "keep 1% per second"):
  At 60fps:  v *= pow(0.01, 0.0167) = v *= 0.926  each frame
  At 120fps: v *= pow(0.01, 0.0083) = v *= 0.962  each frame
  Both reach the same speed after 1 second!
Friction: Things Slow Down paused

Sine Wave Motion: Oscillation

Math.sin() is the animation secret weapon. It produces a smooth wave that oscillates between -1 and +1, repeating forever. Feed it a steadily increasing number (like time) and you get smooth back-and-forth motion for free, no physics needed.

Sine wave anatomy:

Math.sin(time) oscillates between -1 and +1:

 +1 ─  ╭──╮       ╭──╮       ╭──╮
       │  │       │  │       │  │
  0 ───┤  ├───────┤  ├───────┤  ├──── time →
       │  │       │  │       │  │
 -1 ─  ╰──╯       ╰──╯       ╰──╯

To control oscillation:
  Amplitude:  Math.sin(t) * 50       → swings 50px each way
  Frequency:  Math.sin(t * 3)        → 3x faster oscillation
  Offset:     200 + Math.sin(t) * 50 → centers at y=200

Use cases: bobbing, pulsing, floating, breathing effects
Sine Wave: Bobbing, Pulsing, Orbiting paused

Lerp: The Most Useful Animation Function

Lerp stands for linear interpolation. It answers the question: “Given a start value and an end value, what is the value at some percentage between them?” It is a single line of code and it is absurdly useful.

Lerp explained:

function lerp(start, end, t) {
  return start + (end - start) * t;
}

t = 0   → returns start
t = 0.5 → returns halfway between start and end
t = 1   → returns end

Example: lerp(100, 300, 0.25) = 100 + (300-100) * 0.25 = 150

The "smooth follow" trick:
  x = lerp(x, targetX, 0.1)   // each frame, move 10% closer

This creates smooth, decelerating motion: the object
moves fast when far away and slows as it approaches.
It's how most UI animations work!
Lerp: Smooth Follow paused

This “smooth follow” pattern is everywhere: camera systems in games, cursor trails, tooltip positioning, scroll animations, and more. Changing the lerp factor (0.08 above) controls how snappy vs floaty the follow feels. Note that the demos use addListener(canvas, …) instead of canvas.addEventListener so the demo harness can automatically clean up event listeners on reset.

Particle Trails Using Semi-Transparent Clear

Normally we clear the canvas completely each frame with clearRect. But there is a beautiful trick: instead of clearing fully, draw a semi-transparent rectangle over the entire canvas. The previous frame fades but does not disappear instantly, creating a trail or “ghosting” effect.

Normal clear (no trails):
  ctx.clearRect(0, 0, w, h);     // 100% erase, clean slate

Trail effect (semi-transparent overlay):
  ctx.fillStyle = 'rgba(10, 10, 15, 0.1)';  // dark + 10% opaque
  ctx.fillRect(0, 0, w, h);

Lower alpha = longer trails (more ghosting)
Higher alpha = shorter trails (fades faster)

rgba(10, 10, 15, 0.05) → long comet tails
rgba(10, 10, 15, 0.15) → medium trails
rgba(10, 10, 15, 0.4)  → short fade
Particle Trails: Semi-Transparent Clear paused

Animating Colors: Hue Cycling

HSL color mode makes animating colors trivial. The hue component goes from 0 to 360 (red -> yellow -> green -> cyan -> blue -> magenta -> red). Just increase the hue over time and you get a smooth rainbow cycle.

HSL = Hue (0-360), Saturation (0-100%), Lightness (0-100%)

hue: 0=red  60=yellow  120=green  180=cyan  240=blue  300=magenta

Cycling: hue = (hue + speed * dt) % 360

Per-object rainbow: hue = (baseHue + index * spacing) % 360
This offsets each object's color for a rainbow spread.
Hue Cycling: Rainbow Animation paused

FPS Counter: Measuring Performance

How do you know if your animation is running smoothly? Build an FPS counter. Count how many frames happen each second. If the number drops below 60, your drawing code is too slow and you need to optimize.

FPS counter strategy:
  1. Count frames in a running window
  2. Every 0.5 seconds, calculate: frames / elapsed = fps
  3. Display the number

Alternative (simpler): fps = 1 / deltaTime
  But this fluctuates wildly frame-to-frame.
  Averaging over 0.5-1 second looks much better.
FPS Counter with Animated Scene paused

Green means smooth (55+ fps), yellow means acceptable (30-54), red means trouble (below 30). If your FPS drops, try reducing the number of objects, simplifying draw calls, or using techniques like offscreen canvases.

Easing Functions: Natural-Feeling Motion

Linear motion, moving the same amount every frame, looks robotic. Real objects ease in (start slow), ease out (slow down at the end), or both. Easing functions take a progress value t (from 0 to 1) and return a curved version of it.

Common easing functions:

linear(t)       = t              flat line, constant speed
easeInQuad(t)    = t * t          starts slow, ends fast
easeOutQuad(t)   = t * (2 - t)   starts fast, ends slow
easeInOutCubic   = smooth S-curve slow → fast → slow

bounce(t)  = ball-drop effect
elastic(t) = springy overshoot

All take t from 0→1 and return a modified 0→1 value.

Usage:
  var t = elapsed / duration;        // 0 → 1 over time
  var eased = easeInOutCubic(t);     // apply curve
  var x = startX + (endX - startX) * eased;  // interpolate
Easing Functions: Visual Comparison paused

Watch how each ball moves differently despite covering the same distance. The bounce easing looks like a ball dropped on the floor. The elastic one overshoots then settles. Choose the easing that matches the feeling you want.

Springs and Elastic Motion

A spring simulation gives you organic, bouncy motion that is hard to achieve with easing curves alone. The idea comes from Hooke’s law: the further a spring is stretched from its rest position, the harder it pulls back. Add some damping so it eventually settles.

Spring physics:

Spring force = -stiffness * displacement
Damping force = -damping * velocity

Each frame:
  var displacement = position - target;
  var springForce  = -stiffness * displacement;
  var dampingForce = -damping * velocity;
  velocity += (springForce + dampingForce) * dt;
  position += velocity * dt;

stiffness controls how "snappy" it is  (higher = faster)
damping controls how quickly it settles (higher = less bounce)

Low stiffness + low damping  = slow, bouncy jelly
High stiffness + low damping = fast, ringy vibration
High stiffness + high damping = fast snap, minimal bounce
Spring Physics: Click to Set Target paused

Click in different spots rapidly and watch the ball oscillate. Springs are perfect for UI interactions (drag-and-snap, pull-to-refresh, dialog animations) because they respond naturally to interruption; you can change the target mid-animation and the spring adapts smoothly.

Practical: Loading Spinner

Let’s build something real. A loading spinner combines rotation (steadily increasing angle), arc drawing, and a little math to make the arc length pulse in and out. This is the kind of animation you see in every web app.

Animated Loading Spinner paused

Practical: Wave / Ocean Effect

Layer multiple sine waves at different frequencies and amplitudes, and you get a convincing water surface. This technique works for any natural-looking wavy motion: flags, grass, audio visualizers, and more.

Layered Sine Waves: Ocean Effect paused

Practical: Analog Clock

A clock combines several animation concepts: trigonometry for hand positions, real time data from Date, and the game loop for smooth second-hand ticking. This demo reads the actual current time and displays it.

Real-Time Analog Clock paused

What You Learned

Lesson 6 recap, everything you need for canvas animation:

Core loop:      clear → update → draw → requestAnimationFrame
Delta time:      distance = speed * dt  (frame-rate independent)
Physics:         velocity += acceleration * dt; position += velocity * dt
Gravity:         acceleration pointing down + bounce on collision
Friction:        velocity *= Math.pow(damping, dt)
Sine waves:      Math.sin(time * freq) * amplitude + offset
Lerp:            start + (end - start) * t  (smooth follow)
Springs:         force = -stiffness * displacement - damping * velocity
Trails:          semi-transparent fillRect instead of clearRect
Color cycling:   hsl(hue, sat, light) where hue increases over time
Easing:          curve functions that map t(0→1) to modified t(0→1)
FPS counter:     count frames per half-second interval
__raf:           always store requestAnimationFrame ID for cleanup

With these tools you can animate almost anything. The next lesson covers interaction: responding to mouse, touch, and keyboard input to make your animations interactive.