tower / commit
feat(interior): rain is not drawn any more
README.md | 19 +++--- content/geography.json | 28 ++++++++- decisions.md | 20 ++++++ design.md | 16 ++--- docs/screenshots/desktop-exterior-true.png | Bin 480678 -> 485140 bytes docs/screenshots/desktop-exterior.png | Bin 277782 -> 277245 bytes docs/screenshots/desktop-interior-night.png | Bin 237248 -> 238434 bytes docs/screenshots/desktop-interior-overhead.png | Bin 448986 -> 438518 bytes docs/screenshots/desktop-interior-suncap.png | Bin 0 -> 343533 bytes docs/screenshots/desktop-interior.png | Bin 452859 -> 454082 bytes docs/screenshots/desktop-map-sightline.png | Bin 576206 -> 614134 bytes docs/screenshots/mobile-exterior.png | Bin 70045 -> 70204 bytes docs/screenshots/mobile-interior.png | Bin 145370 -> 146247 bytes docs/screenshots/mobile-panel.png | Bin 72872 -> 73743 bytes docs/screenshots/overview.png | Bin 856749 -> 920715 bytes index.html | 1 - src/ui/app.ts | 3 - src/views/interior.ts | 82 ++++--------------------- static/terrain-color.png | Bin 1163745 -> 1310628 bytes static/terrain-corridor.png | Bin 1105895 -> 1149358 bytes static/terrain-height.png | Bin 2829037 -> 2909819 bytes tools/screenshots.mjs | 10 +-- 22 files changed, 80 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 56e7f34..b8f3de8 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,9 @@ npm run screenshots -- --url http://localhost:5173 # use a dev server already ``` The threshold is measured at both ends, not guessed. Seven of the ten shots come out -**byte-identical** between runs — the rain is the only randomised thing in Tower and -it is seeded on the way in, the exterior spin is switched off, and every shot gets a -fresh browser context so no camera leaks into the next. The two exterior shots are +**byte-identical** between runs — nothing in Tower is stochastic any more, the +exterior spin is switched off, and every shot gets a fresh browser context so no +camera leaks into the next. The two exterior shots are the exception, drifting up to **0.98%** because the orbit control damps toward its target asymptotically. Against that, hiding both 3D canvases scored **7% to 31%**, and a view going entirely black scores 12% to 39%. @@ -209,12 +209,13 @@ to a thousandth instead of killing it, which is what night here has to be: the b 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 -**Rain** leans 1.5° antispinward. Nothing *inside* rotates, because you, the ground -and the air all turn together — so for 539 of the 540 km that 1.5° is the only -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. It is by a wide margin the plainest sign of the spin anywhere in the view, and +The eye-height slider runs from 1.7 m to 25 km with the local gravity beside 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. diff --git a/content/geography.json b/content/geography.json index bb35092..59926c0 100644 --- a/content/geography.json +++ b/content/geography.json @@ -59,7 +59,7 @@ "sourceElevationM": 1900, "mouthElevationM": 160, "liftHeadM": 1780, - "pumpPowerW": 6850000000.0, + "pumpPowerW": 6850000000, "pumpPowerPercentOfBudget": 0.0126, "closure": "made-weather", "closureNote": "Tower evaporates The Deep and snows it onto the range; the tarns are the top of the loop. Four rivers descend, three are defeated by the ridges and gather along their front, and all of it leaves through The Narrows as one river.", @@ -140,7 +140,7 @@ "kind": "landmark", "geometry": { "type": "point", - "theta": 0.0, + "theta": 0, "z": 0 }, "notes": "The sunward end of the world, and one of only two views out (§1.4). A cap is a wall, not a floor: gravity there points radially outward, tangent to its surface, so nothing rests on it. Fifty kilometres of vertical cliff with its windows at the base, and the beam entering through a 3.2 km aperture at its centre." @@ -152,7 +152,7 @@ "kind": "landmark", "geometry": { "type": "point", - "theta": 0.0, + "theta": 0, "z": 540000 }, "notes": "The deepward end, 540 km away, standing over The Deepward Sea. Its windows look along the spin axis — which is the celestial pole — so the stars do not sweep past but turn about a fixed centre, one revolution every 7 min 29 s. The only moving sky anyone in Tower ever sees." @@ -249,6 +249,28 @@ "warpM": 9000 } }, + { + "id": "the-girdle", + "name": "The Girdle", + "provisionalName": true, + "kind": "region", + "biome": "alpine", + "geometry": { + "type": "band", + "ring": true, + "z": 90000 + }, + "relief": { + "op": "ridge", + "blend": "max", + "heightM": 2900, + "widthZM": 17000, + "warpM": 12000, + "ridgedAmp": 0.6, + "ridgedScaleM": 14000 + }, + "notes": "The range does not stop at Eastwall. It carries on round the far side of the world and closes on itself — a ring of peaks at z 90 km, unbroken except where the rivers cut it. Snowcrown, The Sisters, Westwall and Eastwall are the named high points of it, not a range of their own; the rest is unwalked. Rings the world 86 km sunward of the three low ridges, and is the reason every river on this half of Tower runs deepward." + }, { "id": "hollow-tarn", "name": "Hollow Tarn", diff --git a/decisions.md b/decisions.md index 324a866..e61ed48 100644 --- a/decisions.md +++ b/decisions.md @@ -1477,3 +1477,23 @@ canon before checking. It does not. The pinwheel turns about the direction so the centre of Tower's only moving sky is a point on the **horizon**, dead ahead at eye level — while the mirror is ~10° above it from 270 km. The two coincide only for an observer standing on the axis, and nobody can stand there. + +## D-049 — Rain is not drawn any more + +Rain existed in the interior view for one reason: with the hull sealed it was the +only moving thing in the sky, and therefore the only evidence Tower turns. §1.9.2 +then undercut it — drag pins a drop's speed, so rain leans a fixed **1.5°** whatever +the cloud, against a waterfall's 11°. The readout printed the angle to stop the +smallness reading as a bug, which is an admission that the picture was not carrying +the claim. + +D-045 gave the view a whole sky going round once every 7 min 29 s. Against that, a +1.5° lean is not evidence of anything, and 6,000 line segments were being stepped +every frame to fail to show it. + +**The physics stays.** `canon/motion.ts` still computes the lean and +`test/motion.test.ts` still checks it — it is a real fact about how anything falls +in here, and the waterfall is still the compass. It is only the *rendering* that is +gone, along with the toggle, the seeded raindrop field, and the six Three imports +that existed for it. The readout line it occupied now prints the pinwheel's period, +which is the interval you can actually time against the sky. diff --git a/design.md b/design.md index d1ffdcc..bb2605d 100644 --- a/design.md +++ b/design.md @@ -720,16 +720,18 @@ The airlight the haze adds is *neutral*; tinting it blue double-counts the wavelength dependence and turns even the desert mauve (D-034). **Nothing inside the scene rotates**, because the observer, the ground and the air -all co-rotate — which leaves what falls to carry the spin, and §1.9.2 turns out to -be lopsided about how. Rain leans a fixed 1.5° whatever the cloud, because drag -pins its speed; only free fall keeps accelerating and so keeps leaning. **The -waterfall is the compass; the rain is barely a hint.** Rain is drawn, and the -readout prints its 1.5° next to it. +all co-rotate. What falls carries the spin, and §1.9.2 is lopsided about how: rain +leans a fixed 1.5° whatever the cloud, because drag pins its speed, while only free +fall keeps accelerating and so keeps leaning. **The waterfall is the compass; the +rain is barely a hint.** Rain is no longer drawn (D-049) — 1.5° is not a picture, +and the pinwheel below does the job it was standing in for. The physics stays in +`canon/motion.ts` and stays tested; it was only ever the rendering that could not +carry it. **The one thing that does turn is outside.** The sky through the far cap does not co-rotate, so it wheels once every 7 min 29 s about the motionless star on the axis -(D-045). It is by a wide margin the plainest evidence of the spin in the view — -1.5° of rain lean against a whole sky going round — and it is only available at +(D-045). It is by a wide margin the plainest evidence of the spin in the view, and +since D-049 it is the only one — and it is only available at night, at the deepward end, looking the one direction that has ever had anything in it but Tower. diff --git a/docs/screenshots/desktop-exterior-true.png b/docs/screenshots/desktop-exterior-true.png index d666a43..4c7f365 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 4d88325..6de717e 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 5d381a6..2ed88be 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 d7e5a64..baf97da 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 new file mode 100644 index 0000000..b44cb0a Binary files /dev/null and b/docs/screenshots/desktop-interior-suncap.png differ diff --git a/docs/screenshots/desktop-interior.png b/docs/screenshots/desktop-interior.png index ac6a76f..4e191fc 100644 Binary files a/docs/screenshots/desktop-interior.png and b/docs/screenshots/desktop-interior.png differ diff --git a/docs/screenshots/desktop-map-sightline.png b/docs/screenshots/desktop-map-sightline.png index c299f16..a6c2328 100644 Binary files a/docs/screenshots/desktop-map-sightline.png and b/docs/screenshots/desktop-map-sightline.png differ diff --git a/docs/screenshots/mobile-exterior.png b/docs/screenshots/mobile-exterior.png index 93b9dc3..16f0977 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 8420958..febe6f1 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 db4169c..db9a50a 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 516a14d..d02d30c 100644 Binary files a/docs/screenshots/overview.png and b/docs/screenshots/overview.png differ diff --git a/index.html b/index.html index 1530efe..0b61f10 100644 --- a/index.html +++ b/index.html @@ -69,7 +69,6 @@ <option value="temperate" selected>Temperate — 200 W/m²</option> <option value="full">Full — 319 W/m²</option> </select></label> - <label class="row"><input id="f-rain" type="checkbox" /> Rain</label> <p class="hint">Drag to look, scroll to zoom in like a pair of binoculars. Selecting a place walks you there. Nothing inside here rotates: you, the ground and the air all turn together. The far cap is a window, so at diff --git a/src/ui/app.ts b/src/ui/app.ts index 812a1be..1a0905e 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -196,8 +196,6 @@ export class App { const light = $<HTMLSelectElement>('f-light') light.onchange = () => this.interior?.setLighting(light.value as Lighting) - const rain = $<HTMLInputElement>('f-rain') - rain.onchange = () => this.interior?.setRain(rain.checked) } /** @@ -286,7 +284,6 @@ export class App { eye.value = String(Math.round(sliderFromEyeHeight(this.interior.eyeM) * 1000)) eye.dispatchEvent(new Event('input')) $<HTMLSelectElement>('f-light').value = this.interior.lighting - $<HTMLInputElement>('f-rain').checked = this.interior.showRain } this.interior.resize() this.interior.setPlaces(this.store.places.filter((p) => p.geometry.type !== 'band')) diff --git a/src/views/interior.ts b/src/views/interior.ts index b86426f..8a4a569 100644 --- a/src/views/interior.ts +++ b/src/views/interior.ts @@ -20,8 +20,7 @@ import type { Texture } from 'three' import { AdditiveBlending, BackSide, CircleGeometry, Color, CylinderGeometry, DoubleSide, - Float32BufferAttribute, BufferGeometry, Group, LineBasicMaterial, LineSegments, - Matrix4, Mesh, PerspectiveCamera, RepeatWrapping, RingGeometry, + Mesh, PerspectiveCamera, RepeatWrapping, RingGeometry, Scene, ShaderMaterial, TextureLoader, Vector3, WebGLRenderer, } from 'three' import { @@ -30,7 +29,6 @@ import { RAYLEIGH_FLOOR_PER_M, SPECIFIC_GAS_CONSTANT, TEMPERATURE_K, gravityAtHeight, } from '../canon/constants.js' -import { RAINDROP_TERMINAL_M_S, steadyFallLeanRad } from '../canon/motion.js' import { type SurfacePoint, mapThetaOrigin, normalizeTheta } from '../canon/geometry.js' import type { Place } from '../world/schema.js' import { EYE_MAX_M, EYE_MIN_M } from '../ui/scales.js' @@ -74,9 +72,6 @@ const DENSITY_A = ((OMEGA_RAD_S ** 2 / (2 * SPECIFIC_GAS_CONSTANT * TEMPERATURE_ const HAZE_DAY = 0x9aa3ad const HAZE_DUSK = 0x8d8792 -/** A raindrop is only ever seen close up, so the field only has to be a small box. */ -const RAIN_BOX_KM = 140 / KM -const RAIN_STREAK_KM = 1.1 / KM /** Shell tessellation. 0.7 km triangles: distant relief is real, your feet are not. */ const SEG_AROUND = 512 @@ -92,6 +87,12 @@ const SEG_ALONG = 768 * gives the glow to the beam and the air rather than to the diffusers, so aiming * the swarm away would not darken anything (D-046). */ +/** The pinwheel's period, the one interval in Tower you can time against the sky. */ +const fmtSpin = (): string => { + const s = (2 * Math.PI) / OMEGA_RAD_S + return `${Math.floor(s / 60)} min ${Math.round(s % 60)} s` +} + export const LIGHTING = { dark: 0, night: BRIGHTNESS_NIGHT_W_M2, @@ -454,9 +455,6 @@ export class InteriorView { private ground: Mesh private groundMat: ShaderMaterial private shared: ShaderMaterial[] = [] - private rain: LineSegments - private rainGroup = new Group() - private rainVel: Float32Array private running = false private lastFrame = 0 private dragging = false @@ -477,7 +475,6 @@ export class InteriorView { lighting: Lighting = 'temperate' /** Phase of the star pinwheel, radians. The only thing in the view that turns. */ private spin = 0 - showRain = false private labels: { place: Place; el: HTMLSpanElement }[] = [] @@ -501,9 +498,6 @@ export class InteriorView { this.scene.add(this.ground) this.buildCaps() this.buildBeam() - this.rainVel = new Float32Array(0) - this.rain = this.buildRain() - this.scene.add(this.rainGroup) this.restoreState() this.applyLighting() @@ -745,50 +739,6 @@ export class InteriorView { this.scene.add(new Mesh(g, m)) } - /** - * Rain, because with the hull sealed it is the only moving thing in the sky and - * the only evidence Tower turns. It leans antispinward by 1.5° — computed, not - * chosen, and much subtler than the stars it replaces. The readout prints the - * angle so the smallness is legible rather than looking like a bug. - */ - private buildRain(): LineSegments { - const N = 2200 - const streak = RAIN_STREAK_KM - const lean = Math.tan(steadyFallLeanRad(RAINDROP_TERMINAL_M_S)) - const pos = new Float32Array(N * 6) - this.rainVel = new Float32Array(N) - for (let i = 0; i < N; i++) { - const x = (Math.random() - 0.5) * RAIN_BOX_KM - const y = (Math.random() - 0.5) * RAIN_BOX_KM - const z = (Math.random() - 0.5) * RAIN_BOX_KM - // Local frame: +x spinward, +y up, +z deepward. Falling is -y, leaning -x. - pos.set([x, y, z, x + streak * lean, y + streak, z], i * 6) - this.rainVel[i] = (0.85 + Math.random() * 0.3) * RAINDROP_TERMINAL_M_S / KM - } - const g = new BufferGeometry() - g.setAttribute('position', new Float32BufferAttribute(pos, 3)) - const line = new LineSegments(g, new LineBasicMaterial({ - color: 0xcfe0f2, transparent: true, opacity: 0.42, depthWrite: false, - })) - line.frustumCulled = false - this.rainGroup.add(line) - this.rainGroup.visible = false - return line - } - - /** Fall, and wrap around the box. Only the y of each streak ever changes. */ - private stepRain(dt: number): void { - const attr = this.rain.geometry.attributes['position']! - const a = attr.array as Float32Array - for (let i = 0; i < this.rainVel.length; i++) { - let y = a[i * 6 + 1]! - this.rainVel[i]! * dt - if (y < -RAIN_BOX_KM / 2) y += RAIN_BOX_KM - a[i * 6 + 1] = y - a[i * 6 + 4] = y + RAIN_STREAK_KM - } - attr.needsUpdate = true - } - // -------------------------------------------------------------------- camera /** Basis at the observer: up is toward the axis, deepward is +z, spinward is theta-hat. */ @@ -818,8 +768,6 @@ export class InteriorView { this.camera.updateProjectionMatrix() for (const m of this.shared) (m.uniforms['uCam']!.value as Vector3).copy(eye) - this.rainGroup.position.copy(eye) - this.rainGroup.setRotationFromMatrix(new Matrix4().makeBasis(spinward, up, deepward)) } /** @@ -983,12 +931,6 @@ export class InteriorView { this.queueSave() } - setRain(on: boolean): void { - this.showRain = on - this.rainGroup.visible = on - this.queueSave() - } - // -------------------------------------------------------------------- state private queueSave(): void { @@ -1003,7 +945,6 @@ export class InteriorView { localStorage.setItem(STATE_KEY, JSON.stringify({ theta: this.at.theta, z: this.at.z, eye: this.eyeM, yaw: this.yaw, pitch: this.pitch, fov: this.fovDeg, - lighting: this.lighting, rain: this.showRain, })) } catch { // Not persisting where you stood is survivable. @@ -1028,8 +969,6 @@ export class InteriorView { if (isLighting(s['lighting'])) { this.lighting = s['lighting'] } - this.showRain = s['rain'] === true - this.rainGroup.visible = this.showRain } catch { // Unreadable storage just means standing in the capital again. } @@ -1057,7 +996,9 @@ export class InteriorView { `looking ${deg(heading).toFixed(0)}° ${compass}, ${deg(this.pitch) >= 0 ? '+' : ''}${deg(this.pitch).toFixed(0)}°`, `field ${this.fovDeg.toFixed(0)}°`, `beam ${deg(beamAngle).toFixed(2)}° wide overhead`, - `rain leans ${deg(steadyFallLeanRad(RAINDROP_TERMINAL_M_S)).toFixed(1)}° antispinward`, + // The pinwheel replaced the rain as the spin's evidence here (D-049), so + // the readout names the period you can actually time against the sky. + `sky turns once in ${fmtSpin()}, about the axis`, ], } } @@ -1079,8 +1020,7 @@ export class InteriorView { if (!this.running) return const dt = Math.min(0.1, (now - this.lastFrame) / 1000) this.lastFrame = now - if (this.showRain) this.stepRain(dt) - /** + /** * The pinwheel. Nothing inside Tower rotates — you, the ground and the air * all turn together — but the sky through the far cap does not, so it wheels * once every 7 min 29 s about the one motionless star on the axis. It is the diff --git a/static/terrain-color.png b/static/terrain-color.png index 48b3614..932924a 100644 Binary files a/static/terrain-color.png and b/static/terrain-color.png differ diff --git a/static/terrain-corridor.png b/static/terrain-corridor.png index 4bf40fd..75947f1 100644 Binary files a/static/terrain-corridor.png and b/static/terrain-corridor.png differ diff --git a/static/terrain-height.png b/static/terrain-height.png index 89c65ce..0c13777 100644 Binary files a/static/terrain-height.png and b/static/terrain-height.png differ diff --git a/tools/screenshots.mjs b/tools/screenshots.mjs index 826f318..89c9637 100644 --- a/tools/screenshots.mjs +++ b/tools/screenshots.mjs @@ -16,7 +16,7 @@ * `divergence`), but the number is a smoke alarm, not a verdict. Look at the sheet. * * Determinism is deliberate and shallow — a fixed viewport, a seeded Math.random - * (the rain is the only randomised thing in Tower) and the exterior spin switched + * (nothing in Tower is stochastic since D-049) and the exterior spin switched * off. Anything else that moves between runs is a real change worth seeing. * * Flags: @@ -263,7 +263,6 @@ const SHOTS = [ * 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 + 164]) - await page.check('#f-rain') await settle(page, 1600) }, }, @@ -324,9 +323,10 @@ async function capture(browser, base, profile, shot) { hasTouch: profile.mobile, }) /** - * The rain is the only randomised thing in the world; everything else is a pure - * function of the geography and the camera. Seeding it keeps the contact sheet - * comparable between runs. + * Insurance rather than a live need: the rain that used to be the world's one + * randomised thing is gone (D-049), and the star field is hashed in GLSL rather + * than sampled. If anything stochastic comes back, the contact sheet stays + * comparable between runs without anyone having to remember why. */ await context.addInitScript(() => { let seed = 0x2f6e2b1