Animation
Game loops, requestAnimationFrame, smooth motion, physics, easing, and practical animation techniques.
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.
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 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.
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. 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!
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 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! 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
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.
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.
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
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
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.
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.
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.
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.