b7f5c41da5892dd0aeff95f92ef6efe439e8c2c8
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2aa4049258 |
feat(brand): re-shoot everything at the new engine, and make it one command
**Seven new shots**, because the world grew the most photogenic things in it
after the last pass: `sfo`, `lax`, `golden-gate`, `bay-bridge`, `freeway`,
`california-relief` and `pacific-sea`. All nine existing ids are unchanged — the
manifest emits a `ShotId` union that lumbridge-v4 imports, so ids are added and
never renamed.
**A shot can now aim itself.** The chapter list has no camera for SFO, LAX, the
bridges, the freeway or the open sea, and the engine has no `?pose=` back door,
so a shot points itself by driving the app's own inputs: a click on the plan view
slides the orbit target to a lat/lng while keeping the chapter's stance, wheel
notches set the standoff, and a drag sets azimuth and elevation. The plan view's
pixel-to-coordinate map is solved at runtime from three hovers of
`#minimap-readout` rather than hard-coded, so it survives a board resize or a
restyle of the widget.
**The tera share card was a picture of the wrong thing, and had been.** Its art
came from `keyboard.press("2")`, which had landed on the California board's drive
mode once the default board changed — so the card under the headline "Cities from
above." was a chase camera on US-101, showing metre-scale cars driving between
kilometre-wide buildings, with the DRIVE readout and the mode pill baked into the
art. It renders, it looks deliberate, and it is why an unguarded key press has no
place in a capture script. `capture.mjs` now clicks an indexed chapter and
asserts its `shortLabel` the way `shots.mjs` does, waits on `#boot` and
`#chapters` instead of sleeping twenty seconds, and gives each card its own hour.
**`npm run refresh` is the durable half.** One command: build, stills, cards,
films, both manifests, and a hashed before/after diff of every deliverable. It
fails loudly and specifically on the two conditions that otherwise produce
confident wrong output — the renderer coming up as SwiftShader, and a chapter
`expect` guard firing. `--stills-only` / `--cards-only` / `--films-only` compose,
`--dry-run` lists the plan without opening a browser, and
`shots.mjs --list` prints the whole shot plan — board, chapter, expect, aim, both
hours — which is what to run first when a guard does fire.
It also re-stamps `PROVENANCE.json`, narrowly: only entries whose origin is
`repository-generated` and whose `generator` names a script the run actually
executed, by literal hash substitution rather than re-serialising the file.
Without that, every legitimate card re-shoot leaves `npm run provenance` red.
**Every film re-shot.** They were at `9c9e78f`, captured 2026-08-07, and predated
the tone mapping, the reflective sea, the sky dome, terrain shadows, the rebuilt
California board, SFO, LAX, both bridges and the moving aircraft.
Tests 1137, typecheck, build, eight budget cells and every provenance and licence
check pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3c4649d078 |
fix: make the app boot on an insecure origin, and re-aim the capture harness
Three things, all found by trying to re-shoot the product imagery and failing.
**`crypto.randomUUID` is secure-context only.** `main.ts` called it at module top
level for its three wire identities, so on any origin that is not HTTPS and not
`localhost` the call threw before the scene was built and the app stopped at
"Starting up" with one TypeError and no other symptom. Every way a developer
normally opens this app is a secure context — `vite dev` and `vite preview` serve
localhost, the deployed site is HTTPS — which is why this survived since
|
||
|
|
9c9e78f6f9 |
The Southland gets a door, and the people in it move
**Mateo Court**, a courtyard block in the Arts District — a third pack and a third *kind* of building. Two office floors and a shed already existed, so this is the plan the format had not been shown: a ring of rooms round an open-air yard with **no corridor anywhere**. Every door opens onto the yard, and the yard does the job a corridor does in the other two. It is also the first pack whose `ceiling: null` means there is genuinely no ceiling at any height rather than "take the lid off so the shot can see in", and the first sited off the Bay Area board at all. That last part needed a fix, not just a coordinate. `OFFICE_MARKERS` was gated on `id === "sf"`, which was correct for exactly as long as every office was in the Bay Area — it would have kept the Los Angeles building off the Los Angeles board and pinned it to San Francisco's. The gate is the board's own bounds now, which is the same question asked honestly. Every door through the 0.25 m courtyard skin is 1.2 m rather than the usual 0.9, and that is not a style choice: `blocked` inflates by the walker's radius *and* the wall's thickness, so a 0.9 m leaf through a wall that thick seals the room behind it while rendering perfectly. Two rooms were sealed exactly that way on the first pass. The test says so, because a well-meaning edit back to 0.9 for consistency would do it again. **People move.** `samplePresenceAt` thinned the roster by hour, which fixed a building that was full at 1 a.m., but everybody was still pinned to their own desk all day. Occupancy is a hand-written booking table now: meetings fill a room for a plausible length, the kitchen island fills at lunch, and a `Presence` binds to a seat id — so "in a meeting" means occupying a meeting-room seat, and the whole thing is choosing seat ids rather than inventing positions. The flood fill that checks a pack is walkable existed three times over. One copy now, since the third is where a divergence lives. **Films.** A new `office-dusk` reel — six hours over Lumbridge HQ catching the moment the house lights take over from the sun, which is the one thing only a time-lapse can show and which did not exist. The three existing reels are re-rendered rather than re-captioned: `films.ts` in v4 named a commit eleven behind HEAD, so the published reels were shot before there were any clouds and before the city drew office pins. Reviewers caught roughly a dozen false statements in the new prose across these files — a room census that did not add up, a wall-thickness count, a claim that every room has daylight when one does not, and a cost figure saying `updateSun` runs once a second when it runs once a minute. The consequential ones are fixed. In a codebase where the comments are the design record, a confident wrong number is a defect. 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> |