Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion e2e/desk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
37 changes: 28 additions & 9 deletions src/scripts/desk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<HTMLElement>(".home-seal") as HTMLElement;
Expand All @@ -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 });
Comment thread
kanywst marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

introTimer is never reset when the timeout fires (line 567 only assigns it; only cancelIntro zeroes it). Once the 650 ms pull-back has run, introTimer stays truthy, so || introTimer here returns early for good. If the fonts arrive after 650 ms (slow network, the case this PR is for), the re-frame is skipped. The intro already computed overview() from the pre-font sizes, so the desk stays mis-framed.

Reset it inside the timeout callback:

introTimer = window.setTimeout(() => { introTimer = 0; go(overview(), { stiffness: 2.2 }); }, 650);

untouched already covers input. Also, halt() calls cancelIntro(), so a desk→list→desk toggle sets untouched = false without any camera input. That is harmless, but the flag then means "halted or touched", not "untouched".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 533abf0.

// Measure here rather than wait for the ResizeObserver, which may not have run yet.
measure();
drawMinimap();
go(openingView(), { stiffness: 2.2 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openingView() reads boxes, vw and vh, which are only refreshed by measure() or by the ResizeObserver (~line 604). document.fonts.ready resolves as a microtask when the font load finishes, which is before the next rendering step. ResizeObserver callbacks are delivered in that rendering step, after layout. So this handler can run while boxes still holds the pre-swap sizes. It would then re-frame from the same stale data and the fix would do nothing, depending on timing.

Suggested fix: re-measure here before framing, so it doesn't depend on observer ordering:

document.fonts.ready.then(() => {
  if (!html.classList.contains("desk") || !untouched || introTimer || liveFrom) return;
  measure();
  go(openingView(), { stiffness: 2.2 });
});

(measure() rebuilds the whole boxes map from offsetWidth/offsetHeight, which forces a synchronous layout and so picks up the swapped fonts.) The same applies to the intro timer callback at ~line 567: it runs 650ms after setMode, so it is probably fine there. Still, if you want the framing correct, measure() before overview() is cheap.

Minor: with the intro running, a font swap that lands after the timer has fired (introTimer === 0, untouched still true) re-targets the pull-back mid-flight. That is harmless, but it is a second animated move the visitor did not ask for. It is instant under reduced motion, because go honours it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9fcc82c: measure() and the minimap run before the re-frame.

});
Loading