Stage 7 UI polish: zoom-in magnifies into the focus cell (no top-left jump)
Tests · UI / test (push) Successful in 12s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 11s

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.
This commit is contained in:
Ilia Denisov
2026-06-03 16:35:39 +02:00
parent 1e7da5925a
commit 8ec71a6816
+11 -23
View File
@@ -43,33 +43,21 @@
let viewport = $state<HTMLElement>();
// 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).