tower / commit
feat(interior): the day is a front, not a dimmer
README.md | 11 +- decisions.md | 37 ++++++ design.md | 22 ++++ docs/screenshots/desktop-exterior-true.png | Bin 506317 -> 506858 bytes docs/screenshots/desktop-exterior.png | Bin 332578 -> 332036 bytes docs/screenshots/desktop-interior-dawn.png | Bin 0 -> 485508 bytes docs/screenshots/desktop-interior-night.png | Bin 278010 -> 276084 bytes docs/screenshots/desktop-interior-overhead.png | Bin 545753 -> 544165 bytes docs/screenshots/desktop-interior-suncap.png | Bin 385286 -> 383930 bytes docs/screenshots/desktop-interior.png | Bin 546693 -> 545739 bytes docs/screenshots/mobile-exterior.png | Bin 81770 -> 81564 bytes docs/screenshots/mobile-interior.png | Bin 172347 -> 172350 bytes docs/screenshots/mobile-panel.png | Bin 74912 -> 76387 bytes docs/screenshots/overview.png | Bin 1080288 -> 1152660 bytes index.html | 5 + src/canon/constants.ts | 22 ++++ src/style.css | 2 + src/ui/app.ts | 35 ++++++ src/views/interior.ts | 157 ++++++++++++++++++++++++- test/canon.test.ts | 6 + tools/screenshots.mjs | 22 ++++ 21 files changed, 315 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7eb82a0..4ff33ea 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ 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-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 | | Mobile — 390×844 | | @@ -211,7 +212,15 @@ tessellation at the same pitch, so it is still the only object in the world with 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. -The light has five settings. **Dark** shuts the 54 TW beam down entirely and is the +**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 diff --git a/decisions.md b/decisions.md index eaf2dad..e19a1d5 100644 --- a/decisions.md +++ b/decisions.md @@ -1578,3 +1578,40 @@ non-zero** when any pixel is clamped instead of printing a line into a log nobod reads. The unit test could never have caught this — it compares the declaration against extremes written down when they were last true, not against the terrain that just came out — and it now says so where it sits. + +## D-053 — The day is a front, not a dimmer + +The obvious day-night cycle is a global fade between the light settings. It would +have worked and it would have thrown away the most interesting thing about how this +world is lit. + +**What the physics allows.** D-046 already established that the swarm cannot make +night, because §1.3 gives the beam's glow to the beam and the air rather than to the +diffusers — a lit beam lights the world whatever the mirrorlets are doing. So beam +power has to ramp, and when it does, the sky brightens *everywhere at once*. There +is no way to have one end of the axis get its morning before the other. + +**What the swarm can still do is choose where the light lands.** The diffusers hand +beam light to the ground, and they are individually flown with absurd control +authority (§1.3.1). So the ground can be lit progressively even though the sky +cannot, and the natural direction is the one the beam already runs: sun cap to far +cap. + +**A Tower dawn is therefore a fully lit sky over dark ground**, with a line of +daylight travelling toward you at 225 m/s. That is not a compromise between the two +options, it is what falls out of taking both constraints seriously — and it is a +better image than either. + +**It gives Tower longitude.** §1.9.4 says the world has no latitude: every point is +equidistant from the light, which is why nothing about it varies. The cycle adds the +axis back as 40 minutes of time difference between the caps. Dusk runs the same +direction, so the sun-cap end has the shorter day by a hair. + +**The panel gets a time slider before it gets a clock.** Running the cycle is the +feature, but a 24-hour day at any watchable rate means waiting for dawn, and dawn +lasts 40 world-minutes out of 1,440. Being able to scrub straight to 06:17 is what +makes the front usable at all — and the front is a pure function of the phase, so +scrubbing and ticking are the same operation. + +The readout names the distance to the line rather than just the hour, because the +distance is the part you can act on: 27 km sunward is about two minutes' wait. diff --git a/design.md b/design.md index b003eb6..e4bae17 100644 --- a/design.md +++ b/design.md @@ -234,6 +234,28 @@ the cavity closed at both ends it is. Aiming light at a region to warm it is a separate lever and stays one (§1.9.4). The mirror is about not wasting the beam; the desert is about where the weather goes. +#### 1.3.4 The day, and why dawn has a direction + +Tower's day is **24 hours**, chosen for the people living under it — nothing +physical offers one, since the spin is 7.5 minutes and has nothing to do with light. + +**Dawn cannot be a dimmer, and it cannot be the swarm either.** §1.3 gives the glow +to the beam and the air, so the sky, the airlight and the rod overhead all brighten +together the moment beam power comes up; there is no way to have one end of the axis +light before the other. But the *ground* is lit by the diffusers, and they can put +the light wherever they like. So Tower brings the day up as a **front running +deepward from the sun cap**, 540 km in 40 minutes — **225 m/s**, near Tower's own rim +speed, and slow enough to stand and watch arrive. + +The result is a morning nothing on Earth has: **a fully lit sky over dark ground**, +with a line of daylight coming toward you along the world. §1.9.4 says Tower has no +latitude. The cycle gives it **longitude** — 40 minutes of it, sun cap to far cap. + +Dusk runs the same way, so the light withdraws in the direction it arrived and the +sun-cap end always has the shorter day by a hair. Nothing forces that; the +alternative was a dusk that retreats, and a consistent direction of time reads as +designed rather than as an oscillation. + ### 1.4 Sealed hull, and the cap windows **The cylinder wall is opaque and unbroken.** No slats, no windows, no view out diff --git a/docs/screenshots/desktop-exterior-true.png b/docs/screenshots/desktop-exterior-true.png index 684a263..2615bcf 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 a01d9ec..3a19f4a 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 new file mode 100644 index 0000000..4dfd0e6 Binary files /dev/null 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 5230a7f..24251b7 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 b6e2c73..7f169b7 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 78d87a5..2fce396 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 3d81c76..53b8fe1 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 c573100..0149785 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 61ae7c8..f1d92d7 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 16fb540..7ab5ffd 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 f7ec772..3063fc3 100644 Binary files a/docs/screenshots/overview.png and b/docs/screenshots/overview.png differ diff --git a/index.html b/index.html index 0b61f10..3d9ae5c 100644 --- a/index.html +++ b/index.html @@ -61,6 +61,11 @@ <div id="interior-options" class="hidden"> <label>Eye height <span id="eye-value" class="dim"></span> <input id="f-eye" type="range" min="0" max="1000" step="1" value="0" /></label> + <label>Time <span id="time-value" class="dim"></span> + <input id="f-time" type="range" min="0" max="1439" step="1" value="720" /></label> + <label class="row"><input id="f-cycle" type="checkbox" /> Run the clock</label> + <label>Rate <span id="cycle-value" class="dim"></span> + <input id="f-cycle-rate" type="range" min="0" max="4" step="1" value="2" /></label> <label>Light <select id="f-light"> <option value="dark">Dark — beam shut down</option> diff --git a/src/canon/constants.ts b/src/canon/constants.ts index 0834653..1e1831a 100644 --- a/src/canon/constants.ts +++ b/src/canon/constants.ts @@ -155,6 +155,28 @@ export const BEAM_ANGULAR_WIDTH_RAD = 2 * Math.atan(BEAM_RADIUS_M / RADIUS_M) export const NIGHT_BEAM_FRACTION = 0.001 export const BRIGHTNESS_NIGHT_W_M2 = BRIGHTNESS_FULL_W_M2 * NIGHT_BEAM_FRACTION +// ------------------------------------------------ the day cycle (design.md §1.3.4) +/** + * Tower's day. Nothing physical sets it — there is no rotation to inherit one from, + * since the spin is 7.5 minutes and has nothing to do with light. It is chosen for + * the people living under it, so it is Earth's. + */ +export const DAY_LENGTH_S = 86_400 +/** + * How long the light takes to cross the world at dawn, and again at dusk. + * + * The swarm cannot brighten everywhere at once and would not want to: the beam + * enters at the sun cap and the diffusers hand it down the axis, so the natural + * thing — and the only thing that gives a world with no east a direction for its + * mornings — is to bring it up as a front running deepward. 40 minutes over 540 km + * is **225 m/s**, which is a shadow line moving at about the rim speed of Tower + * itself, and slow enough to stand and watch arrive. D-053. + */ +export const DAWN_SWEEP_S = 2400 +export const DAWN_FRONT_SPEED_M_S = LENGTH_M / DAWN_SWEEP_S +/** The lag between the sun cap's morning and the far cap's — Tower's only longitude. */ +export const DAY_SWEEP_FRACTION = DAWN_SWEEP_S / DAY_LENGTH_S + // --------------------------------------------------------- structure (design.md §1.1) /** * Wall depth, floor to outer skin. Canon had no figure for this; 1 km is 2% of the diff --git a/src/style.css b/src/style.css index 07ba007..53c9f4a 100644 --- a/src/style.css +++ b/src/style.css @@ -85,6 +85,8 @@ button { } button:hover { background: #222a3d; } button:disabled { opacity: 0.45; cursor: default; } +/* The Light menu is disabled while the clock is driving the light; say so. */ +select:disabled, input:disabled { opacity: 0.45; cursor: default; } button:disabled:hover { background: #1b2130; } button.active { border-color: var(--accent); color: var(--accent); background: #16233a; } button.danger { border-color: #4a2a2a; color: var(--danger); } diff --git a/src/ui/app.ts b/src/ui/app.ts index 1a0905e..2f5b255 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -194,6 +194,39 @@ export class App { } showEye(eyeHeightFromSlider(Number(eye.value) / 1000)) + /** World seconds per real second, slowest first: a Tower day in 24 min at 60x. */ + const RATES = [1, 60, 900, 3600, 14400] + const time = $<HTMLInputElement>('f-time') + const showTime = () => { + const m = Number(time.value) + $('time-value').textContent = + `— ${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}` + } + time.oninput = () => { + showTime() + $<HTMLSelectElement>('f-light').disabled = true + if (!this.interior) return + this.interior.clockDriven = true + this.interior.setDayPhase(Number(time.value) / 1440) + this.renderStanding() + } + showTime() + + const cycle = $<HTMLInputElement>('f-cycle') + const rate = $<HTMLInputElement>('f-cycle-rate') + const applyCycle = () => { + const r = RATES[Number(rate.value)]! + $('cycle-value').textContent = + `— ${r}x, a day in ${fmtDuration(86_400 / r)}` + $<HTMLSelectElement>('f-light').disabled = cycle.checked || this.interior?.clockDriven === true + if (!this.interior) return + this.interior.cycleRate = r + this.interior.setCycling(cycle.checked) + } + cycle.onchange = applyCycle + rate.oninput = applyCycle + applyCycle() + const light = $<HTMLSelectElement>('f-light') light.onchange = () => this.interior?.setLighting(light.value as Lighting) } @@ -284,6 +317,8 @@ 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-cycle').checked = this.interior.cycling + $<HTMLSelectElement>('f-light').disabled = this.interior.cycling } 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 3d7ab1b..ad757dd 100644 --- a/src/views/interior.ts +++ b/src/views/interior.ts @@ -24,7 +24,8 @@ import { Scene, ShaderMaterial, TextureLoader, Vector3, WebGLRenderer, } from 'three' import { - BEAM_RADIUS_M, BRIGHTNESS_FULL_W_M2, COLLECTOR_DIAMETER_M, COLLECTOR_STANDOFF_M, BRIGHTNESS_NIGHT_W_M2, BRIGHTNESS_TEMPERATE_W_M2, + BEAM_RADIUS_M, BRIGHTNESS_FULL_W_M2, COLLECTOR_DIAMETER_M, COLLECTOR_STANDOFF_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, @@ -193,6 +194,10 @@ const GROUND_FRAG = /* glsl */` #include <common> #include <logdepthbuf_pars_fragment> uniform sampler2D uColor; +uniform float uFrontZ; +uniform float uFrontWidth; +uniform float uBelowFront; +uniform float uAboveFront; varying vec2 vUv; varying vec3 vWorld; varying vec3 vNormal2; @@ -235,8 +240,19 @@ void main() { // The beam is a line source on the axis, so the light comes from straight up. vec3 radial = normalize(vec3(vWorld.xy, 0.0)); float lambert = clamp(dot(vNormal2, -radial), 0.0, 1.0); + /** + * The dawn front (D-053). The beam's own glow comes up everywhere at once — it is + * one beam — but the diffusers hand the light down the axis, so the *ground* + * lights as a line running deepward at 225 m/s. Below the front and above it are + * different times of day, and this is the only place in Tower where that is true. + * + * Outside dawn and dusk both sides are 1.0 and this costs a mix. + */ + float side = smoothstep(uFrontZ - uFrontWidth, uFrontZ, vWorld.z); + float daylight = mix(uBelowFront, uAboveFront, side); + // The rest of the world is lit land, and lit land is what fills the shadows. - vec3 lit = base * (0.34 + 0.80 * lambert) * uExposure; + vec3 lit = base * (0.34 + 0.80 * lambert) * uExposure * daylight; vec3 T = towerTransmittance(uCam, vWorld); gl_FragColor = vec4(lit * T + uHaze * uExposure * (1.0 - T), 1.0); @@ -505,6 +521,16 @@ export class InteriorView { lighting: Lighting = 'temperate' /** Phase of the star pinwheel, radians. The only thing in the view that turns. */ private spin = 0 + /** Time of day, 0..1. 0 is midnight, 0.5 is noon. */ + dayPhase = 0.5 + /** Whether the clock is running, and how many world seconds pass per real one. */ + cycling = false + cycleRate = 900 + /** + * Whether the clock, rather than the Light menu, is deciding the light. Set by + * the panel: ticking the box or touching the time slider hands control over. + */ + clockDriven = false private labels: { place: Place; el: HTMLSpanElement }[] = [] @@ -568,6 +594,11 @@ export class InteriorView { // per-frame update can walk one list instead of knowing who wants what. uSpin: { value: 0 }, uStarGain: { value: 0 }, + // Only the ground declares these; carried here so one loop can set them. + uFrontZ: { value: 0 }, + uFrontWidth: { value: 24 }, + uBelowFront: { value: 1 }, + uAboveFront: { value: 1 }, } } @@ -826,13 +857,127 @@ export class InteriorView { return Math.abs(around) + Math.abs(dz) < 1e-6 ? this.yaw : Math.atan2(around, dz) } + /** + * Where the day is, as a light level and a front position. + * + * Dawn and dusk are the interesting part and they are not a dimmer. The beam + * glows by itself (§1.3, D-046), so its power — and with it the sky, the airlight + * and the rod overhead — has to come up globally; there is no way to make one end + * of the axis brighter than the other. What the swarm *can* do is choose where to + * put the light once it is there, and it hands it down the world as a front. So a + * Tower morning is a bright sky over dark ground, with a line of daylight coming + * toward you at 225 m/s. + */ + private sampleDay(): { watts: number; frontZ: number; below: number; above: number; kind: 'dawn' | 'dusk' | null } { + const p = ((this.dayPhase % 1) + 1) % 1 + const sweep = DAY_SWEEP_FRACTION + const DAWN = 0.25 + const DUSK = 0.75 + const smooth = (e0: number, e1: number, x: number): number => { + const t = Math.min(1, Math.max(0, (x - e0) / (e1 - e0))) + return t * t * (3 - 2 * t) + } + // Daylight everywhere, ignoring the front: up through dawn, down through dusk. + const up = smooth(DAWN, DAWN + sweep, p) + const down = smooth(DUSK, DUSK + sweep, p) + const day = up - down + // Noon is Earth clear-sky full; the rest of the day sits nearer temperate. + const shape = day * (0.62 + 0.38 * Math.sin(Math.PI * Math.min(1, Math.max(0, (p - DAWN) / (DUSK - DAWN))))) + const watts = BRIGHTNESS_NIGHT_W_M2 + (BRIGHTNESS_FULL_W_M2 - BRIGHTNESS_NIGHT_W_M2) * shape + + /** + * The front only exists while it is crossing. `below`/`above` are multipliers on + * the ground's share of the light, so the side that has not had its morning yet + * stays dark under a sky that is already bright. + */ + let frontZ = -1e6, below = 1, above = 1 + let kind: 'dawn' | 'dusk' | null = null + const nightShare = 0.04 + if (p >= DAWN && p < DAWN + sweep) { + frontZ = ((p - DAWN) / sweep) * L + below = 1 + above = nightShare + kind = 'dawn' + } else if (p >= DUSK && p < DUSK + sweep) { + frontZ = ((p - DUSK) / sweep) * L + below = nightShare + above = 1 + kind = 'dusk' + } + return { watts, frontZ, below, above, kind } + } + + /** + * Where the dawn line is relative to the observer, when there is one. Naming the + * distance is the point: a front 90 km deepward arrives in about seven minutes, + * and that is a thing you can wait for. + */ + private frontNote(): string { + const { frontZ, kind } = this.sampleDay() + if (!kind) return '' + /** + * Sign matters and is easy to get backwards: z counts deepward from the sun + * cap, and the front always travels deepward. So a front at a *lower* z than + * the observer is sunward of them and still coming; a higher one has already + * gone past. + */ + const d = frontZ - this.at.z / KM + if (Math.abs(d) < 1) return ` · the ${kind} line is here` + return d < 0 + ? ` · ${kind} ${(-d).toFixed(0)} km sunward, coming` + : ` · ${kind} passed, ${d.toFixed(0)} km deepward` + } + + /** Human clock, for the readout. Tower keeps Earth's 24 hours (D-053). */ + get clock(): string { + const secs = (((this.dayPhase % 1) + 1) % 1) * DAY_LENGTH_S + const h = Math.floor(secs / 3600) + const m = Math.floor((secs % 3600) / 60) + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}` + } + + /** + * Put the clock at a given time of day. Works whether or not the cycle is + * running — the front and the light level are a function of the phase, not of + * anything animating, so scrubbing is the same operation as ticking. + */ + setDayPhase(p: number): void { + this.dayPhase = ((p % 1) + 1) % 1 + this.applyLighting() + this.cb.onLook() + this.queueSave() + } + + setCycling(on: boolean): void { + this.cycling = on + if (on) this.clockDriven = true + if (on) this.applyLighting() + else { + // Hand the manual settings back an unswept world. + for (const m of this.shared) { + m.uniforms['uBelowFront']!.value = 1 + m.uniforms['uAboveFront']!.value = 1 + } + this.applyLighting() + } + this.queueSave() + } + private applyLighting(): void { /** * Eyes adapt, so twilight is not 6.7x darker than temperate to look at. The * exponent compresses the canon watts into something a screen can show while * keeping the ordering and the ratios honest. */ - const watts = LIGHTING[this.lighting] + const day = this.clockDriven ? this.sampleDay() : null + const watts = day ? day.watts : LIGHTING[this.lighting] + if (day) { + for (const m of this.shared) { + m.uniforms['uFrontZ']!.value = day.frontZ + m.uniforms['uBelowFront']!.value = day.below + m.uniforms['uAboveFront']!.value = day.above + } + } const e = (watts / BRIGHTNESS_TEMPERATE_W_M2) ** 0.35 /** * The other half of "eyes adapt": the stars are always exactly as bright as @@ -1026,6 +1171,7 @@ 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`, + ...(this.clockDriven ? [`time ${this.clock}${this.frontNote()}`] : []), // 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`, @@ -1056,6 +1202,11 @@ export class InteriorView { * once every 7 min 29 s about the one motionless star on the axis. It is the * only unambiguous evidence of the spin anywhere in the view (D-045). */ + if (this.cycling) { + this.dayPhase = (this.dayPhase + (dt * this.cycleRate) / DAY_LENGTH_S) % 1 + this.applyLighting() + this.cb.onLook() + } this.spin = (this.spin + OMEGA_RAD_S * dt) % (2 * Math.PI) for (const m of this.shared) m.uniforms['uSpin']!.value = this.spin this.renderer.render(this.scene, this.camera) diff --git a/test/canon.test.ts b/test/canon.test.ts index 34b79da..06934b7 100644 --- a/test/canon.test.ts +++ b/test/canon.test.ts @@ -138,6 +138,12 @@ describe('§1.3 sun system', () => { near(C.BEAM_UTILISATION_ONE_PASS, 0.756, 0.005) near(C.BEAM_UTILISATION_TWO_PASS, 0.940, 0.005) }) + it('the dawn front crosses the world at 225 m/s, about Tower rim speed', () => { + near(C.DAWN_FRONT_SPEED_M_S, 225, 1) + // 40 minutes of a 24 hour day: the far cap's morning lags the sun cap's by + // that much, which is the only longitude a world with no east has. + near(C.DAY_SWEEP_FRACTION * 24 * 60, 40, 0.1) + }) it('night is the beam banked to a thousandth, 0.32 W/m²', () => { near(C.BRIGHTNESS_NIGHT_W_M2, 0.32, 0.01) near(C.NIGHT_BEAM_FRACTION, 0.001, 1e-9) diff --git a/tools/screenshots.mjs b/tools/screenshots.mjs index df76a44..593361f 100644 --- a/tools/screenshots.mjs +++ b/tools/screenshots.mjs @@ -313,6 +313,28 @@ const SHOTS = [ await settle(page, 1600) }, }, + { + id: 'interior-dawn', + caption: 'Interior — dawn: a lit sky over dark ground, and the light still 27 km away', + on: ['desktop'], + run: async (page) => { + await openView(page, 'interior') + await pick(page, 'The Capital') + await withPanel(page, async () => { + // 06:17. The dawn front leaves the sun cap at 06:00 and takes 40 minutes to + // cross the world, so at The Capital's z 270 km it is 27 km short and still + // coming — which is the whole point of the shot. + await page.locator('#f-time').fill('378') + await page.locator('#f-time').dispatchEvent('input') + }) + const s = await stage(page) + for (let i = 0; i < 4; i++) { + await drag(page, [s.x + s.w * 0.8, s.y + s.h * 0.5], [s.x + s.w * 0.8 - 246, s.y + s.h * 0.5]) + await page.waitForTimeout(150) + } + await settle(page, 1600) + }, + }, { id: 'reader', caption: 'Reader mode (?reader=1) — terrain, places, a detail card and the probe, no editing',