fuzzyworld

tower / commit

docs(tools): wait for the view before dragging it, and re-photograph the tour

4699e6cb3e5babe65ea72a265bd27f85bf9ebff1 · Pavel Safronov · 2026-09-07 22:05:32 -0700 · parents da6bcb0e96f2

docs(tools): wait for the view before dragging it, and re-photograph the tour

Two shots added for the far cap work — exterior-true, which is the view in
through the window, and interior-night, which is the banked beam and the
pinwheel — plus every existing shot regenerated.

The tour also had a real bug, and it found it itself. interior-overhead
diverged 7.74% and chasing that turned up a drag starting before Three.js
had finished loading: the renderer had not attached its pointer handlers
yet, so roughly two thirds of the gesture was swallowed and the shot came
out pitched +13° when its drag is sized for 45°. It had been doing that
quietly for several runs behind a 2.5 s timeout that was usually, but not
reliably, long enough.

openView now waits for the view to exist instead of guessing how long it
takes. Both 3D views render a readout only once they are live, so a
non-empty one is the signal and no application change was needed to expose
it.

With the drag finally deterministic, 45° turned out to be the wrong framing
now that the far cap is a bright window — it is almost all haze, which is
honest and a poor picture of what the caption claims. 30° keeps the ground's
edge curving up on one side and the cap on the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
 README.md                                      |   3 +-
 docs/screenshots/desktop-exterior-true.png     | Bin 480301 -> 480678 bytes
 docs/screenshots/desktop-exterior.png          | Bin 277525 -> 277782 bytes
 docs/screenshots/desktop-interior-night.png    | Bin 237262 -> 237248 bytes
 docs/screenshots/desktop-interior-overhead.png | Bin 448992 -> 448986 bytes
 docs/screenshots/desktop-interior.png          | Bin 452858 -> 452859 bytes
 docs/screenshots/mobile-exterior.png           | Bin 70244 -> 70045 bytes
 docs/screenshots/mobile-interior.png           | Bin 145358 -> 145370 bytes
 docs/screenshots/mobile-panel.png              | Bin 72871 -> 72872 bytes
 docs/screenshots/overview.png                  | Bin 856785 -> 856749 bytes
 tools/screenshots.mjs                          |  71 ++++++++++++++++++++++---
 11 files changed, 65 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index 18c7d81..56e7f34 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,8 @@ in the picture instead of hiding behind a passing unit test.
 |---|---|
 | [map](docs/screenshots/desktop-map.png) | the unrolled inner surface, with a place selected |
 | [map-sightline](docs/screenshots/desktop-map-sightline.png) | shift-click two points to measure a sightline through the air |
-| [exterior](docs/screenshots/desktop-exterior.png) | hull, checkered end caps and the collector, terrain wrapped on as an x-ray |
+| [exterior](docs/screenshots/desktop-exterior.png) | x-ray hull — the inner surface projected out, with the caps and the collector |
+| [exterior-true](docs/screenshots/desktop-exterior-true.png) | true hull — bare skin, and the land visible through the far cap window |
 | [interior](docs/screenshots/desktop-interior.png) | standing at The Narrows, the ground curving up on both sides |
 | [interior-overhead](docs/screenshots/desktop-interior-overhead.png) | looking up: the far side 100 km overhead is clearer than ground 100 km away |
 | [reader](docs/screenshots/desktop-reader.png) | `?reader=1` — terrain, places, a detail card and the probe, no editing |
diff --git a/docs/screenshots/desktop-exterior-true.png b/docs/screenshots/desktop-exterior-true.png
index 1ac9f25..d666a43 100644
Binary files a/docs/screenshots/desktop-exterior-true.png and b/docs/screenshots/desktop-exterior-true.png differ
diff --git a/docs/screenshots/desktop-exterior.png b/docs/screenshots/desktop-exterior.png
index df28393..4d88325 100644
Binary files a/docs/screenshots/desktop-exterior.png and b/docs/screenshots/desktop-exterior.png differ
diff --git a/docs/screenshots/desktop-interior-night.png b/docs/screenshots/desktop-interior-night.png
index 66e9524..5d381a6 100644
Binary files a/docs/screenshots/desktop-interior-night.png and b/docs/screenshots/desktop-interior-night.png differ
diff --git a/docs/screenshots/desktop-interior-overhead.png b/docs/screenshots/desktop-interior-overhead.png
index 16924d7..d7e5a64 100644
Binary files a/docs/screenshots/desktop-interior-overhead.png and b/docs/screenshots/desktop-interior-overhead.png differ
diff --git a/docs/screenshots/desktop-interior.png b/docs/screenshots/desktop-interior.png
index 65fd822..ac6a76f 100644
Binary files a/docs/screenshots/desktop-interior.png and b/docs/screenshots/desktop-interior.png differ
diff --git a/docs/screenshots/mobile-exterior.png b/docs/screenshots/mobile-exterior.png
index f88fd92..93b9dc3 100644
Binary files a/docs/screenshots/mobile-exterior.png and b/docs/screenshots/mobile-exterior.png differ
diff --git a/docs/screenshots/mobile-interior.png b/docs/screenshots/mobile-interior.png
index 72eaf1b..8420958 100644
Binary files a/docs/screenshots/mobile-interior.png and b/docs/screenshots/mobile-interior.png differ
diff --git a/docs/screenshots/mobile-panel.png b/docs/screenshots/mobile-panel.png
index b935c1a..db4169c 100644
Binary files a/docs/screenshots/mobile-panel.png and b/docs/screenshots/mobile-panel.png differ
diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png
index 1be84ad..516a14d 100644
Binary files a/docs/screenshots/overview.png and b/docs/screenshots/overview.png differ
diff --git a/tools/screenshots.mjs b/tools/screenshots.mjs
index 27c8ded..826f318 100644
--- a/tools/screenshots.mjs
+++ b/tools/screenshots.mjs
@@ -95,12 +95,29 @@ async function drag(page, [fx, fy], [tx, ty]) {
   await page.mouse.up()
 }
 
