The board gets a zoom dial, and its size is never left to chance
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
7f82aa47e0
commit
4430a0def1
+87
-12
@@ -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<HTMLDivElement | null>(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}
|
||||
<div class="board-frame">
|
||||
<div class="board-viewport" bind:this={boardViewportEl} style:--zoom={boardZoom}>
|
||||
<Board
|
||||
{view}
|
||||
edgeSelectMode={edgeSelectMode && yourMoment}
|
||||
@@ -1989,6 +2030,12 @@
|
||||
povCreatureId={prefs.liveFp && !net.spectating ? fpvBody : null}
|
||||
/>
|
||||
</div>
|
||||
<div class="zoom-controls" role="group" aria-label="board zoom">
|
||||
<button class="zoom-btn" title="zoom in" onclick={() => zoomBoard(1)} disabled={boardZoom >= 4}>+</button>
|
||||
<button class="zoom-btn" title="zoom out" onclick={() => zoomBoard(-1)} disabled={boardZoom <= 1}>−</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="paper-rail">
|
||||
@@ -2994,9 +3041,10 @@
|
||||
* keeps the pane out of the intrinsic sizing so the BOARD alone
|
||||
* decides how wide the column is. */
|
||||
.table-stack {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -3005,19 +3053,46 @@
|
||||
/* FPV play: the 3D view is the primary window into the game; the
|
||||
* top-down board shrinks to a keymap beneath it. */
|
||||
.table-stack.fpv-primary {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 64rem;
|
||||
}
|
||||
.board-zone .table-stack.fpv-primary :global(svg.board) {
|
||||
max-height: 30dvh;
|
||||
align-self: center;
|
||||
.board-frame { position: relative; min-width: 0; }
|
||||
/* The board's window: it owns the fitted size, the svg wears an
|
||||
* explicit width (fit × zoom), and anything past 1× scrolls. */
|
||||
.board-viewport {
|
||||
height: calc(100dvh - 21.5rem);
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
.board-zone :global(svg.board) {
|
||||
max-height: calc(100dvh - 21.5rem);
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
.fpv-primary .board-viewport { height: 30dvh; }
|
||||
.board-viewport :global(svg.board) {
|
||||
width: calc(var(--fit-w, 100%) * var(--zoom, 1));
|
||||
flex: none;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
height: auto;
|
||||
margin: auto;
|
||||
}
|
||||
.zoom-controls {
|
||||
position: absolute;
|
||||
right: 0.4rem;
|
||||
bottom: 0.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.zoom-btn {
|
||||
width: 1.7rem;
|
||||
height: 1.7rem;
|
||||
border: 1px solid #8a7a5c;
|
||||
border-radius: 4px;
|
||||
background: rgba(233, 225, 203, 0.9);
|
||||
color: #43331f;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.zoom-btn:disabled { opacity: 0.35; cursor: default; }
|
||||
.paper-rail { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0; }
|
||||
|
||||
.slip {
|
||||
@@ -3501,8 +3576,8 @@
|
||||
.board-zone { order: 1; }
|
||||
.table-edge { order: 2; margin-top: 0.6rem; }
|
||||
.paper-rail { order: 3; margin-top: 0.9rem; }
|
||||
.board-zone :global(svg.board) {
|
||||
max-height: 56dvh;
|
||||
.board-viewport {
|
||||
height: 56dvh;
|
||||
}
|
||||
.chronicle { max-height: 30dvh; flex: initial; }
|
||||
.masthead { gap: 0.45rem; padding-top: 0.5rem; }
|
||||
|
||||
Reference in New Issue
Block a user