`films.mjs` renders time-lapses of the running dist/: a locked camera watching
eighteen hours pass, shadow sweeping the city and the windows coming on as the
computed sun goes down. Two ship — the Financial District and the whole board.
Real-time capture was never on the table and would have been the wrong idea
anyway. SwiftShader draws about three frames a second with no GPU, so a screen
recording is a slideshow; and a sunset takes an hour, which is not a length of
video anybody watches. So it is rendered offline the way films always have been:
set the clock, let the frame settle, expose, advance. The output is smooth 30fps
regardless of what the renderer managed while being photographed.
Two shims in `filmClock()` get that done without a reload per frame, which
matters because a reload is fifteen seconds and would put a 180-frame film at
three quarters of an hour. The skew is a live global read on every `Date` call
rather than baked in at load; and `setInterval(…, 60_000)` is compressed to
120ms, which is the load-bearing half — `main.ts` recomputes the sun on a
once-a-minute wall-clock tick, so a shifted clock otherwise sits unrendered.
Only the 60-second interval is touched, by value.
Both are capture-harness lies told to the page. The deployed bundle has no idea
this file exists.
Not wired into the site. This is the capability and two reels to look at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correcting the pairing this script documents: the daylit frame goes on the
light page and the night frame on the dark one. A reader in dark mode has asked
for a dark room, and a bright noon render is a hole punched in it; the hairline
frame around each figure is what keeps a night shot from dissolving into the
page, which is the border's job rather than the sun's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each of the seven cameras now fires at two clocks — one daylight frame, one
around sixteen degrees below the horizon — and lumbridgecorp.com loads whichever
is the opposite of the reader's theme. A night render on that site's dark page
is a dark rectangle on a dark page: the edges dissolve and the picture stops
being an object. A daylit one on the white page has the same problem in reverse,
and the hazier frames wash out entirely.
It is also the cheapest demonstration available of the claim those pages lean on
hardest. The sun is computed from a real time rather than themed, and a reader
who doubts it can hit the light/dark toggle and watch the same city change hour
with nothing else about the geometry moving.
Shot ids lose their time of day — `soma-afternoon` is `soma`, `fidi-night` is
`fidi`, `peninsula-morning` is `peninsula` — because none of them names one hour
any more. The manifest gains a `Frame` type: `at`, `src`, `alt` and a `note`
belong to an hour, while `place` and `caption` are true of both, so a caption
cannot end up describing light that is not in the frame under it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`shots.mjs` renders the product imagery for lumbridgecorp.com/simulate: seven
frames of the running `dist/`, at chapters and times chosen rather than
defaulted, written as WebP into the sibling site checkout together with a
generated manifest that carries each picture's caption and alt text. The pages
there had described a renderer in prose for their whole life without ever
showing one, which is a strange way to sell a renderer.
The captions live next to the camera poses in this repo, not on the site, so
re-aiming a camera cannot leave a caption behind describing the old view. The
manifest emits a `ShotId` union, so a page asking for a picture that has been
renamed fails the site's typecheck instead of rendering a hole.
Along the way: the share cards were never reproducible. `scenekit.ts` eases a
chapter change over about two seconds of scene time, `stage.ts` clamps `dt` to
50 ms a frame, and SwiftShader here draws about three frames a second — so the
flight takes twenty seconds of wall clock and `capture.mjs` waited four. Every
run caught the camera at a different point over the bay, and none of them at
the chapter the key press asked for. Both scripts now open the page with
reduced motion, which is the app's own answer to "somebody clicked a name in a
list": `flyTo` sets the pose outright. `og-tera.png` is regenerated and is
Hayes Valley for the first time. The bytes still differ run to run, because
aircraft are crossing and cloud shadow is drifting; the framing no longer does.
`harness.mjs` is the static server, the SwiftShader flags, the two-hostnames-
one-dist trick and the clock shim, extracted because there are two consumers
now and two copies would have drifted apart while both claimed to photograph
the same app.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A link to tera.lumbridgecorp.com or office.lumbridgecorp.com unfurled as a bare
blue URL. No picture, no sentence, and a title — "Lumbridge Simulate — San
Francisco" — that was wrong at one of the two doors and stale at the other.
There were no `og:` or `twitter:` tags in the document at all. For a project
whose entire pitch is that you should look at it, that is the most expensive
missing markup in the repo.
Both cards are screenshots of the running app, not drawings of it, and that is
the load-bearing decision rather than a shortcut. `scripts/brand-assets/` builds
them in two passes: shoot the city and the office out of `dist/`, then render
`og.html` over those shots at exactly 1200x630. The thing is worth looking at,
and a drawing of it goes stale in silence — which is not hypothetical. The card
currently live on lumbridgecorp.com is a viewport screenshot of a marketing page
that has since been rewritten, so that preview advertises a positioning the site
no longer uses, and it has been doing so for a month with nobody noticing. A
card regenerated from `dist/` by one command is a card that can be kept true by
running the command.
The clock is shifted to midday for the capture, because the sun is real —
`observe()` computes it from `new Date()` — and a card regenerated at two in the
morning is an honest photograph of a black rectangle. Shifted rather than
frozen: everything else runs off `requestAnimationFrame`, and a stopped clock
stalls the frame loop the screenshot is waiting on.
Every string on the cards is the project's own. The headlines are what
`README.md` already says each half is; "Clone it and it works" is CONTRACT.md
§0's acceptance test in the words `main.ts` uses for it; the licence in the
corner is the one in the repo root. A share card is the most-read and least
reviewed sentence a project has, which is exactly why it should not be where new
claims get invented.
The harder half was that the two doors are one bundle. The app sorts out which
door it is by reading its own hostname; a crawler cannot, because it reads the
HTML and nothing else — so one `index.html` means both doors unfurl as the same
place, and being a different place is the office door's whole reason to exist. A
second hand-written shell is what the Caddy config already argues against for the
static root ("sharing the root rather than copying it means a deploy cannot leave
the two doors on different builds"), and two 900-line files each carrying the
inline stylesheet would drift on the first CSS change with nothing to notice.
So the build emits both. Everything outside the `ogc:` markers is copied byte for
byte — verified: `office.html` and `index.html` are identical below `</head>` and
point at the same bundle hash — and only the head block differs. A change to the
interface reaches both doors by construction. It runs in `writeBundle` rather
than `transformIndexHtml` because it needs the finished document, after Vite has
rewritten the asset URLs, and it errors rather than no-oping if the markers go
missing: a card that is quietly the wrong one is the failure the plugin exists to
prevent.
`deploy/Caddyfile.snippet` documents the one line that turns it on — the office
door's `try_files` fallback pointing at `/office.html` off the same shared root.
A deployment that skips it is not broken; the office door keeps working and
unfurls with the city's card, which is what it did before.
Not deployed. The live Caddyfile still falls back to /index.html for the office
host, so this needs that one-line change on cloud-2 before office.
lumbridgecorp.com unfurls as the office.
Both shells boot clean in Chrome with zero console errors, 31 client tests and
the no-binary gate still pass — `public/` is exempt from it, which is where the
two PNGs live.