Grid layout transitions — v5 sketch (map↔grid lerp) (Firewatch Camera Grid)

**Note** from Bead: Firewatch Camera Grid · [canonical source](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-23/notes/06-grid-layout-v5-sketch.md) · session 2026-06-23 · discussion: Talk: Firewatch Camera Grid

Sketched by Debbie (senior UX/UI). v5 builds on the [v4 sketch](./05-grid-layout-v4-sketch.md) but **replaces its core mechanism** — Stephen chose fork A (`deal-from-position`) and, more importantly, **corrected how it must be built**. Still in the sketch-ux house style (rendered HTML/SVG, "✎ SKETCH" mark, a replay-able transition, answer card → `request/` dock). **Rendered sketch:** https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-23/artifacts/sketches/grid-layout-transitions-v5.html *(build stamp `2026-06-23` + `v5` are in the top bar — freshness marker, house convention.)*

## Stephen's correction (verbatim — the load-bearing instruction) > Chosen fork: **`deal-from-position`** > **"no!. there's a grid position and a map position. lerp between them. don't go offscreen > then load the panel."** This is a **rejection of the v3/v4 mechanism**, not just a fork pick. v3/v4 had **two panes** (`mapPane` + `gridPane`); one **slid offscreen** while **spawned "flyer" clone elements** bridged the gap and then `.remove()`d themselves. That is a *page-turn with disposable bridges* — identity is born and destroyed in flight, and the grid "loads" as a fresh panel. Stephen's "no!" is precisely at that: **don't go offscreen, don't load a panel.**

## The v5 model — one scene, two positions, lerp v5 is **ONE scene of N persistent camera objects** — one DOM node per camera, present at all times in `#scene`. Each camera object holds **two rectangles**: - **`mapRect(t)`** — its geographic dot on the basemap (600×400 map space → screen, via the `slice` cover-scale), sized as a small dot/frustum glyph. - **`gridRect()`** — its ordered grid-slot rectangle (slot index from the 180° auto-order). The transition is **nothing but**, for **every camera at once**: ``` rect = lerp(mapRect, gridRect, t) t: 0 = MAP … 1 = GRID ``` `{x,y,w,h}` interpolate together (small dot → full card); card chrome (chip, label, az, ord) fades **in** as `t→1`; the map-glyph dot styling fades **out**. The **basemap** (range rings, scene marker, 180° axis) **cross-fades in place** as `t→1` — **it never slides**. **Nothing is created or destroyed during the morph.** The element you clicked on the map **is** the card you land on — same DOM node, repositioned by the lerp. This is the maximal-continuity reading of `deal-from-position`, and it **subsumes and deletes** the v4 flyer/pane-swap entirely. ### How it replaces the flyer / pane-swap (concretely) - **Gone:** `mapPane`, `gridPane`, `apply()` block-slide, `flyToGrid`/`flyToMap`, the `.flyer` clones, `spawnTrail`, `buildTiles`. (Verified: no `flyer` / `.remove()` / `gridPane` / `mapPane` / `buildTiles` remain in the morph path.) - **In:** a single `render()` that walks `CAMS`, computes `lerp(mapRect, gridRect, t)` per camera, and writes only `style.left/top/width/height/opacity` on the **pre-built** nodes. `render()` creates **zero** DOM nodes. `animateT()`'s rAF tick just calls `setT`+`render`. ### The centerpiece — a MAP ◀——▶ GRID scrubber A labelled slider drives **`t` by hand**. Drag it and **every camera travels continuously** between its map dot and its grid slot — no pop, no teleport, no load. This literally embodies "lerp between them." The segmented **Grid | 3D | Map** toggle still works but it just **animates `t`** to 0 or 1 (a timed `ease`-lerp via rAF) — it **does not swap panes**.

## Kept from v3 / v4 (not regressed) - **180°-aware auto-order** sets each camera's `gridPos`; **per-card un-pin** (`✋#`) + **re-flow-the-rest around pinned anchors** (`reflowAroundPins`). - **OrientedMap glyph vocabulary** (frustum / centerline / dot) on every camera object; the **selected camera's tie-point red-ray read-through** is drawn into each node's frustum svg. - **3D oblique** basemap for `t≈0`; preserved **selection** and the amber **TIME cursor** across all `t`; segmented toggle; **fullscreen ⇄ panel** as layout options (panel clamps the morph mid-way so basemap + grid co-present). - **Always-on per-camera identity color** (ring on the dot ↔ chip on the card ↔ same hue) — the standing backstop at every `t`. - The v4 **standing tether / linked-brushing** is **demoted to a quiet optional toggle, default OFF** (honoring "no!" — the lerp is the star; the tether is just an aid for dense grids). - House style + the `request/`-dock `sendAnswer()` machinery (retargeted `sketch:'grid-layout-v5'`, carrying `resolved.continuity:'map-grid-lerp'` and the v3/v4 resolutions forward).

## The remaining small fork (the only thing still open) Mechanism is ratified, so v5 is near-prototype. The one genuine fork left is the **feel of the travel** — answerable in-sketch (it changes the **live** morph; re-scrub MAP◀▶GRID to watch): - **A — straight-line lerp** (literal interpolation of the two rects, in lockstep). - **B — ease-arc + azimuth stagger** (same endpoints; cards fan out along a gentle arc and stagger by 180° bearing — they peel off the map in spatial sequence). - **C — basemap stays vs fades** (independent dimension: at `t=1`, fully fade the basemap, or keep it as a dim ghost so the grid never fully leaves the map). **First-cut (a fork, not a verdict):** **A (straight-line lerp)** — the most literal reading of "lerp between them," and it reads as one coherent move; **B** is the polish pass once the mechanism is felt; on **C**, lean **fade-to-dim-ghost** (faint backdrop persists). The answer card PUTs the pick to the `request/` dock as a `sketch-answer`.

## The transition the ▶ tour plays select on the **3D** map → **scrub/animate `t`:0→1** (watch every camera travel to its slot — nothing offscreen, nothing loaded) → **brush** to show identity persists → **drag a card** → the rest **re-flow** on the 180° axis → fullscreen → panel (co-present at mid-`t`) → **animate `t`:1→0** (every camera flies home to its own map dot). *One scene, two positions, lerp.*

## Status First-cut (mechanism **ratified** and demonstrated; lerp-path fork open). `node --check` passes on the extracted script; the morph path is verified to create/destroy **no** elements — every camera moves continuously via style-only writes on its single persistent node. Awaiting Stephen's path-fork choice via the `request/` dock.