fuzzyworld

tower / cascadia / files / README.md

Branch cascadia · default branch · all branches

README.md

Blame: the commit that last changed each line · file · history

cf4f8772 2026-09-07 Pavel Safronov1# Tower
2
3**Live map — <https://pavelsafronov.gitlab.io/tower/>** · [Source](https://gitlab.com/PavelSafronov/tower)
4
5Companion piece to a novel set inside **Tower** — a fictional smart cylinder orbiting Earth,
6spun for 1 g, with an inner surface the size of Wisconsin, lit by 54 TW of sunlight
769726ea 2026-09-08 Pavel Safronov7piped down its axis and scattered by mirrors that make its days and nights. Its wall
8is a kilometre of sealed hull: no windows along all 540 km, and no view of the Earth
9c891d2e 2026-09-08 Pavel Safronov9it orbits. Both end caps are windows, though — 100 km of glass each, and the only
10places in the world with a horizon that is not Tower. Nobody aboard can see the sun
11either: the 244 km collector sits between Tower and it, 79 times its diameter, so
49b0d64d 2026-09-08 Pavel Safronov12the sun cap looks out at a black disc and the stars around it — black except at its
13edge, where the structure holding it flat catches the sun side-on and traces the
14whole 244 km in a thin sun-coloured ring.
cf4f8772 2026-09-07 Pavel Safronov15
28f9a6bc 2026-09-07 Pavel Safronov16Explore Tower from outside, pick a spot on the inner surface, and stand there — with
17the ground curving up on both sides and the far side of the world hanging 100 km
18overhead, clearer than the ground at your feet. Turn its sun up to Earth clear-sky
769726ea 2026-09-08 Pavel Safronov19noon, down to permanent dusk, or shut the beam off entirely and watch the sky come
20out: through the far cap the stars wheel once every 7 min 29 s about a single
21motionless star on the axis. Nothing inside Tower rotates, because you and the
22ground and the air all turn together — so that window is the only place the spin is
23visible at all.
cf4f8772 2026-09-07 Pavel Safronov24
25Tower flies all 78 billion mirrors of its own sunlight, individually, because
26nothing passive would stay in the beam past mid-afternoon. It is a person.
27
28## Status
29
28f9a6bc 2026-09-07 Pavel Safronov30**All three views are built** — map, exterior and interior — over an authored
31geography and a working writer mode.
cf4f8772 2026-09-07 Pavel Safronov32
28f9a6bc 2026-09-07 Pavel Safronov33Built: the unrolled surface map, Tower from outside, standing inside it, place
34editing for thirteen kinds (points, lines, polygons and bands), localStorage
35autosave, backup and static exports, import, permalinks, and the sightline probe.
cf4f8772 2026-09-07 Pavel Safronov36
37- [design.md](design.md) — canon, physics and application architecture
38- [decisions.md](decisions.md) — choices made and why
39
40Both are series-bible material: coherence outranks spectacle, and the prose is
41downstream of them.
42
fc11b064 2026-09-08 Pavel Safronov43On a phone the sidebar is a **drawer** rather than a column: below 720 px the world
44gets the whole screen and the panel slides in over it, one tap away and one tap
45gone. Picking a place closes it, because in every view selecting a place is how you
46travel there — see [decisions.md](decisions.md) D-043.
47
6364748a 2026-09-08 Pavel Safronov48## Screenshots
49
50[![All ten views on one sheet](docs/screenshots/overview.png)](docs/screenshots/overview.png)
51
52`npm run screenshots` drives the real application in a headless browser — clicking
53the view buttons, picking places out of the list, dragging to orbit and to look up —
54and writes [`docs/screenshots/`](docs/screenshots/). Taking them *through the UI* is
55the point: a shot taken this way fails when the UI fails, so a black canvas ends up
56in the picture instead of hiding behind a passing unit test.
57
18958d3d 2026-09-08 Pavel Safronov58The whole tour takes about **35 seconds**. It used to take six and a half minutes,
59nearly all of it spent rendering Tower's haze integral on the CPU — the default
60headless browser has no GPU. It now asks for the full Chrome build, which reaches
61the real one, and photographs four shots at a time, since a shot is mostly spent
62waiting. The cost is that the committed images are tied to the GPU that drew them:
63another machine will show a large one-off divergence rather than a regression.
64
6364748a 2026-09-08 Pavel Safronov65| Desktop — 1440×900 | |
66|---|---|
67| [map](docs/screenshots/desktop-map.png) | the unrolled inner surface, with a place selected |
68| [map-sightline](docs/screenshots/desktop-map-sightline.png) | shift-click two points to measure a sightline through the air |
4699e6cb 2026-09-08 Pavel Safronov69| [exterior](docs/screenshots/desktop-exterior.png) | x-ray hull — the inner surface projected out, with the caps and the collector |
70| [exterior-true](docs/screenshots/desktop-exterior-true.png) | true hull — bare skin, and the land visible through the far cap window |
6364748a 2026-09-08 Pavel Safronov71| [interior](docs/screenshots/desktop-interior.png) | standing at The Narrows, the ground curving up on both sides |
72| [interior-overhead](docs/screenshots/desktop-interior-overhead.png) | looking up: the far side 100 km overhead is clearer than ground 100 km away |
2bb3e0de 2026-09-08 Pavel Safronov73| [interior-dawn](docs/screenshots/desktop-interior-dawn.png) | dawn — a lit sky over dark ground, the light still 27 km away |
6364748a 2026-09-08 Pavel Safronov74| [reader](docs/screenshots/desktop-reader.png) | `?reader=1` — terrain, places, a detail card and the probe, no editing |
75
76| Mobile — 390×844 | |
77|---|---|
78| [map](docs/screenshots/mobile-map.png) | the world with the whole screen, the panel one tap away |
79| [exterior](docs/screenshots/mobile-exterior.png) | the spin readout dropped clear of the panel button |
80| [interior](docs/screenshots/mobile-interior.png) | the standing readout at a size that fits 390 px |
81| [panel](docs/screenshots/mobile-panel.png) | the sidebar as a drawer, opened over the world |
82
83### Looking before committing
84
85[`overview.png`](docs/screenshots/overview.png) is a contact sheet of all ten. It
86exists to be **looked at, once, before committing** — one image that answers "is the
87application still there?". Regenerate, glance at it, then commit.
88
89`--check` puts a number on the same question. Every shot is compared against the
90committed one and the run fails if any of them moved too far:
91
92```
93npm run screenshots -- --check # fail past 3% divergence on any shot
94npm run screenshots -- --threshold 8 # report only, looser bar
95npm run screenshots -- --url http://localhost:5173 # use a dev server already up
96```
97
18958d3d 2026-09-08 Pavel Safronov98The threshold is measured at both ends, not guessed. The 2D shots come out
99**byte-identical** between runs — nothing in Tower is stochastic, the exterior spin
100is switched off, and every shot gets a fresh browser context so no camera leaks into
101the next. **The WebGL shots do not**, because the tour draws on the real GPU and
102Metal does not schedule floating-point work identically twice; they drift by 0.1% to
103**0.9%**. Against that, hiding both 3D canvases scored **7% to 31%**, and a view
104going entirely black scores 12% to 39%.
6364748a 2026-09-08 Pavel Safronov105
106**3% is three times the worst noise and less than half the mildest real failure.**
107It is a smoke alarm, not a verdict: when it fires, go and look at the sheet. Note
108that a view which never worked scores 0% when it breaks further — this catches
109regressions, not things that were already broken.
110
cf4f8772 2026-09-07 Pavel Safronov111## Development
112
113```
114npm install
115npm run dev # http://localhost:5173
1affced2 2026-09-08 Pavel Safronov116npm test # 104 tests; canon figures are pinned to design.md
cf4f8772 2026-09-07 Pavel Safronov117npm run build
118npm run terrain # regenerate the maps from content/geography.json
1affced2 2026-09-08 Pavel Safronov119npm run cascadia # regenerate the 1:1 Cascadia surface (see below)
6364748a 2026-09-08 Pavel Safronov120npm run screenshots # drive the app and rephotograph docs/screenshots/
cf4f8772 2026-09-07 Pavel Safronov121```
122
123Vite + TypeScript, deployed to GitLab Pages by `.gitlab-ci.yml` on every push to the
28f9a6bc 2026-09-07 Pavel Safronov124default branch. Three.js is loaded only when a 3D view is opened, so a reader who
125stays on the map never downloads it.
cf4f8772 2026-09-07 Pavel Safronov126
4a8aed84 2026-09-07 Pavel Safronov127Assets are referenced **relatively**, so the same build works whether Pages serves
128it from `https://pavelsafronov.gitlab.io/tower/` or from a generated unique domain
129at a root. Set a `BASE` CI variable to force an absolute base if that is ever
130needed.
cf4f8772 2026-09-07 Pavel Safronov131
132## Geography
133
134[`content/geography.json`](content/geography.json) is the authored source of truth
28f9a6bc 2026-09-07 Pavel Safronov135for terrain — coordinates in canon (θ, z), elevations in metres relative to the base
136shell, negative for anything cut below it. `render.minHeightM` / `render.maxHeightM`
137declare the range the height raster is stretched across; the generator prints the
138range the terrain actually occupies and warns if it has outgrown the declaration. `npm run terrain` renders it, with no dependencies beyond Node:
cf4f8772 2026-09-07 Pavel Safronov139
140| | |
141|---|---|
28f9a6bc 2026-09-07 Pavel Safronov142| `static/terrain-height.png` | 16-bit greyscale, across the declared `minHeightM`–`maxHeightM` |
62458672 2026-09-07 Pavel Safronov143| `static/terrain-color.png` | the whole world, 264 m/px |
144| `static/terrain-corridor.png` | the developed corridor, 122 m/px |
145| `static/world.json` | seed places for the app |
cf4f8772 2026-09-07 Pavel Safronov146
147It reports each stage as it goes — every relief op, colourisation, and PNG encode —
148and takes about 6 seconds. Flags:
149
150| | |
151|---|---|
152| `--probe <theta>` | print elevation jumps along a row, for checking river gradients and finding discontinuities |
153| `--quiet` | one line per stage instead of one per feature |
154| `--force` | let the seed overwrite places added in the app |
155
1affced2 2026-09-08 Pavel Safronov156### The Cascadia surface — a ruler
157
158The panel's **Surface** control can swap the authored ground for **the Pacific
159Northwest at 1:1**, to answer the question a number cannot: *how big is this
160actually?*
161
162It fits because Tower is a corridor and so is Cascadia. Measured in UTM 10N, the
163ground from the Pacific surf at Kalaloch east to the Columbia at Entiat is
164**312.8 km against Tower's 314.2 km circumference** — so the real coast closes the
165ring on real terrain, with 1.4 km to spare. Walk east from the beach, over the
166Olympics, across Puget Sound, over the Cascades and down the east flank to the
167Columbia, and you are back at the beach. Seattle sits 150 km deepward of the sun
168cap, and the point 100 km straight overhead is *both* Kalaloch and Entiat — one
169place, reached by walking either way round. Nothing is rescaled and nothing is
170invented.
171
172**It is not canon**, contains no Tower geography, defaults off, and draws no
173places. The one artefact is a seam where θ wraps and geography does not — a scarp
174of 369–844 m through the settled half, kept as a landform rather than repaired.
175See D-055 for the alternatives that were measured and rejected.
176
177```
178npm run cascadia # ~2 s from cache, ~5 s cold
179```
180
181| | |
182|---|---|
183| `static/cascadia-height.png` | 16-bit greyscale, sharing canon's `-560`–`4400` m window |
184| `static/cascadia-color.png` | the same 2048 x 1192 grid, 264 m/px |
185
186Elevation comes from the AWS open terrain tiles — SRTM/3DEP over land, GEBCO below
187water, Canadian ground north of 49°N in the same grid — cached under `.cache/`,
188re-fetchable, and not committed. There are no dependencies beyond Node; the UTM
189projection and the PNG decoder are in the repo.
190
191Every surface **must** share the raster grid and height window: the interior view
192bakes that window into its GLSL as literals, so a surface declaring its own would
193silently displace its ground. `test/raster.test.ts` reads the constants back out of
194`tools/cascadia.mjs` and holds it to them.
195
cf4f8772 2026-09-07 Pavel Safronov196## Reading and writing
197
198The map opens in **writer mode**: tools, editor, canon controls and import/export.
199Add **`?reader=1`** to preview what a reader gets — the terrain, the places, a
200detail card and the sightline probe, with no editing. Writer is the default while
201the world is still being built; that is a one-line change when it is ready to
202publish. Either way it is a UI mode, not a security boundary — with no backend an
203editor can only change their own copy (decisions.md D-011).
204
205Any selection updates the URL, so a link is always shareable:
206
207| | |
208|---|---|
209| `#place=the-narrows` | select and centre a place |
210| `#at=0.03,170000` | drop a sightline endpoint at (θ, z) |
211
5f38efd0 2026-09-07 Pavel Safronov212## Views
213
a3f01bd7 2026-09-08 Pavel Safronov214**Map** is the unrolled inner surface. **Exterior** is Tower from outside — a
215kilometre of hull, the checkered sun cap, the glazed far cap, the 244 km collector
216flying ahead and the beam converging into the sun cap. Each piece is captioned.
217
218The hull reads two ways and the **Hull** switch picks one. **True** is the outer
219skin: bare structure, and the far cap a real window — look in through it and the
220land is actually there, lit and receding 540 km down the tube. It is the only line
221of sight into Tower that is not a diagram. **X-ray** wraps the terrain onto the
222outside instead, which is a diagram of what is inside rather than a view of it, and
223useful precisely because the wall is opaque. The active view is remembered and travels in the permalink, so
8b6287ba 2026-09-07 Pavel Safronov224`#view=exterior&place=the-narrows` opens where you left off. Drag to orbit, scroll to zoom, click the hull to drop a point; the spin runs at
5f38efd0 2026-09-07 Pavel Safronov2251x to 1200x real time, and a point picked off the turning hull still comes back in
226canon coordinates.
227
28f9a6bc 2026-09-07 Pavel Safronov228**Interior** is standing inside. The shell is the real cylinder, displaced by the
229height raster and painted with the colour one, so the ground curves up on both sides
230and closes overhead. Haze is integrated through Tower's rotational air column for
231every pixel, which reproduces the strangest fact about the place: the land 100 km
232straight up is **78%** clear while the ground 100 km away is **31%** — distant
233geography here is best seen by looking up.
234
235Drag to look, scroll to zoom in like a pair of binoculars, and select a place — in
236the list or by its caption — to **walk there**. Most of the world is beyond the haze
237horizon from any given spot, so travelling is more use than turning.
238
769726ea 2026-09-08 Pavel Safronov239**The far cap is a window** — 100 km of it, looking into deep anti-sun space. By day
240it is simply a disc of blue, because 540 km of air in front of it scatters more
241light than anything behind it sends; the stars are there at noon and unseeable,
242which is a fact about eyes rather than about glass. Turn the light down and they
243come out. The sun cap stays a checkered plate: it faces the sun, and needs glare
244management rather than a view.
245
da6bcb0e 2026-09-08 Pavel Safronov246The one part of the far cap that is not glass is a **3.2 km mirror** on the axis.
247A quarter of the beam survives all 540 km and arrives collimated, so it lands as
2481.7 MW/m² — 1,200 suns — on a spot a thousandth of the cap's area; the mirror sends
249it back up the column for a second pass, which lifts the beam's utilisation from 76%
250to 94%. You can see it: the rod of light visibly terminates on a bright disc, and
251the disc banks and goes out with the beam.
252
769726ea 2026-09-08 Pavel Safronov253The checker survives on the far cap as the window's **mullions** — the same polar
254tessellation at the same pitch, so it is still the only object in the world with a
255known cell size and still the only rule anyone has for judging distance. It is just
256read against sky now instead of against a plate.
257
2bb3e0de 2026-09-08 Pavel Safronov258**Dawn is a line, not a fade.** The beam glows by itself, so when its power comes up
259the sky brightens everywhere at once — but the ground is lit by the diffusers, and
260they hand the light down the axis. So a Tower morning is a **fully lit sky over dark
261ground**, with daylight arriving as a front running deepward from the sun cap at
262225 m/s, crossing all 540 km in 40 minutes. Tower has no latitude; the cycle gives it
263longitude. Scrub the **Time** slider to 06:17 and stand at The Capital to watch it
264come, or tick **Run the clock** and let it.
265
266The light has five settings when the clock is not driving it. **Dark** shuts the 54 TW beam down entirely and is the
769726ea 2026-09-08 Pavel Safronov267only one that is genuinely black — there is no airlight, no lit ground and no rod
268overhead, and the only thing left in the world is the sky. **Night** banks the beam
269to a thousandth instead of killing it, which is what night here has to be: the beam
270glows by itself, so a lit beam lights the world whatever the mirrors are doing, and
271aiming them away would leave the floor at about 100 W/m² — half of noon.
272
567fe8ad 2026-09-08 Pavel Safronov273The eye-height slider runs from 1.7 m to 25 km with the local gravity beside it, and
274**Rain** leans 1.5° antispinward. Nothing *inside* rotates, because you, the ground
275and the air all turn together — so for 539 of the 540 km that 1.5° is the only
276evidence Tower turns at all. The exception is the window: the sky out there does not
277co-rotate, and wheels once every 7 min 29 s about the one motionless star on the
278axis. It is by a wide margin the plainest sign of the spin anywhere in the view, and
769726ea 2026-09-08 Pavel Safronov279it is only available at night, at the deepward end, looking the one direction that
280has ever had anything in it but Tower.
28f9a6bc 2026-09-07 Pavel Safronov281
cf4f8772 2026-09-07 Pavel Safronov282## Working in the map
283
284Pick a tool, click to place; **Lake**, **Forest**, **Desert** and **Region** trace a
285polygon and **River** and **Canyon** trace an open line (Enter finishes, Esc
286cancels). Drag markers to move them, **shift-click two points** to
287measure a sightline, **f** to fit the view.
288
cf37225c 2026-09-07 Pavel Safronov289Canon is **0 star slats** — the hull is sealed, the inner surface is one unbroken
290169,646 km², and the only views out are at the end caps. The runtime control for
291this is disabled and hidden (decisions.md D-033): the terrain raster is baked at 0
292slats, so any other count draws bands over land that has none.
cf4f8772 2026-09-07 Pavel Safronov293
294Everything autosaves to this browser's localStorage. Two exports:
295
296| | |
297|---|---|
298| **Export backup** | everything, private notes included. Keep out of the public repo — `.gitignore` already excludes `tower-backup-*.json`. |
62458672 2026-09-07 Pavel Safronov299| **Export static map content** | public places only, `notes` stripped. This is `static/world.json`, the world readers see, and the seed for a browser with no local draft. |