06455f74249308b04a66dc9f5214b823fb3a4f03
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a229fb2721 |
The sky gets the things above the aeroplanes
Satellites, end to end: CelesTrak element sets behind the same TTL cache
the weather and the flights use, served as TLEs rather than as positions,
and propagated in the browser with SGP4.
Sending elements is the same trick `flights/plan.ts` plays and it has a
better excuse here — a TLE *is* the closed form, valid for days either
side of its epoch, so one cacheable fetch every six hours replaces a poll
and every viewer agrees about where everything is.
Two things are worth knowing about the shape of it:
- There is no region parameter. An aeroplane at 10,000 m is local and
a satellite at 550 km is above the horizon for a circle two thousand
kilometres across, so one catalogue serves both boards and the client
decides what is above its own horizon. Only the observer is per-city,
which is why `main.ts` shares the elements and rebuilds the catalogue.
- The layer draws on a dome, because it cannot draw anywhere else.
`world.metres(550_000)` is 21,000 scene units against a far plane at
3,000. Azimuth and elevation are real; the radius carries nothing.
Off by default: a clone that started pulling CelesTrak on `npm run dev`
would have volunteered somebody else's bandwidth for its onboarding.
Godmode gets the two dials that point at the sky rather than at the
light — fabricated traffic, which composes with a live ADS-B feed instead
of replacing it, and a switch for the satellite layer with a count beside
it. Both are god-only lies about the inputs, in the manner of the weather
override.
`satellite.js` is the second runtime dependency this package has taken.
Its entry point star-exports an Emscripten build that cannot be shaken
out, so `noWasmPropagator` in the Vite config cuts it: 308 kB of WASM
loader for a bulk propagator nothing calls, against 26 kB for the SGP4
that does the work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0cc2126e85 |
The capture harness uses the graphics card that was there all along
`launch()` asks for `--use-angle=vulkan` and gets the Radeon RX 6700 XT. On the real film loop at 1440x900 that is 7.56 s a frame to 0.47, and about twelve cores pegged down to less than one. A 180-frame film goes from twenty-two minutes to ninety seconds; the three shipped reels go from over an hour to four minutes. The SwiftShader flag was inherited from the share-card script and its comment said "a box with no display and no GPU". Half of that was never true. The card has been in this machine the whole time, with the amdgpu driver and a working RADV ICD, and Chrome reaches it with **no display server at all** — no DISPLAY, no XDG_RUNTIME_DIR, nothing but read access to /dev/dri/renderD128, which group `render` already grants. So the portability the flag was protecting is intact: this still runs over ssh and under cron. Naming the backend is the whole trick, and most of the plausible spellings are traps. `--use-angle=gl`, `--use-gl=egl`, `--use-gl=desktop`, `--enable-features=Vulkan` and passing no GL flags at all were each measured on this box, and all five land on SwiftShader while reporting success. Only `--use-angle=vulkan` and `--use-angle=gl-egl` reach the card. So `launch()` verifies instead of assuming. `--use-angle=vulkan` is a demand, not a preference: with the driver hidden it yields no WebGL context at all rather than falling back, the boot curtain never lifts, and the caller sits in `waitForFunction` until a three-minute timeout before failing with something that looks nothing like "there is no GPU here". Reading `UNMASKED_RENDERER_WEBGL` once per run costs about half a second and turns that into a printed line and a slow, correct render. Both branches are tested; the sabotaged one was verified with `VK_LOADER_DRIVERS_DISABLE`. The pictures are the same pictures: mean absolute difference of half a level out of 255, confined to MSAA edges and to cloud shadow that already drifts between any two runs of the same renderer. Also corrects every comment across the four scripts that asserted three frames a second or twenty-two minutes a reel. Those were true when written and are not now, and the `reducedMotion` rationale in two of them was resting on the number: the real reason a chapter flight cannot be waited out is that it is ~40 frames however fast they are drawn, which was twenty seconds under software GL and is a shorter race on the GPU — still a race. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
68a5165d6f |
The hero gets its own film, shot bare and encoded for a page that autoplays it
`hero-soma` is the landing backdrop: SoMa, midnight to midnight, 180 frames at eight minutes each so the loop closes without repeating a minute. It is the only film shot `chrome: "bare"` — the figures keep the app's panel because its clock is their caption, but a hero has a headline on top and a second interface under that is two interfaces arguing. It is also the only one that runs a full 24 hours, because the page seeks it to the reader's local hour and a film starting at 04:40 has no frame for somebody at two in the morning. Whole-board was the first camera and was wrong: at that standoff the edge of the terrain plate stands against the sky across the top of the frame, and midday is a lot of pale sand. Encoding is two-stage now — frames to a CRF 18 master, deliverables derived from it. A time-lapse is near the worst case for inter-frame compression, because the camera never moves but every pixel changes as the light does, and at the figure default of CRF 22 this reel came out at 2.5 MB. Deriving from a master turned 'try CRF 28' into ten seconds instead of a twenty-two minute re-shoot, and it keeps what ships identical to what this script makes rather than something hand-rolled with ffmpeg the day the size became a problem. It lands at 1440w CRF 28 (1.0 MB) plus a 960w CRF 30 (402 kB) for small screens, because <video> has no srcset and a phone has no use for the wide encode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ff23f5d4bd |
Films reach the site the same way stills do
`films.mjs` now writes into the sibling lumbridge-v4 checkout — the MP4s and their posters into `apps/web/public/films/`, and a generated `films.ts` beside the shots manifest, carrying each reel's caption, alt text and running time. Same shape as `shots.mjs`, including the `FilmId` union that turns a page naming a deleted reel into a typecheck failure rather than a dead <video>. `--publish <dir>` puts an existing render in front of the site without re-shooting it. Twenty-two minutes a reel is long enough that the alternative would have been `cp`, and a hand-copied artefact is the thing this pipeline exists to not have. `--frames` refuses to publish at all: a 24-frame stutter is a rough cut, not something to put on a website by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b4eb2fa1a8 |
The engine can be filmed, by stepping its clock rather than recording it
`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> |
||
|
|
138a342c17 |
The frame matches the page's light rather than opposing it
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> |
||
|
|
d04a75b9bd |
Every shot is taken twice, and the site shows you the other one
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> |
||
|
|
d71e844703 |
The engine gets photographed, and the camera stops moving while the shutter is open
`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> |
||
|
|
37d5320664 |
Both doors get a share card, and they are not the same card
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.
|
||
|
|
44c5a79424 |
SoCal, the whole bay, a moon, and gates that actually run
Six agents in parallel, and the two city packs independently reported the same blocker: `focusRegions` and `coarseFactor` existed on the `City` type and nothing implemented them. Uniform lattices would have been 2.9M points for Southern California and 3.7M for the expanded bay. Both packs were unloadable as written. `buildAxis` is the answer, and it is honest about its limits: refinement is per axis, not per rectangle, so a focus region sharpens its whole row *and* its whole column. Two regions at opposite corners refine nearly everything between them. Measured, not guessed — the bay went 0.53M points with one region and 1.64M with three, for detail nobody is looking at from a board this wide. One region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s. Then three things that were only ever right because San Francisco was the only city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a 230-unit board; the bay is 1003 units across and the camera physically could not retreat far enough to frame it. Fog distances were scene units pinned to the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest weather, which over ninety-four kilometres of bay correctly hides three quarters of it — the night view was a black rectangle for a completely reasonable reason. All three now derive from the board. The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a physical ratio of one to four hundred thousand. What is being reproduced is what a moonlit night looks like on a screen in a lit room. The CI gate caught itself, which is the part worth keeping. Port 8431 was already held by a server from an earlier session, so the boot check polled a healthy stranger while the process it started died on EADDRINUSE. It now refuses to run rather than pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |