diff --git a/ui/src/game/Board.svelte b/ui/src/game/Board.svelte index 1505d17..7288220 100644 --- a/ui/src/game/Board.svelte +++ b/ui/src/game/Board.svelte @@ -105,24 +105,27 @@ prevZoom = on; prevRecenter = rc; if (landscape) { - // Height-driven board in a wide viewport: pan so the focused cell is centred. Interpolate - // BOTH scroll axes together by TIME over the board's grow/shrink transition; the browser - // clamps each to the current scrollable range as the board resizes. The earlier per-frame - // clamp-to-max reached the wide axis (large clientWidth) before the tall one, which the owner - // saw as the board positioning "in two steps" — a single time-eased motion moves the axes in - // sync. Time-based (not a scrollWidth-progress tween like portrait's) so it also settles on - // zoom-out, where the board shrinks below the viewport and stops overflowing. + // A wide viewport overflows VERTICALLY as soon as the square board grows past its height, but + // HORIZONTALLY only once the board grows past the (much larger) width — so a per-axis scroll + // moves the vertical axis early and the horizontal axis late (the browser pins scrollLeft to 0 + // until the board is wider than the viewport): the board positioning "in two steps". Drive + // BOTH axes by one progress q — how far the board has grown past the viewport width, the point + // at which a horizontal pan first becomes possible. Until then q is 0 and the board simply + // zooms centred; past it both axes pan together in one diagonal motion. The full-zoom focus + // target is finalSL/ST on zoom-in and the current position on zoom-out (where final is 0), so + // q running 1→0 unwinds the scroll before the board shrinks back below the viewport. if (toggled || (recentered && on && f)) { - const startSL = vp.scrollLeft; - const startST = vp.scrollTop; - const t0 = performance.now(); - const dur = 250; // ~the .scaler.land width/height transition (0.25s ease) - let raf = requestAnimationFrame(function tick(now: number) { - const p = Math.min(1, (now - t0) / dur); - const e = 1 - Math.pow(1 - p, 3); // easeOutCubic, close to the CSS ease curve - vp.scrollLeft = startSL + (finalSL - startSL) * e; - vp.scrollTop = startST + (finalST - startST) * e; - if (p < 1) raf = requestAnimationFrame(tick); + const scaler = vp.firstElementChild as HTMLElement | null; + const targetSL = on ? finalSL : vp.scrollLeft; + const targetST = on ? finalST : vp.scrollTop; + const span = Math.max(1, fullSide - clientW); + let raf = requestAnimationFrame(function tick() { + const side = scaler ? scaler.getBoundingClientRect().width : fullSide; + const q = Math.max(0, Math.min(1, (side - clientW) / span)); + vp.scrollLeft = targetSL * q; + vp.scrollTop = targetST * q; + const settled = on ? side >= fullSide - 1 : side <= fitSide + 1; + if (!settled) raf = requestAnimationFrame(tick); }); return () => cancelAnimationFrame(raf); } @@ -356,6 +359,9 @@ /* Clamp the pan at the board edge: kill the native rubber-band/overscroll so the zoomed board cannot be dragged past its content into empty space — it just stops at the edge. */ overscroll-behavior: none; + /* The zoom effect drives scrollLeft/Top itself while the board grows/shrinks; turn off the + browser's scroll-anchoring so it does not fight those writes mid-transition. */ + overflow-anchor: none; } /* The query container is the (zoom-scaled) board, so cqw labels scale WITH the board — a magnifying-glass zoom. */ diff --git a/ui/src/game/Rack.svelte b/ui/src/game/Rack.svelte index a622342..71ce0d4 100644 --- a/ui/src/game/Rack.svelte +++ b/ui/src/game/Rack.svelte @@ -121,9 +121,12 @@ container-type: inline-size; } .rack.landscape .tile { - flex: 1 1 0; - width: auto; - min-width: 0; + /* Fixed tile size (1/7 of the fixed-width rack, accounting for the six 5px gaps), NOT flex-grow: + so placing a tile leaves the remaining ones put instead of growing them to refill the row — + matching the portrait rack. The constant rack width also keeps the cqw glyphs above steady as + tiles leave. */ + flex: 0 0 auto; + width: calc((100% - 6 * 5px) / 7); max-width: 46px; } .rack.landscape .letter {