From 4430a0def15b5c5f4de8ee685beea2a36600640e Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sat, 29 Aug 2026 22:57:55 -0400 Subject: [PATCH] The board gets a zoom dial, and its size is never left to chance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Players wanted to magnify crowded squares; the board now lives in a scrolling viewport with +/− controls (1x fit to 4x), zoom centered on the view. The svg's width is always set explicitly — fit × zoom, remeasured by a ResizeObserver — which also cures the stale small board left behind when the first-person pane closed: auto-sizing against shifting CSS constraints is gone entirely. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/web/src/App.svelte | 99 ++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 291d631..4998bc8 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -100,6 +100,45 @@ if (!prefs.cautions || reasons.length === 0) go(); else caution = { reasons, go }; } + /** Board zoom: 1 = fitted to the viewport; higher magnifies for crowded + * squares, with the viewport scrolling. The svg's width is always set + * explicitly (fit × zoom) from measurement — auto-sizing left stale + * dimensions behind when the first-person pane closed. */ + let boardViewportEl = $state(null); + let boardZoom = $state(1); + function zoomBoard(dir: number) { + const el = boardViewportEl; + const prev = boardZoom; + const next = dir > 0 ? prev * 1.4 : prev / 1.4; + boardZoom = next < 1.05 ? 1 : Math.min(4, Math.round(next * 100) / 100); + if (el && boardZoom !== prev) { + const f = boardZoom / prev; + requestAnimationFrame(() => { + el.scrollLeft = (el.scrollLeft + el.clientWidth / 2) * f - el.clientWidth / 2; + el.scrollTop = (el.scrollTop + el.clientHeight / 2) * f - el.clientHeight / 2; + }); + } + } + function fitBoard(el: HTMLDivElement) { + const svg = el.querySelector("svg.board") as SVGSVGElement | null; + const vb = svg?.viewBox.baseVal; + if (!vb || !vb.width || !vb.height) return; + const fitW = Math.min(el.clientWidth, (el.clientHeight * vb.width) / vb.height); + el.style.setProperty("--fit-w", `${Math.max(0, fitW)}px`); + } + $effect(() => { + const el = boardViewportEl; + if (!el) return; + const ro = new ResizeObserver(() => fitBoard(el)); + ro.observe(el); + return () => ro.disconnect(); + }); + $effect(() => { + void view; // the board's viewBox grows with ghost slots and relocations + const el = boardViewportEl; + if (el) requestAnimationFrame(() => fitBoard(el)); + }); + /** A move with its regret checks: stepping off your home still laden, * or stepping onto ground that bites. */ function tryMove(direction: Side, over?: boolean) { @@ -1965,6 +2004,8 @@ : { type: "warpStep" })} edgeSelect={edgeSelectMode && yourMoment} /> {/if} +
+
+
+ + +
+
+