fix(ui): landscape zoom two-step (sync both axes) + fixed rack tile size
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 2m35s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 2m35s
Follow-up to the contour review of the landscape fixes: - Zoom still positioned "in two steps": a wide viewport overflows vertically as soon as the square board grows past its height, but horizontally only once it grows past the (much wider) width — so the browser pins scrollLeft to 0 while the vertical axis already pans (no time/ease tween fixes it; the horizontal scroll range physically is not there yet). Found by instrumenting the scroll trajectory. Now drive both axes by one progress = how far the board has grown past the viewport width (when a horizontal pan first becomes possible): until then the board just zooms centred, past it both axes pan together in one diagonal motion. Also disable scroll-anchoring so the browser stops fighting the programmatic scroll mid-transition. Re-verified: both axes now start and move together. - Rack tiles resized when a tile was placed: landscape used flex-grow, so removing a tile regrew the rest. Give them a fixed size (1/7 of the fixed-width rack), like the portrait rack, so placing a tile leaves the rest put.
This commit is contained in:
+23
-17
@@ -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. */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user