fuzzyworld

tower / commit

feat(interior): Tower keeps its clock on the collector

17ccee99b35ae8e4033a1f1f795099fed3553b36 · Pavel Safronov · 2026-09-08 09:41:00 -0700 · parents 49b0d64d13b0

feat(interior): Tower keeps its clock on the collector

Two lit lines from the collector's hub: one at the full radius, fixed normal
to the plane of the solar system, and one stopping short of the rim at an
angle theta from it, theta running 0 to 2pi over a day.

Why there. Everything inside Tower co-rotates with the hull, so nothing
inside can carry a fixed reference, and everything outside wheels once every
7 min 29 s. The collector is the exception twice over — it flies in
formation rather than spinning, and it never moves in the sky, being always
dead ahead through the sun cap window at 41.8° wide. It is the only surface
in the world worth writing on.

Why two lines rather than a hand and a dial, which turns out to be forced
rather than decorative. From inside, the collector's whole face appears to
rotate at Tower's spin, because the observer is turning and it is not. A
single mark against a fixed background would be useless, since the
background is not fixed — but the angle between two marks is invariant under
that rotation. It is a clock for people who cannot stop the dial turning,
and the design falls straight out of the constraint. The marks are read in
the collector's frame, so the shader adds uSpin to the observed azimuth
exactly as the star field does; both wheel together, because both are
outside.

Lit deliberately rather than found: the collector's back is unlit and 245 K,
so nothing on it would be visible by accident. About 0.5 GW to read against
the haze, one part in a hundred thousand of the beam.

You cannot always read it. From 270 km the sun cap is 96% airlight by day,
so the clock washes out exactly as the far cap's stars do — legible at
night, or from near the cap. That is §1.5 doing what it has always done
rather than a defect in the instrument.

The sun cap shot is now driven from the clock rather than the Light menu, so
the picture agrees with itself: it had been showing a dark world under a
clock reading noon.

Not in the exterior view yet, which has no time of day at all — recorded in
D-055 as a disagreement between the two views rather than left to be found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
 README.md                                      |   8 +++++
 decisions.md                                   |  38 +++++++++++++++++++++
 design.md                                      |  13 +++++++
 docs/screenshots/desktop-exterior-true.png     | Bin 506739 -> 506644 bytes
 docs/screenshots/desktop-exterior.png          | Bin 332293 -> 332426 bytes
 docs/screenshots/desktop-interior-dawn.png     | Bin 488654 -> 489635 bytes
 docs/screenshots/desktop-interior-night.png    | Bin 275988 -> 277170 bytes
 docs/screenshots/desktop-interior-overhead.png | Bin 544184 -> 544181 bytes
 docs/screenshots/desktop-interior-suncap.png   | Bin 397844 -> 401306 bytes
 docs/screenshots/desktop-interior.png          | Bin 545739 -> 545753 bytes
 docs/screenshots/mobile-exterior.png           | Bin 81707 -> 81716 bytes
 docs/screenshots/overview.png                  | Bin 1155305 -> 1154824 bytes
 src/canon/constants.ts                         |  28 +++++++++++++++
 src/views/interior.ts                          |  45 ++++++++++++++++++++++++-
 tools/screenshots.mjs                          |  24 +++++++++++--
 15 files changed, 152 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 8f4d9a2..e3432ab 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,12 @@ the sun cap looks out at a black disc and the stars around it — black except a
 edge, where the structure holding it flat catches the sun side-on and traces the
 whole 244 km in a thin sun-coloured ring.
 
+**Tower's clock is painted on it**, because it is the only thing in the sky that
+does not turn. Two lit lines from the hub: one fixed normal to the plane of the
+solar system, one sweeping a full turn over a day. You read the *angle between
+them* — from inside, the whole face wheels once every 7 min 29 s along with the
+stars, so a hand against a dial would be useless, but an angle survives it.
+
 Explore Tower from outside, pick a spot on the inner surface, and stand there — with
 the ground curving up on both sides and the far side of the world hanging 100 km
 overhead, clearer than the ground at your feet. Turn its sun up to Earth clear-sky
