From beecd9af95537c6bed0cbee3a2c8c7a760581bc1 Mon Sep 17 00:00:00 2001 From: kanywst Date: Sat, 24 Oct 2026 15:26:40 +0900 Subject: [PATCH 1/6] perf(desk): show the desk without waiting for web fonts The desk stayed hidden until document.fonts.ready, so on a slow connection the home seal (the page's largest image) had already loaded and then waited about 1.7 s to paint. Items are re-measured by the ResizeObserver as fonts swap in, and the intro's final framing is computed when it is used. --- src/scripts/desk.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/scripts/desk.ts b/src/scripts/desk.ts index 385685d..60cf6f1 100644 --- a/src/scripts/desk.ts +++ b/src/scripts/desk.ts @@ -548,7 +548,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 = () => (narrow() ? shelfView("home") : fitBox(everything())); 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 +561,9 @@ 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(() => go(overview(), { stiffness: 2.2 }), 650); } else { - go(overview, { instant: true }); + go(overview(), { instant: true }); } } @@ -606,9 +607,7 @@ 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"); -} -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"); From 38b5ca616c9c885962ae33753aeb463b48ae5d97 Mon Sep 17 00:00:00 2001 From: kanywst Date: Sat, 24 Oct 2026 15:32:10 +0900 Subject: [PATCH 2/6] fix(desk): re-frame the opening view once the web fonts have loaded Without the fonts.ready wait, the instant (and reduced-motion) opening fit was computed from fallback-font sizes and never revisited. If the visitor hasn't moved yet, the desk is framed again from the real sizes. Raised in Claude review on #7. --- src/scripts/desk.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/scripts/desk.ts b/src/scripts/desk.ts index 60cf6f1..de4e8ea 100644 --- a/src/scripts/desk.ts +++ b/src/scripts/desk.ts @@ -105,11 +105,14 @@ 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. function cancelIntro() { clearTimeout(introTimer); introTimer = 0; + untouched = false; } // Stops the camera outright: no frame, no pending intro, no promise left waiting. @@ -549,7 +552,7 @@ function setMode(mode: "desk" | "list", intro = false) { measure(); drawMinimap(); // Computed when needed, not now: web fonts may still be swapping in and resizing items. - const overview = () => (narrow() ? shelfView("home") : fitBox(everything())); + 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; @@ -607,7 +610,17 @@ const resized = new ResizeObserver((entries) => { }); for (const el of items) resized.observe(el); +function openingView(): Cam { + return narrow() ? shelfView("home") : fitBox(everything()); +} + // 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; + go(openingView(), { stiffness: 2.2 }); +}); From 533abf0bdf1f479fe2e2bc00fae7fc5c2e74d804 Mon Sep 17 00:00:00 2001 From: kanywst Date: Sat, 24 Oct 2026 15:37:24 +0900 Subject: [PATCH 3/6] fix(desk): clear the intro timer once it has fired A fired timer stayed truthy, so the post-font re-frame was skipped whenever the fonts arrived after the intro, which is the slow-network case it exists for. Raised in Claude review on #7. --- src/scripts/desk.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scripts/desk.ts b/src/scripts/desk.ts index de4e8ea..e8469c7 100644 --- a/src/scripts/desk.ts +++ b/src/scripts/desk.ts @@ -564,7 +564,10 @@ 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 }); } From 9fcc82c7df4cb8934bb7612f2bd780c2aad9cb4d Mon Sep 17 00:00:00 2001 From: kanywst Date: Sat, 24 Oct 2026 19:22:29 +0900 Subject: [PATCH 4/6] fix(desk): measure before re-framing once the fonts arrive The ResizeObserver might not have run yet when fonts.ready resolves. Raised in Claude review on #7. --- src/scripts/desk.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/scripts/desk.ts b/src/scripts/desk.ts index e8469c7..799d64e 100644 --- a/src/scripts/desk.ts +++ b/src/scripts/desk.ts @@ -625,5 +625,8 @@ html.classList.add("ready"); // 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 }); }); From cc41f04f0ccc8e78074438b975d7bf17165ecdef Mon Sep 17 00:00:00 2001 From: kanywst Date: Sat, 24 Oct 2026 19:26:26 +0900 Subject: [PATCH 5/6] test(e2e): don't wait on external board images in the no-JS test --- e2e/desk.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) { From 7bec9a55c06ecb0ff0fcd69e138efe696368ca13 Mon Sep 17 00:00:00 2001 From: kanywst Date: Sat, 24 Oct 2026 23:16:15 +0900 Subject: [PATCH 6/6] docs(desk): name both unrequested camera moves in the comment --- src/scripts/desk.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scripts/desk.ts b/src/scripts/desk.ts index 799d64e..0b2572d 100644 --- a/src/scripts/desk.ts +++ b/src/scripts/desk.ts @@ -108,7 +108,8 @@ 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;