From 8ec71a68161e73d7ad2125b667910e2c13c74534 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 3 Jun 2026 16:35:39 +0200 Subject: [PATCH] Stage 7 UI polish: zoom-in magnifies into the focus cell (no top-left jump) Drive the focus-centring with requestAnimationFrame across the ~0.25s width transition instead of a single scrollTo after transitionend. The board now stays locked on the placed cell as it grows, removing the visible 'centre top-left, then correct' double motion. --- ui/src/game/Board.svelte | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/ui/src/game/Board.svelte b/ui/src/game/Board.svelte index 7c842de..6256e04 100644 --- a/ui/src/game/Board.svelte +++ b/ui/src/game/Board.svelte @@ -43,33 +43,21 @@ let viewport = $state(); // Genuine layout zoom (the board grows; cqw labels stay constant), so native scroll - // works in every browser. Centre the focus cell when zoomed in. + // works in every browser. Keep the focus cell centred on every frame of the zoom-in + // (the board widens over ~0.25s) so it magnifies *into* that cell, rather than growing + // from the top-left corner and then jumping to centre once the transition ends. $effect(() => { const vp = viewport; if (!vp || !zoomed || !focus) return; const f = focus; - const scaler = vp.firstElementChild as HTMLElement | null; - const center = () => { - // Use the rendered scrollable width so the maths stays correct (gaps, padding). - const cell = vp.scrollWidth / 15; - vp.scrollTo({ - left: (f.col + 0.5) * cell - vp.clientWidth / 2, - top: (f.row + 0.5) * cell - vp.clientHeight / 2, - behavior: 'smooth', - }); - }; - // When zoom has just turned on the board is still widening; centring now would - // clamp to the still-small scroll range and land top-left. Wait for the width - // transition to finish. If already zoomed (only the focus changed), centre at once. - if (scaler && scaler.clientWidth < vp.clientWidth * Z - 1) { - const onEnd = () => { - scaler.removeEventListener('transitionend', onEnd); - center(); - }; - scaler.addEventListener('transitionend', onEnd); - return () => scaler.removeEventListener('transitionend', onEnd); - } - center(); + const start = performance.now(); + let raf = requestAnimationFrame(function tick(now) { + const cell = vp.scrollWidth / 15; // grows frame by frame as the board widens + vp.scrollLeft = (f.col + 0.5) * cell - vp.clientWidth / 2; + vp.scrollTop = (f.row + 0.5) * cell - vp.clientHeight / 2; + if (now - start < 300) raf = requestAnimationFrame(tick); + }); + return () => cancelAnimationFrame(raf); }); // Double-tap toggles zoom (pinch was dropped — it conflicts with native scroll).