Field notesRendering

Camera smoothing: exponential damping or a spring?

A camera that follows scroll needs a deliberate arrival: direct enough that the reader stays oriented, smooth enough that a wheel event does not jerk the scene. Two common models are exponential damping and a damped spring. Neither is automatically more natural. They have different state, response, and failure modes.

The current Lab smooths position and look-at target with 1 - Math.exp(-dt * 8). It does not use the spring described in the earlier version of this article. Here is how to choose between the two models without relying on that mistaken premise.

The problem is a fixed per-frame coefficient

Consider x += (target - x) * 0.05. After each frame, 95% of the previous error remains. After one second at 60 updates, the remaining error is 0.95 ** 60, about 4.61%. At 120 updates it is about 0.21%. The faster display has changed the motion.

This update does not overshoot a stationary target when its coefficient stays between zero and one. It approaches the target monotonically. An earlier claim that ordinary lerp naturally overshoots was wrong.

function damp(current, target, rate, dt) {
  return target + (current - target) * Math.exp(-rate * dt);
}

// dt is elapsed seconds; rate is in inverse seconds.
x = damp(x, target, 8, dt);

For a fixed target, the remaining error after time t is multiplied by exp(-rate * t). At rate 8, the error halves in approximately 87 milliseconds and falls to 5% in approximately 374 milliseconds. These are calculated properties of the equation, not measured page performance.

Compare the response yourself

Both curves start at zero, aim at one, and use a constant target. The spring starts with zero velocity. Giving both rates the same number does not match their settling times; the plot makes that difference visible.

Exponential damping (solid)Critical spring (dashed)

Two responses to a target moving from zero to oneExponential damping approaches the target faster than a critically damped spring with the same numerical rate. Exact sample values follow the plot.100 s1.5 s
Position for a unit target; rate 8 per second
ElapsedDampingSpring
0.1 s0.5510.191
0.25 s0.8650.594
0.5 s0.9820.908
1 s1.0000.997

Calculated response at rate 8. No animation or device benchmark is running.

A spring preserves velocity

A spring carries both position and velocity. For unit mass, one useful model is x″ + 2ζωx′ + ω²(x - target) = 0. The natural frequency ω controls the response scale. The damping ratio ζ controls its character: below one can oscillate; one is critically damped; above one is overdamped.

With ζ = 1 and a constant target during the step, this is an exact update. It avoids the timestep instability of a naive Euler integration:

function stepCriticalSpring(state, target, omega, dt) {
  const displacement = state.position - target;
  const c = state.velocity + omega * displacement;
  const decay = Math.exp(-omega * dt);
  return {
    position: target + (displacement + c * dt) * decay,
    velocity: (state.velocity - omega * c * dt) * decay,
  };
}

Use the old position and old velocity to compute both new values. Updating one field early and reading it to compute the other changes the equation. This version assumes finite inputs, positive omega, and nonnegative dt; the downloadable example module includes input checks.

A critically damped spring starting at rest will approach a fixed target without oscillating. An initial velocity or a changing target can still carry it across the target. “Critical damping” is not a promise that every animated path can never cross its destination.

Choose the response before tuning constants

Use exponential damping when you want a predictable follow with no retained momentum. It is a good fit for a reading camera that must remain close to the current scroll position. Use a spring when velocity continuity contributes to the interaction, such as an object that should carry momentum after a drag ends.

Keep the camera position and the look-at target coordinated. Smoothing only position while instantly changing orientation can produce a calm translation with a sudden rotation. The current Lab applies the same exponential coefficient to both vectors, as shown in the source excerpt.

Separate elapsed time from pause policy

An exact constant-target update composes across different frame subdivisions. A moving target is sampled at different times on a 30 Hz and 120 Hz display, so the complete interactive paths need not be identical. Frame-rate independence is a property with assumptions, not a blanket performance claim.

Decide what should happen after a hidden tab returns. The Lab bounds its simulation step to 50 milliseconds and pauses its active work when hidden. That prioritizes continuity over catching up every missed second. Another application may instead snap directly to the new target. Reset the frame timestamp on resume so hidden time does not accidentally become one enormous integration step.

Test the model and the interaction separately

For the equation, compare one 1-second step with 30, 60, and 120 smaller steps toward the same target. They should agree within floating-point tolerance. Then test the actual page: jump between chapters, pause mid-travel, resize, and return from a hidden tab. A stable equation does not guarantee a well-framed camera.

In the current Lab diagnostics, chapter is the chapter, cameraSettled reports proximity to the current target, and qIdx is the rendering-quality index. Poll the property that answers the question; an arbitrary nine-second sleep or a quality index cannot prove that a camera reached chapter three.

Sources & revision

Read the dated source excerpt. It includes original file paths, line numbers, and SHA-256 hashes for the version used in this guide. The public repository may contain a different revision.

28 Oct 2026: Corrected the camera model, the direction of damping, the meaning of qIdx, and unsupported CI timing claims. Added a reproducible response comparison.

These notes use AI assistance. Read the editorial method and corrections policy. Found an error? Send a correction with this article’s title.