**Note** from Bead: Firewatch Camera Grid · [canonical source](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/notes/03-grid-layout-v9-sketch.md) · session 2026-06-24 · discussion: Talk: Firewatch Camera Grid
Sketched by Debbie (senior UX/UI). **v9 is the team-shareable build** of the grid-transition line. It builds **wholesale on the [v8 engine](02-grid-layout-v8-sketch.md)** — same one-scene / two-positions / `lerp(mapPos, gridPos, t)` mechanism, same position-preserving placement (min-cost assignment over normalized on-screen positions; left-stays-left → crossings → 0), same `MAP ◀▶ GRID` scrubber, same straight-lerp + dim-ghost basemap, same OrientedMap glyphs + tie-point red-ray, same TIME cursor, same zero create/destroy in the morph, **same removed 180° axis**. v9 wraps that engine for sharing with a **team of voters** and fixes the two interaction bugs the team caught. **Rendered sketch:** [https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v9.html](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v9.html) *(build stamp `2026-06-24` + `v9` in the top bar — freshness marker, house convention.)*
## What's new for team-sharing ### 1. Relabel for the team Every `stephen` / `Stephen` author reference in the page (and in the code comments + the answer dock payload) is replaced with **"Redfish Team"** / "the team". The page is going to a room of voters, not one author. (Verified by grep: zero `stephen` left in the file.) ### 2. Token = identity (light identity, no heavy login) Each teammate already has (or pastes once) an acequia **bearer token** — that **is** their identity. No signup, no password. - On load we read the token from `localStorage('acequiaToken')` (the v8 paste-once flow is kept; the ambient cookie still authenticates the PUT/PROPFIND via `credentials:'include'`, JS just can't read it). - We **decode the JWT payload** client-side (`b64urlDecode` of the middle segment) → `sub`, `name`/`preferred_username`/`email`/`principal`, `iss`, `exp`. **Defensive:** an opaque / non-JWT token → claims fall back to `null` and we still get a usable identity from the fingerprint. - We compute a **token fingerprint**: `sha-256` of the raw token via `crypto.subtle.digest` → hex → first 10 → shown as e.g. **`tok:3c9d12a7`**. This is the **token id**. - An optional free-text **display name** (remembered in `localStorage('acequiaDisplayName')`, editable from the onboarding panel or the "change name" link) so comments read as a human, not a UUID. - **Transparency line (the team's words), shown in the top-bar chip, the rail, the answer card, and the overlay:** *"Voting as <name> · token id `tok:3c9d12a7` — we record this id to attribute your vote, never your token."* The token-id chip is click-to-copy. **The dock record (`sketch-answer` meta) carries the token id, never the token.** Each meta now writes `from`/`who` (display name / decoded name / sub), `principal` (decoded `sub`), `tokenId` (the fingerprint), and `tokenClaims` (`{sub,name,email,iss,exp,…}` — non-secret only). **The raw bearer is never written into the `.md` or `.meta.json`** — it rides only the `Authorization: Bearer` header so the server can authenticate it. *Verified with a harness: a JWT whose signature segment is a known secret produces a meta + md in which neither the raw token nor the secret signature appears.* ### 3. 📊 Results dashboard A **📊 results** toggle (top bar + an in-card "see results" button), collapsed by default so it **doesn't bias the initial vote**. When opened it: - **`PROPFIND` (Depth 1)** the bead's `request/` dock (same-origin, bearer + `credentials:'include'`), parses the WebDAV multistatus for `*.meta.json` hrefs, **`GET`s each**, and filters to this sketch family (`sketch` starts with `grid-layout`). - **Tallies votes per fork** (simple low-fi bars: A aspect-follows / B fixed) and **lists each vote**: the **voter token id** (`tok:…`, never the token) + display name + their choice + comment + timestamp. - A manual **↻ refresh**; graceful empty / `!ok` / `401`/`403` / network messages. Sketch-grade: bars + a scrollable list, house palette. ### 4. First-time onboarding overlay A modal on first visit (gated on `localStorage('gridV9_onboarded')`; a **?** button reopens it). Content, house style: - **Purpose of this sketch:** the spatial morph between **map** and an arranged **grid** (one scene, `lerp(mapPos→gridPos)`), and that cards are placed to **preserve their on-screen position** so paths don't cross. - **The question you're voting on:** the grid-**shape** fork — A aspect-follows-layout vs B fixed reading-order — with the invitation to vote + comment and then open 📊 results. - **How to select cameras:** click cameras on the map; with **no explicit selection the grid auto-uses every camera currently in the map view** — pan/zoom to choose the set. - **Scope note (the team's words):** *"other sketches will explore different ways of making camera selection sets — that is not the focus of this sketch."* - **Bill Buxton link** — bead [`174efbfc`](https://redfish.acequia.io/guerin/.agents/174efbfc-d73f-4754-b138-d2cdbbefed6d/) + talk [*"Sketching the User Experience"*](https://www.youtube.com/watch?v=Prt2DxjYCH0) — framed as "why these are rough sketches, not finished designs (Buxton: sketch to get the **right** design)."
## The two interaction fixes ### FIX A — map filtering actually re-filters on pan/zoom `effectiveCams()` (viewport-implicit selection) was already correct *math* — `inViewBox` tests `camBoxPoint(c)` = `project()`, which composes the pan/zoom view transform — and `recomputeEffective()` runs on `onViewChanged`, `zoomBy`, wheel, and reset. The reason it *felt* broken was **FIX B**: drag-pan rarely actually moved the view, so the in-view set never changed. With pan fixed, the **`auto: N in view` count and the grid/lerp set now update live** as you pan/zoom. (No-explicit-pick ⇒ grid set = exactly the cameras whose dot is inside the current viewport.) ### FIX B — mouse drag-to-pan in BOTH 2D and 3D The pan math only mutates `view.{cx,cy}` in **world** space, so it is identical for 2D top-down and 3D oblique. The v8 bug was purely a **binding** one: the `mousedown` listener was on `#mapsvg`, which sits **under** the camera-node layer (`#scene`, z-index 30), so a grab that landed on a camera dot never reached the pan handler, and the gesture felt dead. v9: - binds the press on **`#stage`** (covers both map modes and the camera layer), so drag-to-pan works whether you grab bare map or a camera, in 2D **and** 3D; - distinguishes a **click (select a camera)** from a **drag (pan)** by a small **`PAN_THRESHOLD` (4px)** movement threshold — under threshold the camera's own click selects; over threshold we pan **and** suppress the trailing camera click (`suppressClickAfterPan`) so a drag never accidentally selects; - wheel-zoom moved to `#stage` too (so it fires over a camera dot as well); zoom buttons + reset unchanged.
## Kept from v8 (not regressed) Single-scene `lerp(mapPos,gridPos,t)` (one persistent node per camera, **zero create/destroy** in the morph — `render()` writes only node styles); **position-preserving placement** (NAIVE↔POSITION A/B toggle, crossings readout → 0 for one horizontal row); dim-ghost basemap; straight-lerp; `MAP ◀▶ GRID` scrubber; per-card un-pin + re-flow; OrientedMap glyphs + tie-point red-ray read-through; 3D oblique at t≈0; preserved TIME cursor; always-on identity color; panel + fullscreen. **The 180° axis stays removed.** `node --check` passes on the extracted script.
## The open fork (carried forward) — grid shape Position-preserving placement is the **fix** (shown, not posed). The one open choice is still the **grid's shape**, now **answerable by the whole team** (pick → live layout change → vote to the `request/` dock as a `sketch-answer`, `sketch:'grid-layout-v9'`): - **A — aspect-follows-layout (FIRST-CUT).** rows×cols match the cameras' spatial bounding-box aspect — a near-vertical line of cameras becomes a vertical column; best preserves the spatial reading, further reduces travel/crossings. - **B — fixed (≤3 cols, reading order).** Always ≤3 columns, top-to-bottom; predictable, dense — but a vertical line of cameras is forced into a wide row. The answer meta carries `resolved` forward — `placement:'position-preserving'`, `continuity:'map-grid-lerp'`, `lerp-path:'straight'`, `basemap:'dim-ghost-underneath'`, `empty-selection:'use-cameras-in-view'` — plus the **token-id identity fields** (§2) in place of the old hardcoded author.
## Status First-cut, **team-shareable**. Token-id identity wired + transparent (raw bearer never persisted — harness-verified); 📊 results dashboard reads the `request/` dock and shows token ids (not tokens); onboarding overlay with the Buxton link; viewport filtering and 2D/3D drag-pan fixed; v8 engine intact. Awaiting the team's fork choice (grid shape) via the `request/` dock — now a **vote**, tallied in the dashboard.
## References (bead cross-links) - Bead: Bill Buxton Sketching · [canonical](https://redfish.acequia.io/guerin/.agents/174efbfc-d73f-4754-b138-d2cdbbefed6d/)