#62: portrait game-over/title reflow + mutable meta-button layout #67
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/62-portrait-gameover"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fast-follow of #57 (portrait solo PLAY reflow). The non-play overlays still rendered their landscape-tuned (960×640) geometry into the taller portrait canvas, so the two-column game-over clipped and the SOLO/VERSUS/REMATCH/YARD buttons ran off the right edge. Tracker: #62
What this PR does
computeMetaLayout(portrait)— mutable meta-button rects (REMATCH/YARD/SOLO/VERSUS), mirroring thecomputePlayLayout/YOU_WELLpattern from #57.render()and theinXxxButton()hit-tests read the same live rects, so a tap lands where the button is drawn in either orientation. Portrait = stacked + centered (game-over buttons bottom-anchored, title buttons in the balanced title block); landscape resets to the frozen historical values so desktop stays pixel-identical.drawGameOverLeaderboardPortrait— single-column stack: GAME OVER → score/time/placement → initials box → HIGH SCORES table → stacked buttons. The initials box sits atcx/INITIALS_BOX_Y_PORTRAIT— exactly where #59's A-Z tap-scroller lands, so that layout is settled here.drawLeaderboardRowscompact mode — portrait drops the TIME/LINES columns (and narrows the row highlight) so the table fits any portrait width (down to the 360-logical floor). Landscape keeps all columns.drawGameOverSolo/drawTitle/drawCheckin— portrait vertical balancing (the 640-tuned blocks shift down to center in the 900-tall portrait canvas) + width-adaptive score/input panels. The title's keyboard-only hint line is omitted on touch (it's meaningless on a phone and overflowed narrow widths).Decision tree (not just the conclusion)
Why mutable rects over a canvas
translate? The buttons are hit-tested (inSoloButtonetc. read the exported rects). Actx.translatewould move the drawing but not the rect the tap is tested against → render/hit-test divergence. Mutable rects keep the single source of truth. A translate is used for the text-only screens (solo game-over, check-in) where there's nothing to hit-test — there it's the lighter tool.Why stack buttons in portrait instead of shrinking the side-by-side pair? At the 360-logical floor, two 190px buttons + gap don't fit a row. Stacking is the orientation-robust answer. If a future design wants side-by-side on wide portrait (tablets), the right move is a width threshold inside
computeMetaLayout, not a rewrite.Why drop TIME/LINES in portrait rather than scale the font? Rank + initials + score are the load-bearing leaderboard columns; TIME/LINES are secondary. Dropping them reads cleaner than a sub-legible 5-column squeeze. If the operator wants them back, a 2-row-per-entry portrait variant would be the path.
Verification (closed loop)
Playwright drove the real exported render fns with synthetic state (game-over can't be reached fast via
?mock):x≥0 ∧ x+w≤W ∧ y+h≤H); screenshots confirm single-column, no clip, balanced.npx tsc --noEmitclean (thebuildscript'stsc &&gate;vite buildalone skips it).Acceptance criteria
Out of scope
drawHighScores(L-key attract overlay) portrait reflow — also two-column; deferred (attract screen, not core loop). Fast-follow if the operator hits it.🤖 Generated with Claude Code
Surveyor review — APPROVE ✅ (one should-consider, non-blocking)
Verified at head
7f6f1ad3ff16e67a9bb0da8ac621eefa2b534bb9, merge-base = current main tipdf28442(clean, single-file render.ts, no cross-PR overlap). tsc clean,vite buildclean.Skeptic's pass on the landscape-frozen rects (the no-regression guarantee) — PASSES exactly
computeMetaLayout(false)resets the four meta buttons to hard-coded absolutes; I checked each against the historicalCANVAS_W/2±Nconstants at the landscapeCANVAS_W=960:!portrait)960/2−250 = 230, y472 w230 h54230, 472/230/54960/2+20 = 500, y472 w230 h54500, 472/230/54960/2−210 = 270, y440 w190 h52270, 440/190/52960/2+20 = 500, y440 w190 h52500, 440/190/52Byte-identical to the frozen historical layout (and the original
export constobjects were themselves module-load-frozen numbers, so the reset reproduces them exactly). Desktop/landscape is pixel-identical. The core ask is solid.Mutable-export contract — correct mirror of
computePlayLayout/YOU_WELLThe button objects are mutated in place by
computeMetaLayout, and render (button(ctx, REMATCH_BTN, …)) and hit-test (inRect(REMATCH_BTN, …)) both read the same live objects — so draw and click-detection stay coherent across the reflow.drawGameOverSolo,drawTitle, anddrawGameOverLeaderboardeach callcomputeMetaLayout(isPortrait())before drawing their buttons. Per-frame, idempotent, follows orientation. Good.should-consider (non-blocking) —
drawGameOveris the one button-path that never callscomputeMetaLayoutdrawGameOver(the versus game-over, render.ts:566) draws REMATCH/YARD (620-623) but — unlike its three siblings — does not callcomputeMetaLayout. It inherits whatever the last sibling left on the module-level objects. Consequences:isPortrait()is always false there, so every sibling reset the buttons to frozen values;drawGameOverinherits them. ✅ (This is why the no-regression guarantee still holds for versus.)computeMetaLayout(true), so versus game-over inherits portrait coords. That actually fits the narrow canvas better than the landscape-frozenYARD.x=500 (+w=730 > ~667)would (it'd clip) — but it's accidental, not declared.drawGameOveris the only screen that doesn't reflow on orientation change — rotate the device during a versus game-over and the buttons stay at stale coords until you navigate away (the next screen resets them).Not a must-fix: it doesn't regress landscape, and versus-portrait is the deferred case per #63's solo-first split. But it's the lone asymmetry, and it's exactly the "works by accident / defensive-gate" shape that becomes load-bearing later — specifically, whoever reflows versus-portrait game-over in a future PR will trip on this (the buttons won't recompute). Cheap pre-emption: add
computeMetaLayout(isPortrait())at the top ofdrawGameOverto match its three siblings (it changes nothing visible today — landscape→frozen, portrait→the portrait coords it already inherits — but makes the layout explicit and orientation-responsive). Or, if you'd rather keep versus-portrait fully deferred, a one-line comment indrawGameOverdocumenting the intentional inheritance closes the skeptic's question.nit (micro) — hard-coded resets vs
CANVAS_W/2-relative declarationscomputeMetaLayout's landscape branch hard-codes230/500/270/500where the declarations areCANVAS_W/2±N. Equal atCANVAS_W=960(always true in landscape; and the consts were module-load-frozen too) — so zero behavior diff — but a latent coupling if the base canvas width ever changes. Computing fromCANVAS_W/2would be self-documenting; truly minor.INITIALS_BOX_Y_PORTRAIT seam (=284) — noted
Confirmed as the deliberate landing point for #59's A-Z scroller. Good forward-design marker; no action.
must-fix: none. should-consider: the
drawGameOverconsistency one-liner (above) — your call, non-blocking. nit: the hard-coded resets.Clean to merge. Stamping APPROVED below, pinned to the verified head. If you fold the
drawGameOverone-liner, ping for a re-pin; otherwise it's a tracked should-consider, not a merge gate.APPROVED at head
7f6f1ad3ff16e67a9bb0da8ac621eefa2b534bb9(on current maindf28442). tsc + build clean. Skeptic's pass on the landscape-frozen rects PASSES exactly — computeMetaLayout(false) restores REMATCH/YARD/SOLO/VERSUS to 230/500/270/500 = the historical CANVAS_W/2±N values (y/w/h matching), so desktop is pixel-identical. Mutable-export contract correct: render + hit-test both read the live button objects; computeMetaLayout mirrors computePlayLayout/YOU_WELL. One non-blocking should-consider (drawGameOver, the versus game-over, is the lone button-path that doesn't call computeMetaLayout → inherits coords; benign today, no landscape regression, but won't reflow on orientation change + will trip a future versus-portrait reflow — one-line fix or a documenting comment). nit: hard-coded resets vs CANVAS_W/2-relative. Substance in the issue-comment above. Clean to merge.Re-pinned: APPROVED on new head
dc5b2ee4bc09341815a179e359e1e105448df67e(was7f6f1ad). The fold delta is comment-only — verifiedgit diff 7f6f1ad..dc5b2eeis exactly the 6-linedrawGameOverdocumentation block, zero code change; landscape frozen rects (230/500/270/500) untouched, so the arithmetic no-regression proof is preserved; tsc clean ondc5b2ee. The should-consider is closed the right way: the comment option (not the one-liner) was the better call — addingcomputeMetaLayout(isPortrait())alone would half-reflow the versus screen (buttons reflow, the landscape-tuned scorecard clips), so the fix belongs with the portrait branch, exactly asdrawGameOverLeaderboarddid it. Tracked in #68 (AC3) as the durable home, with the in-code comment pointing there. All substantive verification from review 2727 stands (single-file render.ts, on current maindf28442). Clean to merge atdc5b2ee.