Engineering

Automatic zoom is a camera problem, not a cursor problem

Point a zoom at the cursor and you get a video that feels slightly seasick. Lumen treats a finished recording as footage instead: the whole pointer path is known before the first frame renders, so it can plan shots the way a camera operator would.

By Raashed ·

One recording, two cameras

Both cameras share the same zoom timing, so only the aiming differs. The right one is Lumen's real planner and camera, running in your browser from the same engine code the app ships. Switch the left one between a raw lock and a tuned spring, or press Use my mouse to record 8 seconds of your own. Nothing you record leaves this page.

Following the cursor is the wrong goal

A hand on a mouse is never still. It overshoots, wobbles and drifts while you read. A camera locked to that signal moves every pixel on screen, all the time, and the viewer can no longer read anything.

So the question is not how to smooth the cursor. It is three smaller questions, each with its own answer: when to zoom in, where to hold, and how to get there.

When to zoom: find working sessions

Lumen first drops movement that is only tremor, anything under 0.4% of the frame. A resting pointer then produces no samples at all, so it cannot hold a zoom open or trigger one at 0:00 just because recording began.

What remains is grouped into working sessions: runs of movement with pauses under 3 seconds. A session shorter than half a second that travels less than 8% of the frame is a nudge, and gets ignored.

A zoom starts 0.4 seconds before the pointer moves, so it arrives with the action rather than after it, and holds 1.5 seconds after the pointer stops. Every zoom lasts at least 3 seconds, so it reads as a deliberate shot rather than a blip. Two zooms less than 2.5 seconds apart merge into one, because holding through a short rest looks better than zooming out and straight back in.

Every automatic zoom uses the same 2.1x magnification. The planning below decides timing and framing, not scale.

Where to hold: plan shots around rest stops

The places a pointer stops are the places the viewer needs to see. Lumen counts a rest stop when the pointer stays within 0.6% of the frame for a quarter of a second.

Inside a session, the planner walks those stops in order and frames them the way an operator would. It keeps adding stops to the current shot while they span no more than 30% of the frame, and cuts to a new shot only when the work moves somewhere that does not fit.

Each shot holds for at least 2.5 seconds. If the pointer darts between two nearby spots sooner than that, they stay in one wider, steady shot, because that reads better than a camera whipping between two close-ups. When a cut does happen, it happens as the pointer leaves, so the camera travels with it.

How to move: the camera knows the future

A live follow camera is always late, because it can only react. A finished recording has no such limit. Lumen aims at where the pointer will next come to rest, looking up to 1.5 seconds ahead, so a move becomes a single glide toward the destination instead of a trace of the route the hand took. Then it holds.

A dead zone keeps the frame still the rest of the time. The camera only re-centers if the next rest stop is more than 12% of the half-viewport away, so tiny adjustments never move it. One safety net overrides that: if the pointer itself gets within 90% of the frame edge mid-move, the camera follows it, so it can never leave the shot.

Why two springs, in log space

The camera's scale and position come from a simulation at 120 Hz, not per-frame easing, for three reasons.

Zoom runs in log space, because perceived zoom is logarithmic. Going from 1x to 4x linearly punches in and then crawls. In log space it feels even.

Targets are clamped before they reach the spring. Valid viewports form a convex set, and a critically damped spring's impulse response is never negative, so its output is always a blend of valid targets. The camera needs no per-frame clamp, which is what used to cause snapping near the edges of the frame.

And the simulation is deterministic and cached, so preview, scrubbing and export all land on the same frame.

A single critically damped spring has one flaw: it leaves at full acceleration, which reads as a small jolt. Chaining two fixes that. The first spring eases the target and the second one is the camera, so motion starts from almost zero acceleration and still never overshoots.

The camera update

// Exact critically damped update, stable at any step size.
function springStep(s: { p: number; v: number }, goal: number, w: number, dt: number) {
  const decay = Math.exp(-w * dt);
  const e = s.p - goal;
  const c1 = s.v + w * e;
  s.p = goal + (e + c1 * dt) * decay;
  s.v = (c1 - w * (e + c1 * dt)) * decay;
}

// Two in a row: the first eases the target, the second is the camera.
springStep(target, shotCenter, 7.65, dt);
springStep(camera, target.p, 7.65, dt);

Share of a camera move completed

  • One spring
  • Two springs
0%50%100%0s0.5s1s1.5s2sOne springTwo springs
Both models finish 90% of a move in about 0.87 seconds. One spring peaks at 19.5 units/s² in its very first step. Two springs peak at 7.6 units/s² around 0.17 seconds in, about 2.5 times gentler, with no jolt at the start.
Show the data as a table
Share of a camera move completed over time
TimeOne springTwo springs
0.00s0.0%0.0%
0.10s7.5%0.9%
0.25s31.0%13.3%
0.50s65.7%53.8%
0.75s85.0%82.7%
1.00s93.9%94.8%
1.50s99.1%99.7%
2.00s99.9%100.0%

What happens with no cursor data

Wayland blocks global pointer access, so on a Wayland session Lumen has no cursor track to plan from. It still records, and it falls back to reading the picture itself by comparing frames to find where they changed.

That signal is noisier, so it gets more filtering. Changes under 0.02% of the frame are ignored. A change covering more than 30% of the frame is treated as a scene cut or a scroll, and ends the current focus. The outer 5% of changed pixels on each axis are trimmed, so a clock ticking in a corner cannot stretch the box. A lone changed sample is dropped as noise.

This is a real downgrade. Zooms follow on-screen activity rather than your hand, so they are less precise than on Xorg, Windows or macOS, where Lumen uses the real pointer path.

What the numbers actually say

The demo's 21.6-second sample path has the tremor, overshoot and reading drift of a real hand. Running it through all three cameras, with zoom timing shared so only the aiming differs, gives a clear result and a humbling one.

Against a raw lock on the pointer the gap is large. The hardest jolt falls from 80.5 to 5.7 viewport widths per second squared, about 14 times gentler, and total on-screen movement falls from 5.7 to 4.6 viewport widths.

Against a well-tuned spring chase the gap is small. The hardest jolt goes from 6.8 to 5.7 and movement from 4.9 to 4.6. The planner is also in motion for more of its zoomed time, 55% against 51%, because its glides are slower. On three random paths, both cameras took about half a second to settle after the pointer stopped, and neither clearly won at holding still through reading drift.

So the honest claim is modest. Most of the distance from unwatchable to watchable comes from any good smoothing. Planning changes the shape of the move: the camera picks a destination and goes there, rather than tracing the hand's route. Whether that looks better is a judgement call, which is why the demo lets you switch and see for yourself.

What this still cannot do

The planner knows where the pointer went, not why. It cannot tell the important click from the incidental one, so automatic zooms are a first cut and stay editable. A zoom you place by hand keeps its aim until the pointer rests far from center, because in that case the intent is yours, not the planner's.

See it on your own recording.

Lumen runs on Windows, Linux and macOS. Recordings stay on your machine.