2aa4049258
**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>
247 lines
9.4 KiB
HTML
247 lines
9.4 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Tera / Spaces — share cards</title>
|
|
<!--
|
|
One template, two doors, for the same reason the app is one bundle behind
|
|
two names: `?card=tera` and `?card=office` differ in four strings and one
|
|
background, and two files would drift the moment somebody adjusted the
|
|
scrim on one of them.
|
|
|
|
No webfonts. The card is rasterised by a headless browser on a box that may
|
|
or may not have network at the time, and a card whose type silently falls
|
|
back to Times is worse than one that never used a webfont — so this uses
|
|
the same system stack the app itself uses (`ui-monospace` throughout, with
|
|
Georgia for the headline, which is the face `index.html`'s own no-JS
|
|
fallback already sets).
|
|
|
|
1200x630 is the size every unfurler crops toward: Twitter/X's
|
|
`summary_large_image`, Open Graph's own recommendation, Slack, Discord,
|
|
iMessage, LinkedIn. Nothing important goes within 48px of an edge, because
|
|
several of them shave a few pixels and one of them rounds the corners.
|
|
-->
|
|
<style>
|
|
html, body { margin: 0; padding: 0; background: #06080b; }
|
|
|
|
.card {
|
|
position: relative;
|
|
width: 1200px;
|
|
height: 630px;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
background: #06080b;
|
|
color: rgba(255, 255, 255, 0.92);
|
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
}
|
|
|
|
/* The art is the product, shot from the running app — see `capture.mjs`.
|
|
`cover` plus a per-card origin, because the two shots want cropping
|
|
toward different halves of themselves. */
|
|
.art {
|
|
position: absolute;
|
|
inset: 0;
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
}
|
|
|
|
/*
|
|
Two scrims, not one, and they do different jobs.
|
|
|
|
The horizontal one buys contrast for the type: the left 58% goes to
|
|
near-solid so a white headline sits on a known background rather than on
|
|
whatever pixel the render happened to put there — which is the failure
|
|
that makes screenshot-backed cards unreadable at timeline size. The
|
|
vertical one is a vignette that keeps the top and bottom edges from
|
|
competing with the corner text, and it is much weaker, because the whole
|
|
point is that you can still see the place.
|
|
*/
|
|
.scrim {
|
|
position: absolute;
|
|
inset: 0;
|
|
background:
|
|
linear-gradient(90deg, rgba(6, 8, 11, 0.97) 0%, rgba(6, 8, 11, 0.9) 38%,
|
|
rgba(6, 8, 11, 0.45) 62%, rgba(6, 8, 11, 0.12) 100%),
|
|
linear-gradient(180deg, rgba(6, 8, 11, 0.45) 0%, rgba(6, 8, 11, 0) 30%,
|
|
rgba(6, 8, 11, 0) 68%, rgba(6, 8, 11, 0.6) 100%);
|
|
}
|
|
|
|
/* The app's own hairline grid, at the app's own opacity. It is the one
|
|
piece of furniture both this and the lumbridgecorp.com card share, so
|
|
the two read as a family without being the same layout. */
|
|
.grid {
|
|
position: absolute;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
|
|
background-size: 48px 48px;
|
|
}
|
|
|
|
.body {
|
|
position: absolute;
|
|
inset: 0;
|
|
padding: 64px 72px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.eyebrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
font-size: 17px;
|
|
letter-spacing: 0.22em;
|
|
text-transform: uppercase;
|
|
color: #f2b134;
|
|
margin: 0 0 28px;
|
|
}
|
|
/* The same coin the lumbridgecorp.com card leads with. */
|
|
.coin {
|
|
width: 15px;
|
|
height: 15px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 34% 30%, #ffe8ac, #f2b134 48%, #d9932b 100%);
|
|
box-shadow: 0 0 16px rgba(242, 177, 52, 0.45);
|
|
}
|
|
|
|
h1 {
|
|
font-family: Georgia, "Times New Roman", serif;
|
|
font-weight: 400;
|
|
font-size: 68px;
|
|
line-height: 1.04;
|
|
letter-spacing: -0.022em;
|
|
margin: 0 0 26px;
|
|
max-width: 15ch;
|
|
color: #f4f6f9;
|
|
}
|
|
|
|
.sub {
|
|
font-size: 21px;
|
|
line-height: 1.5;
|
|
letter-spacing: 0.005em;
|
|
color: rgba(255, 255, 255, 0.62);
|
|
margin: 0;
|
|
max-width: 30ch;
|
|
}
|
|
|
|
/* Bottom left, on the baseline the padding already establishes. The host
|
|
is the one string on the card that is not a claim about anything. */
|
|
.host {
|
|
position: absolute;
|
|
left: 72px;
|
|
bottom: 56px;
|
|
font-size: 18px;
|
|
letter-spacing: 0.14em;
|
|
color: rgba(255, 255, 255, 0.5);
|
|
margin: 0;
|
|
}
|
|
.host b { color: #ffd68a; font-weight: 400; }
|
|
|
|
.licence {
|
|
position: absolute;
|
|
right: 72px;
|
|
bottom: 56px;
|
|
font-size: 15px;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: rgba(255, 255, 255, 0.34);
|
|
margin: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="art" id="art"></div>
|
|
<div class="scrim"></div>
|
|
<div class="grid"></div>
|
|
<div class="body">
|
|
<p class="eyebrow"><span class="coin"></span><span id="eyebrow"></span></p>
|
|
<h1 id="headline"></h1>
|
|
<p class="sub" id="sub"></p>
|
|
</div>
|
|
<p class="host">lumbridgecorp.com<b id="host"></b></p>
|
|
<p class="licence">Apache-2.0</p>
|
|
</div>
|
|
|
|
<script>
|
|
/*
|
|
Every string here is the project's own.
|
|
|
|
The headline for each door is the sentence `README.md` already uses to
|
|
say what that half is, and the sub-lines describe only what the code
|
|
actually does — a sun and moon computed locally, an office that renders
|
|
with no server at all, a licence that is in the repo root. A share card
|
|
is the most-read sentence a project has and the least reviewed, which is
|
|
exactly why it should not be the one place new claims get invented.
|
|
*/
|
|
const CARDS = {
|
|
tera: {
|
|
eyebrow: "Lumbridge Simulate",
|
|
headline: "Cities from above.",
|
|
// The last sentence is CONTRACT.md §0's acceptance test, in the words
|
|
// `main.ts` already uses for it.
|
|
sub: "A real sun and moon over a board you can fly around, and an office you can step into. Clone it and it works.",
|
|
host: "tera.",
|
|
art: "art-tera.png",
|
|
// Was "56% 88%", pinned low so the far edge of the board — where the
|
|
// terrain stopped and the sky began in a hard diagonal — stayed out of
|
|
// frame, because a card with that seam in it read as a rendering fault
|
|
// rather than as a map.
|
|
//
|
|
// There is no seam any more. The sea now runs eighteen board spans and
|
|
// ends past the fog's far plane, and the sky is a world-space dome
|
|
// rather than a screen-space gradient, so the world dissolves into the
|
|
// horizon instead of stopping at a visible edge. The framing is free to
|
|
// sit where the picture is best rather than where the defect was not.
|
|
//
|
|
// The state board is shot on a 2000x900 sensor, which is *wider* than
|
|
// the card, so `cover` crops horizontally and not at all vertically:
|
|
// the whole state is in frame top to bottom, and this number decides
|
|
// which two hundred pixels of ocean go. 0% keeps the western sea.
|
|
position: "0% 50%",
|
|
shift: 150,
|
|
},
|
|
office: {
|
|
eyebrow: "Lumbridge Spaces",
|
|
headline: "A studio you can walk around.",
|
|
sub: "Three buildings, real hours, the lights on at night, the hardware on the desk — rendered from a data file anybody can copy.",
|
|
host: "office.",
|
|
art: "art-office.png",
|
|
position: "62% 46%",
|
|
shift: 0,
|
|
},
|
|
};
|
|
|
|
const which = new URLSearchParams(location.search).get("card") ?? "tera";
|
|
const card = CARDS[which] ?? CARDS.tera;
|
|
document.getElementById("eyebrow").textContent = card.eyebrow;
|
|
document.getElementById("headline").textContent = card.headline;
|
|
document.getElementById("sub").textContent = card.sub;
|
|
document.getElementById("host").textContent = "";
|
|
document.querySelector(".host").innerHTML =
|
|
'<b>' + card.host + '</b>lumbridgecorp.com';
|
|
const art = document.getElementById("art");
|
|
art.style.backgroundImage = 'url("' + card.art + '")';
|
|
art.style.backgroundPosition = card.position;
|
|
/*
|
|
Slide the whole art layer right, and let the card clip it.
|
|
|
|
`cover` can crop a subject toward an edge; it cannot move a subject that
|
|
is already centred, because the offset it computes is bounded by the
|
|
overflow. The state board is centred in the app frame — the camera is —
|
|
and the left 58% of this card is scrim, so without this the headline sits
|
|
on top of California rather than beside it. Moving the layer instead of
|
|
the crop leaves the exposed strip at the far left showing the card's own
|
|
background, which is what the scrim was already painting there anyway.
|
|
*/
|
|
const shift = card.shift ?? 0;
|
|
art.style.left = shift + "px";
|
|
art.style.right = -shift + "px";
|
|
document.title = which;
|
|
</script>
|
|
</body>
|
|
</html>
|