From 270cddda3133d80a019eba53dd9ec80202be7b50 Mon Sep 17 00:00:00 2001 From: Kartios Date: Thu, 6 Aug 2026 02:32:19 -0700 Subject: [PATCH] The phone can reach the plan, read the disclosure, and see the room MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things, all on the door people actually open. office.lumbridgecorp.com is a link you send somebody, and a link somebody is sent is opened on a phone. **The plan had no way in.** The stylesheet builds it a whole bottom-sheet layout below 600px — full width above the rail, 38dvh tall, hover-only text dropped — and the only thing that could open it was the `M` key, which on a phone is not a control that is hard to find but a control that does not exist. The comment above that layout says "it is still behind `M`, so it costs nothing until it is asked for", and on a phone it could not be asked for. So there is a button, in the one cluster a thumb reaches, at every width rather than only the narrow ones — a control only a keyboard can reach is a control some people do not have, and that is as true at 1920 as at 390. It is glyph-only, because the `M` hint beside it already carries the word and two controls labelled "plan" in one bar is one too many, and it carries `aria-pressed` for the same reason the chapter buttons do: a toggle that does not look like its state is a button that appears to do nothing on every second press. **The disclosure was behind a hamburger.** "Sample occupancy — these people are invented" lived on `#office-badge` inside `#panel`, and on a phone `#panel` is a sheet that starts closed. The one sentence standing between twenty-five invented people at real desks and a screenshot presented as a staff list was hidden, on the device most likely to take the screenshot. It moves to `#source`, which is where this page already says what on screen is and is not real, is fixed and always drawn, and which the phone stylesheet itself calls "the one caption that is never allowed to be dropped for space". It replaces the city's liveness line while you are inside rather than joining it, because the markers, the sky and the traffic are facts about a board behind you — and "live data" printed over invented colleagues is the exact species of lie that wording was rewritten to stop telling. It also needed a rule of its own: `.source` on a phone is one nowrap ellipsised line, which rendered this as "sample occupancy · these people …", a truncation that removes the only word that mattered. A disclosure cut off mid-clause is worse than none, because it looks like a caption somebody bothered to write and nobody goes looking for the rest. **The room did not fit.** `camera.fov` is vertical, so visible width is `distance * tan(fov/2) * aspect`, and a phone held upright has an aspect near 0.5 against the 16:9 those viewpoints were framed by eye against. At the same distance that is a third of the width: the arrival shot put a thirty-four-metre floor plate off the corner of the screen under a frame full of empty sky. The pack was not wrong and the renderer was not wrong — the number meant something else on that screen. `poseFor` now preserves the width the author framed, which is the thing they were choosing; "everything in this building is somewhere in this frame" is a statement about width, and the extra height a tall screen throws in costs nothing. It only ever pushes back, never pulls in, so a window wider than 16:9 is untouched. The correction scales the whole offset from the target and not the ground distance alone, and that is the entire fix rather than a refinement. Multiplying only the distance does not step back from a shot, it flattens it: 32 m out and 14 m up became ninety-odd metres out and still fourteen up, a near-horizontal squint at the edge of a floor plate stranded near the horizon. I built that first and it was worse than the bug. Checked on an iPhone 13 viewport and at 1920x1080 and 1440x900. Phone: the whole floor centred at the authored angle with people at the benches, the disclosure on two lines and complete, the button tapped open and the plan sheet up with eight rooms named. Desktop 16:9 is pixel-identical — the correction is exactly 1 there — and the console is clean on all three. --- index.html | 17 ++++++++++ src/interiors/officeScene.ts | 57 ++++++++++++++++++++++++++++++++-- src/main.ts | 60 ++++++++++++++++++++++-------------- 3 files changed, 108 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index af198e2..1a6f50f 100644 --- a/index.html +++ b/index.html @@ -472,6 +472,10 @@ @media (hover: hover) { .help:hover { background: rgba(255, 255, 255, 0.16); color: var(--ink); } } + /* A toggle has to look like its state, or it is a button that appears to do + nothing on every second press. Same treatment the chapter and city + buttons take, for the same reason and from the same attribute. */ + .help[aria-pressed="true"] { background: rgba(242, 177, 52, 0.2); color: var(--amber-ink); } /* ---- Bottom left: where the numbers came from ------------------------- Shown only when the data is live — see `renderSource` in main.ts for why @@ -684,6 +688,9 @@ /* No keyboard, so no key hints — and with them gone the card around them is a full-width bar of glass holding one button. On a phone the hint *is* the `?` button. */ + /* The key hints go because there is no keyboard. The plan button is not + one of them and is not a `span`, so it stays — which is the whole + reason it exists. */ .hint span { display: none; } .hint { align-self: flex-end; @@ -707,6 +714,16 @@ overflow: hidden; text-overflow: ellipsis; } + /* Except when it is carrying the office's disclosure, which is a + sentence and not a label. "sample occupancy · these people …" is a + truncation that removes the only word that mattered, and a disclosure + cut off mid-clause is worse than none at all — it looks like a caption + somebody bothered to write, so nobody goes looking for the rest. It + gets the width and the second line. */ + body.sample-occupancy .source { + max-width: calc(100vw - var(--s3) * 2); + white-space: normal; + } /* The plan, as a bottom sheet. At 11.5rem in a corner it ate a fifth of a 375px map and its own affordances were about six pixels across — diff --git a/src/interiors/officeScene.ts b/src/interiors/officeScene.ts index 5b54baa..6aad42c 100644 --- a/src/interiors/officeScene.ts +++ b/src/interiors/officeScene.ts @@ -292,16 +292,67 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions): const floorY = plan.level(viewpoint.levelId)?.floorY ?? 0; const focus = viewpoint.focus; const TARGET_Y = 1.2; + /** + * The correction scales the whole offset from the target, not the ground + * distance alone, and the difference is the entire fix. + * + * Pushing the camera back while leaving `height` where it was does not step + * away from the shot, it *flattens* it: the reference pack's establishing + * view is 32 m out and 14 m up, a comfortable look down onto the floor, and + * multiplying only the 32 leaves the camera ninety-odd metres away and still + * fourteen up — a near-horizontal squint at the edge of a floor plate, + * stranded near the horizon with the bottom half of the frame empty. Tried + * it; it was worse than the bug. Scaling both preserves the elevation angle + * exactly, so the shot is the one the author framed, from further away. + */ + const k = widthCorrection(); + const above = Math.max(focus.height, TARGET_Y + 0.3) - TARGET_Y; return { target: new THREE.Vector3(focus.at.x, floorY + TARGET_Y, focus.at.z), position: new THREE.Vector3( - focus.at.x + Math.sin(focus.rotation) * focus.distance, - floorY + Math.max(focus.height, TARGET_Y + 0.3), - focus.at.z + Math.cos(focus.rotation) * focus.distance, + focus.at.x + Math.sin(focus.rotation) * focus.distance * k, + floorY + TARGET_Y + above * k, + focus.at.z + Math.cos(focus.rotation) * focus.distance * k, ), }; } + /** + * The aspect ratio a pack's `distance` was written against. + * + * Every viewpoint in `lumbridge-hq.ts` was framed by eye in a landscape + * browser, which makes 16:9 the honest reading of what those numbers mean — + * and this is the only place that reading is written down, so a pack author + * who wants to know what `distance: 32` promises can find out. + */ + const AUTHORED_ASPECT = 16 / 9; + + /** + * How much further back a narrow viewport has to stand. + * + * `camera.fov` is *vertical*, so the width you can see is + * `distance * tan(fov / 2) * aspect` — and a phone held upright has an aspect + * near 0.5 against the 1.78 the pack was written for. At the same distance + * that is a third of the width, which is exactly what the arrival shot looked + * like: a thirty-four-metre floor plate shoved off the corner of the screen + * with the top of the frame full of empty sky. The pack was not wrong and the + * renderer was not wrong; the number simply meant something else on that + * screen. + * + * So the correction preserves the *width* the author framed, which is the + * thing they were actually choosing — "everything in this building is + * somewhere in this frame" is a statement about width, and the extra height a + * tall screen throws in for free costs nothing. It only ever pushes back, + * never pulls in: a viewport wider than 16:9 already shows more than was asked + * for, and creeping closer to trim it would crop an establishing shot on a + * desktop to make a rule tidy. + */ + function widthCorrection(): number { + const aspect = kit.camera.aspect; + if (!(aspect > 0) || !Number.isFinite(aspect)) return 1; + return Math.max(1, AUTHORED_ASPECT / aspect); + } + /** * Where you arrive when the pack declares no viewpoints at all. * diff --git a/src/main.ts b/src/main.ts index 586e84c..32fae96 100644 --- a/src/main.ts +++ b/src/main.ts @@ -840,6 +840,7 @@ function watchOccupancy() { scene.setPresence(people); officePlan?.setPresence(people); renderOfficeBadge(); + renderSource(); }); } @@ -973,6 +974,35 @@ function renderLegend() { */ function renderSource() { if (!source) return; + /** + * The office's provenance line, which outranks the city's. + * + * It used to live on `#office-badge` inside `#panel`, and on a phone `#panel` + * is a bottom sheet that starts closed — so the one sentence standing between + * twenty-five invented people at real desks and a screenshot presented as a + * staff list was behind a hamburger, on the device most likely to take the + * screenshot. This slot is the right home for it anyway and not merely a + * visible one: `#source` is where this page already says what on screen is and + * is not real, it is fixed and always drawn, and the phone stylesheet calls it + * "the one caption that is never allowed to be dropped for space". + * + * It replaces the city's label rather than joining it, because while you are + * standing in the office the markers, the sky and the traffic are facts about + * a board behind you — and "live data" printed over invented colleagues is the + * exact species of lie the liveness wording was rewritten to stop telling. + */ + const sample = inside && presenceIsSample; + // A class rather than an inline style, so the stylesheet keeps the decision + // about how a phone lays this out and this keeps the decision about what it + // says. The rule it turns on is the one that stops the sentence being + // ellipsised down to the half that reads as reassuring. + document.body.classList.toggle("sample-occupancy", sample); + if (sample) { + const caption = "sample occupancy · these people are invented"; + if (source.textContent !== caption) source.textContent = caption; + source.hidden = false; + return; + } const label = describeLiveness({ markers: liveData, // An override is a sky somebody invented, so it retires the claim for as @@ -1041,29 +1071,11 @@ function renderCredits() { function renderOfficeBadge() { if (!officeBadge) return; const publicOffice = inside && office !== null && office.depth === "public"; - /** - * The other thing worth saying in this spot, and the more urgent of the two. - * - * The public badge above explains an absence. This one explains a *presence*, - * which is the direction that can actually mislead somebody: twenty-five - * invented names sitting at real desks look exactly like a staff list, and the - * only thing standing between that and a screenshot presented as one is this - * sentence. `sample.ts` says the same thing to a reader of the source; this - * says it to the person looking at the room. - * - * It is not suppressed on a real deployment whose API happened to be down, - * because that is precisely the case where it is needed: a `/presence` that - * did not answer falls back to the fabricated roster, and a member who is not - * told that will read invented people as their colleagues. - */ - const sampleOffice = inside && office !== null && office.depth === "full" && presenceIsSample; - officeBadge.hidden = !publicOffice && !sampleOffice; - if (sampleOffice) { - officeBadge.replaceChildren( - document.createTextNode("Sample occupancy — these people are invented."), - ); - return; - } + // The fabricated-occupancy caption used to be here too and is now on + // `#source` — see `renderSource`. This badge keeps the message that is a call + // to action rather than a disclosure, because that one belongs beside the + // office controls and survives being missed; the other one does not. + officeBadge.hidden = !publicOffice; if (!publicOffice) return; officeBadge.replaceChildren( document.createTextNode("Public view — the building, not the people. "), @@ -1269,6 +1281,8 @@ function togglePlan() { applyPlan(); } +planToggle?.addEventListener("click", () => togglePlan()); + panelToggle?.addEventListener("click", () => { panelOpen = !panelOpen; applyPanel();