Command Palette

Search for a command to run...

Hectal
Course outline
Topic 7.5Intermediate ~26 min · step 5 of 6

Camera Animation

In one line

To move the camera between views, turn progress into a position: lerp the target, slerp the camera's direction and lerp its distance, shaped by an easing curve and measured in seconds. Stop when the user grabs the controls, and jump instead of flying when reduced motion is on.

0/6 · 0%

Think of it like this

A film crew moves the camera on a dolly track. It starts slowly, speeds up, and eases to a stop. A hard cut between two shots is instant but can be jarring. A shaky, fast sweep makes some viewers feel sick. A good camera move is smooth, takes a moment and never cuts through the set.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

lerp
Linear interpolation: the point t of the way from a to b, in a straight line.
slerp
Spherical linear interpolation: turning from one direction or rotation to another along the shortest arc at a steady speed.
easing
A function that reshapes progress from 0 to 1 so a move starts and ends gently.
tween
A move between two values over a set time, "in between" frames.
prefers-reduced-motion
A setting in the visitor's operating system, readable from CSS and JavaScript, asking sites to avoid large animations.
vestibular
To do with the inner ear's sense of balance. Large camera movements on a screen can upset it.

Step by step

01Look at the path

The first example flies the camera between a side view and a close view of a wheel, waits one second, and again. Its console lists the eased progress and the camera position at five moments.

02Let the user win

The second example runs the same flight twice, with the visitor's drag after half a second. The first ignores the drag and ends exactly at the destination. The second cancels on start and ends where the visitor left the camera.

03Compare the paths

The third example samples both paths, the straight lerp and the arc, and prints how close each gets to the bike's centre.

04Cut instead of flying when asked

The first example's first line prints the two durations: 1.2 s with motion allowed, 0 s (an instant cut) when prefers-reduced-motion: reduce matches. The check runs each time a flight starts, so turning the setting on in the operating system takes effect at the next button press, with no reload.

Try it yourself

  1. 1

    Linear instead of eased

    In the first example change the first line to const easeInOut = (t) => t;. The progress column becomes 0.250, 0.500, 0.750, and the camera at t 0.25 is already at 0.09, 0.65, 2.15 instead of 0.03, 0.51, 2.49: the move starts at full speed.

  2. 2

    Opposite views

    In the third example change degToRad(160) to degToRad(180). The straight line now passes exactly through the target: closest to the bike centre 0.00 m. The arc still prints 3.00 m. For exactly opposite directions setFromUnitVectors chooses the turning axis itself (here the vertical one).

Code & diagrams

Fly between two views New tab

The pathAt() function is the same one the showroom's controls use. The console lists the path at five moments; the flight then repeats with a 1 s pause.

idle

Console

Run the scene to see its console.

Expected console output

duration: motion allowed 1.2 s | reduced motion 0 s (an instant cut)
t 0.00 eased 0.000 camera 0.00, 0.45, 2.60
t 0.25 eased 0.063 camera 0.03, 0.51, 2.49
t 0.50 eased 0.500 camera 0.08, 0.76, 1.68
t 0.75 eased 0.938 camera -0.15, 0.73, 0.86
t 1.00 eased 1.000 camera -0.20, 0.70, 0.75
The user grabs the controls mid-flight New tab

Console only. No pointer is simulated: the code sends the `start` event OrbitControls sends for a press, then calls rotateLeft(0.8) as the drag. Each story runs 90 ticks of 1/60 s.

idle

Console

Run the scene to see its console.

Expected console output

flight ignores the user:   camera ends at -0.20, 0.70, 0.75 (the drag is lost)
flight stops on 'start':   camera ends at -1.48, 0.67, 1.63 (where the user left it)
the detail view is at           -0.2, 0.7, 0.75
A straight line cuts through the bike New tab