-/** Open one of the three views by clicking its button, as a reader would. */
+/**
+ * Open one of the three views by clicking its button, as a reader would.
+ *
+ * Waits for the view to exist rather than for a guess at how long it takes. Three
+ * is a deferred import, so both 3D views appear some hundreds of milliseconds
+ * after the click — and a drag begun before the renderer has attached its pointer
+ * handlers is silently half-swallowed. That is not hypothetical: it is how
+ * interior-overhead came out pitched +13° when its drag is sized for +45°, and it
+ * had been doing so quietly for several runs behind a 2.5 s timeout that was
+ * usually but not always long enough.
+ *
+ * Both readouts are rendered only once their view is live, so a non-empty one is
+ * the signal — no app change needed to expose it.
+ */
 async function openView(page, view) {
   await page.click(`#views button[data-view="${view}"]`)
-  // Three.js is a deferred import, so the first exterior or interior open is a
-  // network fetch and a shader compile before there is anything to photograph.
-  await page.waitForTimeout(view === 'map' ? 400 : 2500)
+  if (view !== 'map') {
+    await page.waitForFunction(
+      (sel) => (document.querySelector(sel)?.textContent ?? '').length > 0,
+      view === 'exterior' ? '#spin' : '#standing',
+      { timeout: 30_000 })
+  }
+  await page.waitForTimeout(view === 'map' ? 400 : 700)
   await settle(page)
 }
 
@@ -177,13 +194,14 @@ const SHOTS = [
   },
   {
     id: 'exterior',
-    caption: 'Exterior — hull, checkered end caps and the collector, terrain wrapped on as an x-ray',
+    caption: 'Exterior, x-ray hull — the inner surface projected out, with the caps and the collector',
     on: ['desktop', 'mobile'],
     run: async (page) => {
       await openView(page, 'exterior')
       // Spin off: a turning Tower photographs differently every run, and the value
       // of these images is that a difference means something.
       await withPanel(page, () => page.uncheck('#f-spin'))
+      await withPanel(page, () => page.selectOption('#f-ext-view', 'xray'))
       const s = await stage(page)
       await drag(page, [s.x + s.w * 0.5, s.y + s.h * 0.5], [s.x + s.w * 0.5 - 150, s.y + s.h * 0.5 + 70])
       // The default view sits about 1,200 km out, where Tower is a sliver. Close in
@@ -197,6 +215,26 @@ const SHOTS = [
       await settle(page, 3000)
     },
   },
+  {
+    id: 'exterior-true',
+    caption: 'Exterior, true hull — bare skin, and the land visible in through the far cap window',
+    on: ['desktop'],
+    run: async (page) => {
+      await openView(page, 'exterior')
+      await withPanel(page, () => page.uncheck('#f-spin'))
+      await withPanel(page, () => page.selectOption('#f-ext-view', 'true'))
+      // Swing round to the far cap: the window is the whole point of this mode and
+      // it is at one specific end of a 540 km object.
+      await pick(page, 'The Far Cap')
+      const s = await stage(page)
+      await page.mouse.move(s.x + s.w * 0.5, s.y + s.h * 0.5)
+      for (let i = 0; i < 3; i++) {
+        await page.mouse.wheel(0, -400)
+        await page.waitForTimeout(120)
+      }
+      await settle(page, 3000)
+    },
+  },
   {
     id: 'interior',
     caption: 'Interior — standing at The Narrows, the ground curving up on both sides',
@@ -217,10 +255,14 @@ const SHOTS = [
       const s = await stage(page)
       /**
        * Dragging down pitches the view up. The interior turns at 0.0032 rad per
-       * pixel at the default 60° field, so ~245 px is 45° — high enough to have the
-       * far side of the world in frame, low enough to keep the ground in it too.
+       * pixel at the default 60° field, so 164 px is 30°.
+       *
+       * It was 245 px / 45° until the far cap became a window. At 45° the frame is
+       * almost all haze — which is honest, and a poor picture of the claim in the
+       * caption. 30° keeps the ground's edge curving up on one side and the cap on
+       * the other, which is what the shot is actually for.
        */
-      await drag(page, [s.x + s.w * 0.5, s.y + s.h * 0.32], [s.x + s.w * 0.5, s.y + s.h * 0.32 + 245])
+      await drag(page, [s.x + s.w * 0.5, s.y + s.h * 0.32], [s.x + s.w * 0.5, s.y + s.h * 0.32 + 164])
       await page.check('#f-rain')
       await settle(page, 1600)
     },
@@ -237,6 +279,19 @@ const SHOTS = [
       await settle(page)
     },
   },
+  {
+    id: 'interior-night',
+    caption: 'Interior — the far cap window at night: stars, and the beam banked to a thousandth',
+    on: ['desktop'],
+    run: async (page) => {
+      await openView(page, 'interior')
+      // The Capital is 270 km from the far cap, which is far enough to see the
+      // whole 100 km window and near enough that some of the sky survives the air.
+      await pick(page, 'The Capital')
+      await withPanel(page, () => page.selectOption('#f-light', 'night'))
+      await settle(page, 1600)
+    },
+  },
   {
     id: 'reader',
     caption: 'Reader mode (?reader=1) — terrain, places, a detail card and the probe, no editing',