@@ -70,6 +76,8 @@ another machine will show a large one-off divergence rather than a regression.
 | [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 |
+| [interior-night](docs/screenshots/desktop-interior-night.png) | the far cap window at night: stars, and the beam banked to a thousandth |
+| [interior-suncap](docs/screenshots/desktop-interior-suncap.png) | the sun cap window: the collector, its sunlit rim, and Tower's clock on its face |
 | [interior-dawn](docs/screenshots/desktop-interior-dawn.png) | dawn — a lit sky over dark ground, the light still 27 km away |
 | [reader](docs/screenshots/desktop-reader.png) | `?reader=1` — terrain, places, a detail card and the probe, no editing |
 
diff --git a/decisions.md b/decisions.md
index 29c1d02..98b1cfe 100644
--- a/decisions.md
+++ b/decisions.md
@@ -1644,3 +1644,41 @@ seven times too wide, which washed the whole disc gold instead of tracing it.
 lamp, so the rim is outside `uExposure`, like the stars. It does fade as the eye
 adapts, but from a much higher floor than a star's — this is sunlit structure, and
 it stays faintly visible at noon where a star cannot.
+
+## D-055 — The clock is on the collector, and it is read as an angle
+
+Two lit lines from the collector's hub: one at the full radius, fixed normal to the
+plane of the solar system, and one stopping short of the rim at an angle θ from it,
+θ running 0 to 2π over one day.
+
+**Why there.** Everything inside Tower co-rotates with the hull, so nothing inside
+can carry a fixed reference. Everything outside appears to wheel once every 7 min
+29 s. The collector is the exception twice over: it flies in formation rather than
+spinning, and it never moves in the sky — it is always dead ahead through the sun
+cap window, 41.8° wide. It is the only surface in the world worth writing on.
+
+**Why two lines rather than a hand and a dial**, which is the part that turns out to
+be forced rather than decorative. From inside, the collector's whole face appears to
+rotate at Tower's spin, because the observer is turning and it is not. A single mark
+against a fixed background would be useless — the background is not fixed. But the
+*angle between two marks* is invariant under that rotation, so a pair of lines
+survives what neither line does alone. It is a clock designed for people who cannot
+stop the dial turning, and the design falls straight out of the constraint.
+
+The marks are read in the collector's own frame, so the shader adds `uSpin` to the
+observed azimuth exactly as the star field does. Both wheel together, which is
+correct: they are both outside.
+
+**Lit deliberately**, not found. The collector's back is unlit and 245 K; nothing on
+it would be visible by accident. It is an instrument, so Tower runs it — about
+0.5 GW to read against the haze, which is one part in a hundred thousand of the
+beam, less than the swarm spends holding its own station.
+
+**You cannot always read it.** From 270 km the sun cap is 96% airlight by day, so
+the clock is washed out exactly as the far cap's stars are. It is legible at night,
+or from near the sun cap. That is the same fact §1.5 has been making all along and
+not a defect in the instrument.
+
+**Not yet in the exterior view**, which has no time of day at all — its collector is
+a flat disc with no clock on it. Giving it one means giving that view the day model
+first, so the two views disagree about the same object until then.
diff --git a/design.md b/design.md
index bdbbe05..813a3cf 100644
--- a/design.md
+++ b/design.md
@@ -114,6 +114,19 @@ anything backlit looks. **It is not sunlight leaking past the edge**: Tower is w
 inside the umbra, so the sun's disc is hidden from every point in the world. The ring
 is lit structure. D-054.
 
+**And Tower keeps its clock on it.** Two lit lines from the collector's hub: one
+running the full radius, fixed normal to the plane of the solar system, and one
+stopping short of the rim at an angle θ from it, where θ runs 0 to 2π over a day.
+The collector is the only surface in the sky worth writing on — it does not spin,
+and it is always dead ahead through the sun cap window.
+
+**The time is the angle between the two lines, and it has to be.** From inside, the
+whole collector face appears to wheel once every 7 min 29 s along with the stars,
+because the observer co-rotates and it does not. A single hand against a fixed dial
+would be unreadable, since the dial is not fixed; the angle between two marks is
+invariant under that rotation. It is a clock built for people who cannot stop the
+dial turning. D-055.
+
 #### 1.3.1 The diffuser swarm
 
 The diffusers must be individually invisible, or the sky becomes a row of separate
diff --git a/docs/screenshots/desktop-exterior-true.png b/docs/screenshots/desktop-exterior-true.png
index ef7e6af..219fcbe 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 7810fb6..a4ffec4 100644
Binary files a/docs/screenshots/desktop-exterior.png and b/docs/screenshots/desktop-exterior.png differ
diff --git a/docs/screenshots/desktop-interior-dawn.png b/docs/screenshots/desktop-interior-dawn.png
index cee0a2a..781c0e4 100644
Binary files a/docs/screenshots/desktop-interior-dawn.png and b/docs/screenshots/desktop-interior-dawn.png differ
diff --git a/docs/screenshots/desktop-interior-night.png b/docs/screenshots/desktop-interior-night.png
index 7e05589..9b69dbc 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 e911380..26adac4 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-suncap.png b/docs/screenshots/desktop-interior-suncap.png
index b17d8da..cabb7f4 100644
Binary files a/docs/screenshots/desktop-interior-suncap.png and b/docs/screenshots/desktop-interior-suncap.png differ
diff --git a/docs/screenshots/desktop-interior.png b/docs/screenshots/desktop-interior.png
index 9ab8ec9..ae3a000 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 2fa380c..506c583 100644
Binary files a/docs/screenshots/mobile-exterior.png and b/docs/screenshots/mobile-exterior.png differ
diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png
index bdeb741..313871b 100644
Binary files a/docs/screenshots/overview.png and b/docs/screenshots/overview.png differ
diff --git a/src/canon/constants.ts b/src/canon/constants.ts
index e9e3ec9..eb11458 100644
--- a/src/canon/constants.ts
+++ b/src/canon/constants.ts
@@ -97,6 +97,34 @@ export const COLLECTOR_ANGULAR_DIAMETER_RAD =
  */
 export const COLLECTOR_RIM_M = 4000
 
+// ------------------------------------------------ the collector clock (D-055)
+/**
+ * Tower's clock is painted on the outside of the world, because the collector is
+ * the only thing in its sky that does not turn.
+ *
+ * Everything inside co-rotates with the hull, and everything outside appears to
+ * wheel once every 7 min 29 s (§1.4's pinwheel). The collector is the exception
+ * twice over: it flies in formation rather than spinning, and it is always in the
+ * same place — dead ahead through the sun cap window. That makes it the one surface
+ * in the world worth writing on.
+ *
+ * **It is read as an angle, not as a hand against a dial**, and it has to be. From
+ * inside, the whole face appears to rotate at Tower's spin, so any single mark is
+ * useless — but the angle *between* two marks is invariant under that rotation.
+ * Hence one fixed line and one moving one: a clock for people who cannot stop the
+ * dial turning.
+ */
+export const COLLECTOR_CLOCK_MARK_M = 2200
+/** The hand stops short of the rim, so it is never confused with the lit edge. */
+export const COLLECTOR_CLOCK_HAND_FRACTION = 0.72
+/**
+ * Lit deliberately rather than found: it is an instrument, so Tower runs it. A
+ * 2.2 km line down a 122 km radius is ~5.4e8 m²; at the ~1 W/m² it takes to read
+ * against the haze that is **0.5 GW**, which against 54.3 TW is one part in a
+ * hundred thousand. The swarm spends more than that holding its own station.
+ */
+export const COLLECTOR_CLOCK_POWER_W = 0.5e9
+
 /**
  * Umbra radius at the sun cap. Tower's 51 km sits well inside it, which is what
  * makes the eclipse total rather than partial.
diff --git a/src/views/interior.ts b/src/views/interior.ts
index 6650c5e..b200e6a 100644
--- a/src/views/interior.ts
+++ b/src/views/interior.ts
@@ -25,7 +25,8 @@ import {
 } from 'three'
 import {
   BEAM_RADIUS_M, BRIGHTNESS_FULL_W_M2, COLLECTOR_DIAMETER_M, COLLECTOR_STANDOFF_M,
-  COLLECTOR_RIM_M, DAY_LENGTH_S, DAY_SWEEP_FRACTION, BRIGHTNESS_NIGHT_W_M2, BRIGHTNESS_TEMPERATE_W_M2,
+  COLLECTOR_CLOCK_HAND_FRACTION, COLLECTOR_CLOCK_MARK_M, COLLECTOR_RIM_M,
+  DAY_LENGTH_S, DAY_SWEEP_FRACTION, BRIGHTNESS_NIGHT_W_M2, BRIGHTNESS_TEMPERATE_W_M2,
   BRIGHTNESS_TWILIGHT_W_M2, G0, HULL_DEPTH_M, LENGTH_M, OMEGA_RAD_S, RADIUS_M,
   RAYLEIGH_FLOOR_PER_M,
   SPECIFIC_GAS_CONSTANT, TEMPERATURE_K, gravityAtHeight,
@@ -52,6 +53,9 @@ const COLLECTOR_R = COLLECTOR_DIAMETER_M / KM / 2
 const COLLECTOR_Z = -COLLECTOR_STANDOFF_M / KM
 /** Width of the sunlit edge structure, in km (D-054). */
 const COLLECTOR_RIM = COLLECTOR_RIM_M / KM
+/** Clock geometry on the collector face, in km (D-055). */
+const CLOCK_MARK = COLLECTOR_CLOCK_MARK_M / KM
+const CLOCK_HAND_R = COLLECTOR_R * COLLECTOR_CLOCK_HAND_FRACTION
 
 /**
  * Rayleigh extinction per kilometre at floor density, per channel.
@@ -380,9 +384,27 @@ const SUN_CAP_FRAG = /* glsl */`
 #include <logdepthbuf_pars_fragment>
 uniform vec3 uTint;
 uniform sampler2D uGlazing;
+uniform float uClockAngle;
 varying vec3 vWorld;
 ${HAZE_GLSL}
 ${STAR_GLSL}
+
+/**
+ * One radial line on the collector face: from the hub out to rMax, at space angle
+ * a0. Polar rather than cartesian because the hit is already polar — r*sin(da) is
+ * the perpendicular distance to the line, and r*cos(da) is how far along it the
+ * point sits, which doubles as the end test.
+ *
+ * (No backticks in here: this whole shader is a JS template literal.)
+ */
+float radialMark(float r, float a, float a0, float rMax) {
+  float da = a - a0;
+  float along = r * cos(da);
+  if (along < 0.0 || along > rMax) return 0.0;
+  return 1.0 - smoothstep(${(CLOCK_MARK * 0.35).toFixed(3)},
+                          ${CLOCK_MARK.toFixed(3)}, abs(r * sin(da)));
+}
+
 void main() {
   #include <logdepthbuf_fragment>
   vec2 uv = vWorld.xy / ${(2 * R).toFixed(1)} + 0.5;
@@ -404,6 +426,21 @@ void main() {
     sky = mix(sky, vec3(0.0), step(r, ${COLLECTOR_R.toFixed(2)}));
     // Tight core for the structure itself, and a wide dim skirt for the scatter
     // around it. One exponential alone reads as a drawn outline rather than light.
+    /**
+     * The clock (D-055). Both marks are read in the collector's own frame, which
+     * is why the observed azimuth has uSpin added to it exactly as the star field
+     * does: the collector does not co-rotate, so from inside its whole face wheels
+     * once every 7 min 29 s. That is also why the time is the *angle between* the
+     * two lines rather than the position of either — the pair survives a rotation
+     * that neither line does alone.
+     */
+    vec2 hp = vWorld.xy + dir.xy * t;
+    float a = atan(hp.y, hp.x) + uSpin;
+    sky += vec3(0.78, 0.86, 1.0)
+         * (radialMark(r, a, ${(Math.PI / 2).toFixed(5)}, ${COLLECTOR_R.toFixed(2)})
+          + radialMark(r, a, ${(Math.PI / 2).toFixed(5)} + uClockAngle, ${CLOCK_HAND_R.toFixed(2)}))
+         * (0.3 + 0.7 * uStarGain) * 0.55;
+
     float d = abs(r - ${COLLECTOR_R.toFixed(2)});
     float rim = exp(-d / ${COLLECTOR_RIM.toFixed(3)})
               + 0.10 * exp(-d / ${(COLLECTOR_RIM * 3).toFixed(3)});
@@ -615,6 +652,7 @@ export class InteriorView {
       uSpin: { value: 0 },
       uStarGain: { value: 0 },
       // Only the ground declares these; carried here so one loop can set them.
+      uClockAngle: { value: 0 },
       uFrontZ: { value: 0 },
       uFrontWidth: { value: 24 },
       uBelowFront: { value: 1 },
@@ -989,6 +1027,11 @@ export class InteriorView {
      * exponent compresses the canon watts into something a screen can show while
      * keeping the ordering and the ratios honest.
      */
+    // The hand tracks the time of day always — the clock runs whether or not it is
+    // the thing deciding how bright the world is.
+    for (const m of this.shared) {
+      m.uniforms['uClockAngle']!.value = 2 * Math.PI * (((this.dayPhase % 1) + 1) % 1)
+    }
     const day = this.clockDriven ? this.sampleDay() : null
     const watts = day ? day.watts : LIGHTING[this.lighting]
     if (day) {
diff --git a/tools/screenshots.mjs b/tools/screenshots.mjs
index 593361f..4499d73 100644
--- a/tools/screenshots.mjs
+++ b/tools/screenshots.mjs
@@ -290,18 +290,36 @@ const SHOTS = [
       // 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'))
+      /**
+       * Driven from the clock rather than the Light menu, so the picture agrees
+       * with itself: 01:00 gives night lighting *and* puts the collector's hand
+       * 15° off its fixed mark. Setting the menu to night instead left the world
+       * dark under a clock reading noon.
+       */
+      await withPanel(page, async () => {
+        await page.locator('#f-time').fill('60')
+        await page.locator('#f-time').dispatchEvent('input')
+      })
       await settle(page, 1600)
     },
   },
   {
     id: 'interior-suncap',
-    caption: 'Interior — the sun cap window: the collector\'s black disc, and stars round the edge of it',
+    caption: 'Interior — the sun cap window: the collector, its sunlit rim, and Tower\'s clock on its face',
     on: ['desktop'],
     run: async (page) => {
       await openView(page, 'interior')
       await pick(page, 'The Capital')
-      await withPanel(page, () => page.selectOption('#f-light', 'night'))
+      /**
+       * Driven from the clock rather than the Light menu, so the picture agrees
+       * with itself: 01:00 gives night lighting *and* puts the collector's hand
+       * 15° off its fixed mark. Setting the menu to night instead left the world
+       * dark under a clock reading noon.
+       */
+      await withPanel(page, async () => {
+        await page.locator('#f-time').fill('60')
+        await page.locator('#f-time').dispatchEvent('input')
+      })
       // Turn to face sunward. 246 px is 45° at the default field, and the drag has
       // to stay inside the viewport, so it goes round in four passes rather than
       // one 982 px sweep that would run off the edge and drop the pointer capture.