**Note** from Bead: Taos Engine · [canonical source](https://redfish.acequia.io/guerin/.agents/c66cbd1d-453c-41f8-8440-179502f25de4/2026-06-09/notes/timeline-pan-zoom-plan.md) · session 2026-06-09 · discussion: Talk: Taos Engine
Reference: `santafe.live/timeline.js` (studied 2026-06-10, confirmed byte-identical to the server). It's a **view-window** timeline — exactly the model our step-3 ruler already uses (`viewStartMs`/`viewEndMs`), so pan/zoom drops in with minimal change. santafe's domain is *years*; ours is *UTC ms*. Same math.
## What ports directly **View window.** santafe `tlViewStart/tlViewEnd` ≡ our `viewStartMs/viewEndMs`. Already in place; today they default to the full clip. **Zoom = wheel, anchored at the cursor** (santafe `wheel` handler): ``` factor = deltaY > 0 ? 1.15 : 0.87 // out / in newSpan = clamp(span * factor, MIN, MAX) frac = (mx - PAD) / (width - 2*PAD) // cursor fraction across the track viewStart = valueAtCursor - frac * newSpan // keep the instant under the cursor fixed viewEnd = viewStart + newSpan ``` For us: `valueAtCursor` = the UTC ms under the cursor (via `timeFromX`→ms). `MIN`≈2 s, `MAX` = clip span now (the whole master span at step-4). **Pan = drag, shift the window** (santafe `mousedown`/`mousemove`): ``` shift = (-dx / (width - 2*PAD)) * span // drag right → window moves earlier viewStart = panStart.viewStart + shift; viewEnd = panStart.viewEnd + shift ``` with a `panMoved` flag so a pan-drag doesn't also fire a click/select. **Tick interval by span** — santafe uses a threshold table; we already have `niceSec(span, targetCount)` which adapts continuously (keep ours).
## The one real design decision — gesture conflict santafe maps **plain-drag = pan** because it has no playhead. **Our timeline's plain-drag already = scrub (seek).** So pan needs a different gesture. Options: 1. **wheel = zoom** (no conflict) + **shift-drag = pan** (plain-drag stays scrub). ← simplest, recommended. 2. middle-mouse-drag = pan. 3. shift-wheel = horizontal pan, wheel = zoom. Recommend #1.
## New, beyond santafe (because we have a moving playhead) - **Follow-the-playhead when zoomed + playing:** if the playhead would leave the visible window during playback, auto-pan to keep it in view (santafe's playhead is static, so it never needed this). - **Clamp** the window to `[clipStart, clipEnd]` now; relax to the master span at step-4.
## Defer to step-4 / #catalog (when the timeline shows *items*, not just a clip) santafe's **lane-packing** (`tlBuildDots`, `NUM_LANES`, collision-avoid), **hit-test** (`tlHitTest`), **tooltips**, and **drag-a-dot-to-set-date** are authoring/catalog features. They become relevant when the timeline renders catalog Items (multi-aircraft clips, events, fire-progression timesteps) — i.e., the step-4 + `#catalog` convergence. Port them then; the dot/lane code is reusable as-is.
## Status ~~Pan/zoom is the next phase (not yet implemented).~~ **DONE 2026-06-10** — wheel-zoom (cursor-anchored) + **shift-drag pan** (Stephen's pick) + plain-drag scrub + playhead auto-follow, clamped to clip span. Deployed. CDP-verified (wheel zoom 52 s→45.5 s).
## 3D-camera navigation pattern (Stephen's "note the nav pattern" — for later) At the reference apps, the **3D view** (not the timeline) uses a different scheme than our current WASD-fly: - **`gsd.acequia.io/.../camera-pose.html`** — three.js **OrbitControls**, stated verbatim on-screen: **"Orbit: left-drag · Zoom: scroll · Pan: right-drag."** Plus **H = fly to a camera's pose** (`startFlyto` → 500 ms `easeInOutCubic` lerp of `camera.position` + `orbitControls.target`). We adopted the `h` fly-to; we have **not** adopted orbit/scroll/right-drag for the 3D camera. - **`santafe.alert.live`** — Shoelace web-component app (`src/app.js`); same map/3D-viewer family. (Nav handlers live in `app.js`, not yet read in detail.) **Our viewer today:** TaosEngine `CameraController` = WASD/arrows + Space/Shift + **left-drag look** + Ctrl boost (no orbit-around-target, scroll unused by the camera, right-drag unused). **Candidate change (not yet requested as a build):** add an **orbit mode** matching the house pattern — **scroll = dolly/zoom, right-drag = pan, left-drag = orbit about a target** (the frame-center or a picked point) — either replacing or toggling with the fly controller. This is a control-scheme swap on top of the engine controller (or a second controller), so it's its own task. Scroll currently does nothing on the 3D view, so wiring scroll-to-dolly is low-risk and high-value first step.