Console only: 41 sample points along each path, and the closest each comes to the target (the bike's centre).

idle

Console

Run the scene to see its console.

Expected console output

straight line: closest to the bike centre 0.52 m
slerp arc:     closest to the bike centre 3.00 m
exactly opposite views: turn axis 0, 1, 0 (chosen by three.js, not by you)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Ignore the user

In the second example change story(true) to story(false), or delete the controls.addEventListener("start", ...) line.

terminal
$ Run
── what you'll see ──
flight ignores the user: camera ends at -0.20, 0.70, 0.75 (the drag is lost)
flight stops on 'start': camera ends at -0.20, 0.70, 0.75 (where the user left it)

Myth vs fact

Myth

Animating the camera is the same as animating a mesh.

Fact

With OrbitControls the camera's position is recomputed from the controls' target, so you must set the target too and let update() run, or the controls undo your move on the next frame.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    setFromUnitVectors for exactly opposite directions has to pick a turning axis itself, so a flight to the exact opposite side may not turn the way you want. Presets are rarely that far apart, but a test with a 180° case is cheap.

  • ▸

    A hidden tab pauses requestAnimationFrame. The Phase 3 loop uses THREE.Timer with timer.connect(document), which reports a delta of 0 while the tab is hidden and resets its clock when the tab returns (Topic 3.2), so a flight resumes instead of jumping to its end.

Remember this

  1. 1

    The problem this solves: the "rear wheel" button must take the visitor to a new view. Setting the camera there at once confuses them about where they are. A careless animation can drag the camera through the bike, fight the visitor's own drag, or make a person with a vestibular disorder dizzy.

  2. 2

    Lerp and slerp. a.lerp(b, t) moves a Vector3 from a to b in a straight line, as t goes from 0 to 1. Quaternion.slerp (spherical linear interpolation) turns along the shortest arc at a steady speed. A turn made by lerping angles can take the long way round and speed up and slow down unevenly (Topic 2.6).

  3. 3

    Easing. If t grows steadily the camera starts and stops abruptly. An easing function reshapes it. easeInOut(t) = t < 0.5 ? 4t³ : 1 − (−2t + 2)³ / 2 maps 0.25 to 0.063, 0.5 to 0.5 and 0.75 to 0.938: slow, fast in the middle, slow. Every frame, t = elapsed / duration, where elapsed sums the loop's delta (Topic 3.2). That keeps the move at 1.2 s on a 30 Hz and a 144 Hz screen, and a hidden tab (delta 0) does not skip it.

  4. 4

    Do not lerp the position. A straight line between two camera positions on opposite sides of the bike passes through the bike. In the third example the straight path from 0° to 160° comes within 0.52 m of the bike's centre. Instead keep the camera on a sphere around the target: lerp the target, slerp the direction from the target to the camera, and lerp the distance. The arc stays 3.00 m away.

  5. 5

    Work with the controls. During a flight set camera.position and controls.target each frame and keep calling controls.update(). Listen to the controls' start event (press, touch or wheel): if the user grabs the camera mid-flight, cancel the flight, or every drag is overwritten and the camera snaps to its destination (the second example prints both endings). A new flight starts from where the camera is now.

  6. 6

    Reduced motion. Some people get dizzy or sick from large screen movements. The CSS media query (prefers-reduced-motion: reduce) says so, and window.matchMedia("(prefers-reduced-motion: reduce)").matches reads it. When it is true, replace the flight with a cut: set the final position at once. Read it every time you start a move, so a change in the system settings is respected without a reload.

Explain it without notes

01

Why is lerping the camera's position between two views a bad path, and what is the better one?

02

What should happen to a camera flight when the visitor presses the mouse button on the canvas, and why?

Practice

01

Write easeOut(t) that starts fast and ends slowly, using 1 - (1 - t) ** 3, and print it at 0.25, 0.5 and 0.75.

02

Write goTo(view) that respects reduced motion: it should return 0 and move at once if the visitor asked for it, otherwise start a 1 s flight and return 1.

Done when you can

  • Use lerp, slerp and an easing curve driven by seconds, not frames

  • Move the camera on an arc around the target, not on a straight line

  • Cancel a flight when the user grabs the controls

  • Replace a flight with a cut when reduced motion is on