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();
|
if (!prefs.cautions || reasons.length === 0) go();
|
||||||
else caution = { reasons, 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,
|
/** A move with its regret checks: stepping off your home still laden,
|
||||||
* or stepping onto ground that bites. */
|
* or stepping onto ground that bites. */
|
||||||
function tryMove(direction: Side, over?: boolean) {
|
function tryMove(direction: Side, over?: boolean) {
|
||||||
@@ -1965,6 +2004,8 @@
|
|||||||
: { type: "warpStep" })}
|
: { type: "warpStep" })}
|
||||||
edgeSelect={edgeSelectMode && yourMoment} />
|
edgeSelect={edgeSelectMode && yourMoment} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="board-frame">
|
||||||
|
<div class="board-viewport" bind:this={boardViewportEl} style:--zoom={boardZoom}>
|
||||||
<Board
|
<Board
|
||||||
{view}
|
{view}
|
||||||
edgeSelectMode={edgeSelectMode && yourMoment}
|
edgeSelectMode={edgeSelectMode && yourMoment}
|
||||||
@@ -1989,6 +2030,12 @@
|
|||||||
povCreatureId={prefs.liveFp && !net.spectating ? fpvBody : null}
|
povCreatureId={prefs.liveFp && !net.spectating ? fpvBody : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
<aside class="paper-rail">
|
<aside class="paper-rail">
|
||||||
@@ -2994,9 +3041,10 @@
|
|||||||
* keeps the pane out of the intrinsic sizing so the BOARD alone
|
* keeps the pane out of the intrinsic sizing so the BOARD alone
|
||||||
* decides how wide the column is. */
|
* decides how wide the column is. */
|
||||||
.table-stack {
|
.table-stack {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -3005,19 +3053,46 @@
|
|||||||
/* FPV play: the 3D view is the primary window into the game; the
|
/* FPV play: the 3D view is the primary window into the game; the
|
||||||
* top-down board shrinks to a keymap beneath it. */
|
* top-down board shrinks to a keymap beneath it. */
|
||||||
.table-stack.fpv-primary {
|
.table-stack.fpv-primary {
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
}
|
}
|
||||||
.board-zone .table-stack.fpv-primary :global(svg.board) {
|
.board-frame { position: relative; min-width: 0; }
|
||||||
max-height: 30dvh;
|
/* The board's window: it owns the fitted size, the svg wears an
|
||||||
align-self: center;
|
* 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) {
|
.fpv-primary .board-viewport { height: 30dvh; }
|
||||||
max-height: calc(100dvh - 21.5rem);
|
.board-viewport :global(svg.board) {
|
||||||
width: auto;
|
width: calc(var(--fit-w, 100%) * var(--zoom, 1));
|
||||||
max-width: 100%;
|
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; }
|
.paper-rail { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0; }
|
||||||
|
|
||||||
.slip {
|
.slip {
|
||||||
@@ -3501,8 +3576,8 @@
|
|||||||
.board-zone { order: 1; }
|
.board-zone { order: 1; }
|
||||||
.table-edge { order: 2; margin-top: 0.6rem; }
|
.table-edge { order: 2; margin-top: 0.6rem; }
|
||||||
.paper-rail { order: 3; margin-top: 0.9rem; }
|
.paper-rail { order: 3; margin-top: 0.9rem; }
|
||||||
.board-zone :global(svg.board) {
|
.board-viewport {
|
||||||
max-height: 56dvh;
|
height: 56dvh;
|
||||||
}
|
}
|
||||||
.chronicle { max-height: 30dvh; flex: initial; }
|
.chronicle { max-height: 30dvh; flex: initial; }
|
||||||
.masthead { gap: 0.45rem; padding-top: 0.5rem; }
|
.masthead { gap: 0.45rem; padding-top: 0.5rem; }
|
||||||
|
|||||||
Reference in New Issue
Block a user