Course outline
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.
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
tof the way fromatob, 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
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
Opposite views
In the third example change
degToRad(160)todegToRad(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 directionssetFromUnitVectorschooses the turning axis itself (here the vertical one).
Code & diagrams
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.
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.75Console 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.
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.75Console only: 41 sample points along each path, and the closest each comes to the target (the bike's centre).
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.
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.
- ▸
setFromUnitVectorsfor 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 usesTHREE.Timerwithtimer.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
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
Lerp and slerp.
a.lerp(b, t)moves aVector3fromatobin a straight line, astgoes 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
Easing. If
tgrows steadily the camera starts and stops abruptly. An easing function reshapes it.easeInOut(t) = t < 0.5 ? 4t³ : 1 − (−2t + 2)³ / 2maps 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, whereelapsedsums the loop'sdelta(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
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
Work with the controls. During a flight set
camera.positionandcontrols.targeteach frame and keep callingcontrols.update(). Listen to the controls'startevent (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
Reduced motion. Some people get dizzy or sick from large screen movements. The CSS media query
(prefers-reduced-motion: reduce)says so, andwindow.matchMedia("(prefers-reduced-motion: reduce)").matchesreads 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
Why is lerping the camera's position between two views a bad path, and what is the better one?
What should happen to a camera flight when the visitor presses the mouse button on the canvas, and why?
Practice
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.
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