Tower
Live map — https://pavelsafronov.gitlab.io/tower/ · Source
Companion piece to a novel set inside Tower — a fictional smart cylinder orbiting Earth, spun for 1 g, with an inner surface the size of Wisconsin, lit by 54 TW of sunlight piped down its axis and scattered by mirrors that make its days and nights. Its wall is a kilometre of sealed hull: no windows along all 540 km, and no view of the Earth it orbits. Both end caps are windows, though — 100 km of glass each, and the only places in the world with a horizon that is not Tower. Nobody aboard can see the sun either: the 244 km collector sits between Tower and it, 79 times its diameter, so the sun cap looks out at a black disc and the stars around it — black except at its 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 noon, down to permanent dusk, or shut the beam off entirely and watch the sky come out: through the far cap the stars wheel once every 7 min 29 s about a single motionless star on the axis. Nothing inside Tower rotates, because you and the ground and the air all turn together — so that window is the only place the spin is visible at all.
Tower flies all 78 billion mirrors of its own sunlight, individually, because nothing passive would stay in the beam past mid-afternoon. It is a person.
Status
All three views are built — map, exterior and interior — over an authored geography and a working writer mode.
Built: the unrolled surface map, Tower from outside, standing inside it, place editing for thirteen kinds (points, lines, polygons and bands), localStorage autosave, backup and static exports, import, permalinks, and the sightline probe.
- design.md — canon, physics and application architecture
- decisions.md — choices made and why
Both are series-bible material: coherence outranks spectacle, and the prose is downstream of them.
On a phone the sidebar is a drawer rather than a column: below 720 px the world gets the whole screen and the panel slides in over it, one tap away and one tap gone. Picking a place closes it, because in every view selecting a place is how you travel there — see decisions.md D-043.
Screenshots
npm run screenshots drives the real application in a headless browser — clicking
the view buttons, picking places out of the list, dragging to orbit and to look up —
and writes docs/screenshots/. Taking them through the UI is
the point: a shot taken this way fails when the UI fails, so a black canvas ends up
in the picture instead of hiding behind a passing unit test.
The whole tour takes about 35 seconds. It used to take six and a half minutes, nearly all of it spent rendering Tower's haze integral on the CPU — the default headless browser has no GPU. It now asks for the full Chrome build, which reaches the real one, and photographs four shots at a time, since a shot is mostly spent waiting. The cost is that the committed images are tied to the GPU that drew them: another machine will show a large one-off divergence rather than a regression.
| Desktop — 1440×900 | |
|---|---|
| map | the unrolled inner surface, with a place selected |
| map-sightline | shift-click two points to measure a sightline through the air |
| exterior | x-ray hull — the inner surface projected out, with the caps and the collector |
| exterior-true | true hull — bare skin, and the land visible through the far cap window |
| interior | standing at The Narrows, the ground curving up on both sides |
| interior-overhead | looking up: the far side 100 km overhead is clearer than ground 100 km away |
| interior-night | the far cap window at night: stars, and the beam banked to a thousandth |
| interior-suncap | the sun cap window: the collector, its sunlit rim, and Tower's clock on its face |
| interior-flying | flying — 4 km up, off the ground, with the world closing overhead |
| interior-dawn | dawn — a lit sky over dark ground, the light still 27 km away |
| reader | ?reader=1 — terrain, places, a detail card and the probe, no editing |
| Mobile — 390×844 | |
|---|---|
| map | the world with the whole screen, the panel one tap away |
| exterior | the spin readout dropped clear of the panel button |
| interior | the standing readout at a size that fits 390 px |
| panel | the sidebar as a drawer, opened over the world |
Looking before committing
overview.png is a contact sheet of all ten. It
exists to be looked at, once, before committing — one image that answers "is the
application still there?". Regenerate, glance at it, then commit.
--check puts a number on the same question. Every shot is compared against the
committed one and the run fails if any of them moved too far:
npm run screenshots -- --check # fail past 3% divergence on any shot
npm run screenshots -- --threshold 8 # report only, looser bar
npm run screenshots -- --url http://localhost:5173 # use a dev server already up
The threshold is measured at both ends, not guessed. The 2D shots come out byte-identical between runs — nothing in Tower is stochastic, the exterior spin is switched off, and every shot gets a fresh browser context so no camera leaks into the next. The WebGL shots do not, because the tour draws on the real GPU and Metal does not schedule floating-point work identically twice; they drift by 0.1% to 0.9%. Against that, hiding both 3D canvases scored 7% to 31%, and a view going entirely black scores 12% to 39%.
3% is three times the worst noise and less than half the mildest real failure. It is a smoke alarm, not a verdict: when it fires, go and look at the sheet. Note that a view which never worked scores 0% when it breaks further — this catches regressions, not things that were already broken.
Development
npm install
npm run dev # http://localhost:5173
npm test # 94 tests; canon figures are pinned to design.md
npm run build
npm run terrain # regenerate the maps from content/geography.json
npm run screenshots # drive the app and rephotograph docs/screenshots/
Vite + TypeScript, deployed to GitLab Pages by .gitlab-ci.yml on every push to the
default branch. Three.js is loaded only when a 3D view is opened, so a reader who
stays on the map never downloads it.
Assets are referenced relatively, so the same build works whether Pages serves
it from https://pavelsafronov.gitlab.io/tower/ or from a generated unique domain
at a root. Set a BASE CI variable to force an absolute base if that is ever
needed.
Geography
content/geography.json is the authored source of truth
for terrain — coordinates in canon (θ, z), elevations in metres relative to the base
shell, negative for anything cut below it. render.minHeightM / render.maxHeightM
declare the range the height raster is stretched across; the generator prints the
range the terrain actually occupies and warns if it has outgrown the declaration. npm run terrain renders it, with no dependencies beyond Node:
static/terrain-height.png |
16-bit greyscale, across the declared minHeightM–maxHeightM |
static/terrain-color.png |
the whole world, 264 m/px |
static/terrain-corridor.png |
the developed corridor, 122 m/px |
static/world.json |
seed places for the app |
It reports each stage as it goes — every relief op, colourisation, and PNG encode — and takes about 6 seconds. Flags:
--probe <theta> |
print elevation jumps along a row, for checking river gradients and finding discontinuities |
--quiet |
one line per stage instead of one per feature |
--force |
let the seed overwrite places added in the app |
Reading and writing
The map opens in writer mode: tools, editor, canon controls and import/export.
Add ?reader=1 to preview what a reader gets — the terrain, the places, a
detail card and the sightline probe, with no editing. Writer is the default while
the world is still being built; that is a one-line change when it is ready to
publish. Either way it is a UI mode, not a security boundary — with no backend an
editor can only change their own copy (decisions.md D-011).
Any selection updates the URL, so a link is always shareable:
#place=the-narrows |
select and centre a place |
#at=0.03,170000 |
drop a sightline endpoint at (θ, z) |
Views
Map is the unrolled inner surface. Exterior is Tower from outside — a kilometre of hull, the checkered sun cap, the glazed far cap, the 244 km collector flying ahead and the beam converging into the sun cap. Each piece is captioned.
The hull reads two ways and the Hull switch picks one. True is the outer
skin: bare structure, and the far cap a real window — look in through it and the
land is actually there, lit and receding 540 km down the tube. It is the only line
of sight into Tower that is not a diagram. X-ray wraps the terrain onto the
outside instead, which is a diagram of what is inside rather than a view of it, and
useful precisely because the wall is opaque. The active view is remembered and travels in the permalink, so
#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
1x to 1200x real time, and a point picked off the turning hull still comes back in
canon coordinates.
Interior is standing inside. The shell is the real cylinder, displaced by the height raster and painted with the colour one, so the ground curves up on both sides and closes overhead. Haze is integrated through Tower's rotational air column for every pixel, which reproduces the strangest fact about the place: the land 100 km straight up is 78% clear while the ground 100 km away is 31% — distant geography here is best seen by looking up.
Drag to look, scroll to zoom in like a pair of binoculars, and select a place — in the list or by its caption — to walk there. Most of the world is beyond the haze horizon from any given spot, so travelling is more use than turning.
The far cap is a window — 100 km of it, looking into deep anti-sun space. By day it is simply a disc of blue, because 540 km of air in front of it scatters more light than anything behind it sends; the stars are there at noon and unseeable, which is a fact about eyes rather than about glass. Turn the light down and they come out. The sun cap stays a checkered plate: it faces the sun, and needs glare management rather than a view.
The one part of the far cap that is not glass is a 3.2 km mirror on the axis. A quarter of the beam survives all 540 km and arrives collimated, so it lands as 1.7 MW/m² — 1,200 suns — on a spot a thousandth of the cap's area; the mirror sends it back up the column for a second pass, which lifts the beam's utilisation from 76% to 94%. You can see it: the rod of light visibly terminates on a bright disc, and the disc banks and goes out with the beam.
The checker survives on the far cap as the window's mullions — the same polar tessellation at the same pitch, so it is still the only object in the world with a known cell size and still the only rule anyone has for judging distance. It is just read against sky now instead of against a plate.
Dawn is a line, not a fade. The beam glows by itself, so when its power comes up the sky brightens everywhere at once — but the ground is lit by the diffusers, and they hand the light down the axis. So a Tower morning is a fully lit sky over dark ground, with daylight arriving as a front running deepward from the sun cap at 225 m/s, crossing all 540 km in 40 minutes. Tower has no latitude; the cycle gives it longitude. Scrub the Time slider to 06:17 and stand at The Capital to watch it come, or tick Run the clock and let it.
The light has five settings when the clock is not driving it. Dark shuts the 54 TW beam down entirely and is the only one that is genuinely black — there is no airlight, no lit ground and no rod overhead, and the only thing left in the world is the sky. Night banks the beam to a thousandth instead of killing it, which is what night here has to be: the beam glows by itself, so a lit beam lights the world whatever the mirrors are doing, and aiming them away would leave the floor at about 100 W/m² — half of noon.
The eye-height slider runs from 1.7 m to 25 km with the local gravity beside it, and Fly lets go of the ground altogether — an autopilot on a permanent helix, 18° off the axis, crossing the world lengthwise while it rolls around it and reflecting off the caps rather than stopping. Your input turns your head, not the aircraft, so you can watch whatever you like while it flies. The slider becomes the altitude it holds, terrain-following underneath: climbing at once, sinking slowly. Gravity falls linearly to zero at the axis, so height makes flight easier — 0.91 g at 4.3 km up, and the beam widens overhead as you close on it.
Nothing inside rotates, because you, the ground and the air all turn together — so for 539 of the 540 km there is no evidence Tower turns at all. The exception is the window: the sky out there does not co-rotate, and wheels once every 7 min 29 s about the one motionless star on the axis. Rain used to stand in for that, leaning a permanent 1.5°, and is no longer drawn — the lean is real and still computed, but 1.5° was never a picture of anything. It is by a wide margin the plainest sign of the spin anywhere in the view, and it is only available at night, at the deepward end, looking the one direction that has ever had anything in it but Tower.
Working in the map
Pick a tool, click to place; Lake, Forest, Desert and Region trace a polygon and River and Canyon trace an open line (Enter finishes, Esc cancels). Drag markers to move them, shift-click two points to measure a sightline, f to fit the view.
Canon is 0 star slats — the hull is sealed, the inner surface is one unbroken 169,646 km², and the only views out are at the end caps. The runtime control for this is disabled and hidden (decisions.md D-033): the terrain raster is baked at 0 slats, so any other count draws bands over land that has none.
Everything autosaves to this browser's localStorage. Two exports:
| Export backup | everything, private notes included. Keep out of the public repo — .gitignore already excludes tower-backup-*.json. |
| 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. |
