1
0

A plan view in the corner, a night you can actually see, and three kinds of visitor

The right half of the screen was empty sky. It holds the board now, drawn flat,
with the footprint of the camera's own frustum on it — the one part of a minimap
that earns its place, because it answers "where am I looking from" without
leaving the shot. Click it, drag it, scroll it. It is a 2D canvas rather than a
second WebGL context, cached per city and redrawn only when something moved.

Night was black. Not dark — black: at 3 a.m. the coastline, the hills and the
bay were one shape, and the frame read as a failed render rather than as
darkness. The sky already had a floor for exactly this reason and nothing did
the equivalent for the ground, so the ground has one now. The moon still has to
be worth computing, so the gap between a moonlit night and a moonless one is
preserved rather than filled in.

Three tiers, resolved once in the new src/access.ts: anonymous, signed in,
admin. Anonymous gets the map and a public office — the shell, the furniture,
the named viewpoints, nobody home — built without the private objects rather
than with them hidden, because scene.traverse makes hiding a leak with a bow on
it. The time scrubber and the debug readouts are admin only, and admin is
granted by TERA_ADMIN_SUBJECTS on the server and inferred nowhere else. An
unreachable API means member, never god: the promise is "clone it and it works",
not "clone it and you are an administrator of a deployment you did not
configure".

Three things this run found and fixed rather than shipped:

  - entryUrl came off the wire and went straight into an href with no scheme
    check, and a CSP of script-src 'self' 'unsafe-inline' does not stop a
    javascript: URL from navigating. One rejection point in access.ts now.
  - A 5xx from /health was the same null as "no API at all" and therefore the
    opposite conclusion. Eight seconds of tera-api restarting would have told
    every anonymous visitor they were a member. A 5xx is an answer; it fails
    closed.
  - decodeURIComponent in cookieToken was the one path in auth/index.ts that
    threw rather than returning ANONYMOUS, so one malformed cookie header from
    an unauthenticated caller turned /api/v1/session into a 500.

Also: keyboard shortcuts, focus rings, a boot state instead of a blank 2.3
seconds, a collapsible panel under 900px, and no horizontal overflow at 375,
768, 1440 or 2560.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-05 22:53:30 -07:00
parent 47faec9f9d
commit 5bc7258753
24 changed files with 3982 additions and 211 deletions
+37 -9
View File
@@ -113,8 +113,23 @@ const OFFICE_LIT = 0.24;
const WINDOW_WARM = 0xffc178;
const WINDOW_COOL = 0xd8e4ff;
/** Peak emissive radiance of a lit pane. Below 1 so a window is bright, not blown. */
const WINDOW_GAIN = 0.8;
/**
* Peak emissive radiance of a lit pane. Below 1 so a window is bright, not blown.
*
* 0.8 when the ground under the city was effectively black, 0.95 now that
* `atmosphere.ts` holds a real floor under a moonless night. That floor moved
* the terrain from about #000004 to something you can find a coastline in, and
* a window has to stay the brightest thing in the frame by a comfortable factor
* or the whole picture stops being a city at night and becomes a city at dusk.
* It is the *ratio* that is being defended here, not the absolute value.
*
* Still under 1, and that is not an accident: at 1.0 the emissive term alone
* saturates the channel and a lit pane clips to white, taking `WINDOW_WARM` with
* it. A skyline whose windows have lost the difference between tungsten and a
* ceiling fluorescent is a skyline with the character taken out of it, and there
* is no HDR buffer here to get it back from.
*/
const WINDOW_GAIN = 0.95;
/** Sodium, because a street lamp is the one light in a city that still is. */
const LAMP_COLOR = 0xffb264;
@@ -372,14 +387,22 @@ if (uNight > 0.002) {
float footprint = max(fwidth(grid.x), fwidth(grid.y));
float detail = 1.0 - smoothstep(0.5, 1.4, footprint);
// 0.56 x 0.38 is the pane inside its cell, so 0.2128 x chance is the grid's
// own mean — and 1.8 times that is what is actually used, which is a lie
// own mean — and 2.6 times that is what is actually used, which is a lie
// worth being explicit about. The mean is the right answer for a display
// whose response is linear, and no display's is: a pixel that in reality
// contains one small blazing window and three dark ones does not read to
// the eye as the average of the four, it reads as lit. With no HDR buffer
// and no bloom to arrive at that honestly, the multiplier is the cheap way
// to keep the far city as bright as the near city says it ought to be.
float glow = mix(1.8 * 0.2128 * chance, coverage * on, detail);
//
// 1.8 for as long as the ground was black, because against black anything
// reads. This is the branch the whole-board framing takes — every pixel of
// the city is past the fwidth cutoff from up there — so it is also the
// branch that had to answer when the atmosphere's night floor brought the
// terrain up to meet it. At 1.8 against the new floor the lit grid and the
// bare ground came out at the same luminance and downtown stopped being
// findable, which is a worse bug than the one being fixed.
float glow = mix(2.6 * 0.2128 * chance, coverage * on, detail);
// Roughly seven windows in ten warm. A skyline is mostly people's lamps and
// only partly the floors the cleaners are still on.
@@ -553,17 +576,22 @@ function smoothstep(edge0: number, edge1: number, x: number): number {
* - **-8° and below**: 1.0. The lights stopped changing some minutes ago;
* what changed after that was the sky behind them.
*
* Downtown's mean emission at distance is 1.8 x 0.2128 x 0.24 x 0.8 = 0.074,
* against the avenues' 0.025 — a ratio of just under 3:1, which is the whole
* Downtown's mean emission at distance is 2.6 x 0.2128 x 0.24 x 0.95 = 0.126,
* against the avenues' 0.043 — a ratio of just under 3:1, which is the whole
* picture, since the thing that makes a night skyline is not that the towers
* are taller but that they are the part of the city with all its lights still
* on. Around each of those figures the per-building variation spans 0.15x to
* 2.7x, so a run of towers has dark ones in it and the odd one blazing, and the
* financial district does not smear into a single rectangle when you pull back.
*
* On a moonless night the buildings come out at about #4a403d against water at
* #191b21 and a sky at #2d3855: the city is the brightest thing in the frame,
* as it should be, and the sky is still visibly a sky.
* On a moonless night, whole-board framing, measured off the render: the Bay
* Area board puts downtown at about y34 mean and its brightest windows past
* y130, against land at y23, bay water at y15 and sky at y29; the SoCal board,
* which has no marine layer over it, comes out at y36 / y137 against land y34,
* ocean y13 and sky y15. The city is comfortably the brightest thing in the
* frame in both, which is the relationship that has to hold — and it stopped
* holding, briefly, when `atmosphere.ts` first raised the ground under it.
* That is what the 2.6 and the 0.95 are for.
*
* SF's twenty-nine roads at 55 m spacing come to 12,038 lamps in one draw call,
* comfortably under the 24,000 ceiling. The ceiling exists for the city pack