diff --git a/e2e/desk.spec.ts b/e2e/desk.spec.ts index 1a13c92..4c28125 100644 --- a/e2e/desk.spec.ts +++ b/e2e/desk.spec.ts @@ -219,7 +219,8 @@ test.describe("without JavaScript", () => { test.use({ javaScriptEnabled: false }); test("everything is still there as a list", async ({ page }) => { - await page.goto("/"); + // Only the markup matters here; don't wait on board images from raw.githubusercontent.com. + await page.goto("/", { waitUntil: "domcontentloaded" }); await expect(page.locator("html")).toHaveClass(/\blist\b/); await expect(page.getByRole("heading", { level: 1, name: "0-draft" })).toBeVisible(); for (const r of repos) { diff --git a/src/scripts/desk.ts b/src/scripts/desk.ts index 385685d..0b2572d 100644 --- a/src/scripts/desk.ts +++ b/src/scripts/desk.ts @@ -105,11 +105,15 @@ function go(to: Cam, opts: { instant?: boolean; stiffness?: number } = {}): Prom let raf = 0; let last = 0; let introTimer = 0; +// Until the visitor moves the camera, the opening framing is still ours to correct. +let untouched = true; -// The intro's pull-back is the only motion the visitor didn't ask for; any input cancels it. +// The intro's pull-back, and the re-frame once fonts arrive, are the only motion the visitor +// didn't ask for; any input cancels both. function cancelIntro() { clearTimeout(introTimer); introTimer = 0; + untouched = false; } // Stops the camera outright: no frame, no pending intro, no promise left waiting. @@ -548,7 +552,8 @@ function setMode(mode: "desk" | "list", intro = false) { place(); measure(); drawMinimap(); - const overview = narrow() ? shelfView("home") : fitBox(everything()); + // Computed when needed, not now: web fonts may still be swapping in and resizing items. + const overview = openingView; if (intro && !reduced.matches) { // Start nose to nose with the seal, then pull back to show the whole desk. const seal = document.querySelector(".home-seal") as HTMLElement; @@ -560,9 +565,12 @@ function setMode(mode: "desk" | "list", intro = false) { h: seal.offsetHeight * 0.4, }; go(fitBox(face, 0, 0, 0), { instant: true }); - introTimer = window.setTimeout(() => go(overview, { stiffness: 2.2 }), 650); + introTimer = window.setTimeout(() => { + introTimer = 0; + go(overview(), { stiffness: 2.2 }); + }, 650); } else { - go(overview, { instant: true }); + go(overview(), { instant: true }); } } @@ -606,9 +614,20 @@ const resized = new ResizeObserver((entries) => { }); for (const el of items) resized.observe(el); -async function start() { - await document.fonts.ready; - setMode(recalled() === "list" ? "list" : "desk", true); - html.classList.add("ready"); +function openingView(): Cam { + return narrow() ? shelfView("home") : fitBox(everything()); } -start(); + +// No waiting for web fonts: the desk shows as soon as the script runs (it holds the page's +// largest image), and the ResizeObserver above re-measures items as the fonts swap in. +setMode(recalled() === "list" ? "list" : "desk", true); +html.classList.add("ready"); +// Once the fonts have swapped in, frame the desk again from the real sizes, unless the visitor +// has already moved, a site is open, or the intro is still about to compute it anyway. +document.fonts.ready.then(() => { + if (!html.classList.contains("desk") || !untouched || introTimer || liveFrom) return; + // Measure here rather than wait for the ResizeObserver, which may not have run yet. + measure(); + drawMinimap(); + go(openingView(), { stiffness: 2.2 }); +});