fix(example/game-2d): keep ball state consistent across tabs and fullscreen - #142
Merged
eastspire merged 1 commit intoSep 4, 2026
Conversation
…screen
The Canvas 2D, WebGL, and WebGPU tabs of the game_2d demo produced visibly
different initial ball sizes (4-14 px CSS radius on the GPU tabs vs 5.5-19 px
on the 2D tab) because random_ball_radius read the live canvas width via
GAME_2D_CANVAS_SELECTOR, which targets the Canvas 2D canvas -- a hidden
element when the user is on the GL/GPU tabs. getBoundingClientRect on a
display:none element returns 0x0, so the helper fell back to the design
reference (600) on those tabs while reading the real 820-px clientWidth on
the 2D tab, producing the inconsistent radius range.
Two birds, one cleanup:
1. random_ball_radius now returns a constant 4-14 CSS px radius against
GAME_2D_CANVAS_WIDTH instead of scaling by the live canvas width. All
three tabs spawn identical ball radii, and rescale_balls_to_canvas
already preserves the radius across fullscreen enter/exit (only
positions are rescaled) so an absolute CSS-pixel radius stays stable
through the fullscreen cycle.
2. handle_rescale_dirty_canvas2d used to zero canvas_cache on every
successful rescale. The next physics tick then read (cw, ch) = (0, 0)
from the None branch of the cache lookup -- one tick of zero-size
bounds -- before the SSAA re-acquire block on the next line caught up,
wall-clamping every ball to (radius, radius). On fullscreen exit this
pulled a settled fullscreen cluster back to the floor of the inline
canvas, masking the rescale as 'balls reset to initial positions'.
Leave canvas_cache populated (the canvas DOM element survives the
fullscreen transition -- the euv engine re-keys it rather than
recreating it, so its live clientWidth/clientHeight already reflect
the new CSS box); just drop the SSAA wrapper so the next acquire
resizes the backing store.
3. The Canvas 2D / WebGL / WebGPU loops now read physics bounds from the
rescale helper's last_canvas_size buffer first, falling back to
canvas.client_width only for the very first frame. This avoids the
single-frame (0, 0) read even if the canvas DOM layout has not yet
committed the post-resize size on the current RAF tick.
Verified with Playwright (DPR=2, 1280x1400 viewport):
- 2D tab auto-spawns 4 balls with CSS-px radii in [4, 14]
- 2D tab fullscreen enter: 5/5 balls preserve their fractional position
within 1e-3; fullscreen exit: 5/5 balls land back within 1 phys-px
of their inline positions
- GL tab fullscreen enter/exit preserves ball positions visually
- GPU rendering remains unaffected by the pre-existing uniform2f bug
in headless Chrome (out of scope here)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two long-standing inconsistencies in the
game_2dexample:Ball radii depended on which tab the user was viewing. The Canvas 2D tab auto-spawned balls at 5.5-19 CSS-px radius (the live 820-px canvas width × the design 4-14 px range scaled to a 600-px reference), while the WebGL and WebGPU tabs auto-spawned balls at 4-14 CSS-px radius (their
<canvas>elements are hidden via the tab UI, sogetBoundingClientRectreturned 0×0 andrandom_ball_radiusfell back to the design reference). The user reported: "三个tab小球退出全屏和进入全屏初始状态需要保持一致" — the three tabs' initial ball state should stay consistent.Fullscreen enter/exit visibly reset ball positions. After entering landscape fullscreen, the in-line balls clustered at the top-left of the fullscreen canvas instead of being rescaled to maintain their relative position. After exiting, the balls snapped back to near their initial spawn positions instead of preserving where they had fallen.
Fix
random_ball_radiusinexample/src/page/game_2d/hook/fn.rsno longer scales by the live canvas width. It returns a constant 4-14 CSS-pixel radius against the design referenceGAME_2D_CANVAS_WIDTH. All three tabs now spawn identical ball radii.rescale_balls_to_canvasalready preserves the radius across fullscreen enter/exit (only positions are rescaled), so the absolute CSS-pixel radius stays stable through the fullscreen cycle.handle_rescale_dirty_canvas2dused to dropcanvas_cachetoNoneon every successful rescale. The next physics tick then read(cw, ch) = (0, 0)from theNonebranch of the cache lookup — one tick of zero-size bounds — before the SSAA re-acquire block on the next line caught up.resolve_wall_collisionclamped every ball to(radius, radius)for that single frame, which is exactly the "balls reset to initial spawn positions" regression.canvas_cacheis now left populated: the canvas DOM element survives the fullscreen transition unchanged (the euv engine re-keys it rather than recreating it), so its liveclientWidth/clientHeightalready reflect the new CSS box; only the SSAA wrapper is dropped so the next acquire resizes the backing store.The Canvas 2D / WebGL / WebGPU loops now prefer
last_canvas_size_for_loop(the single source of truth the rescale helper keeps fresh) for physics-bounds reads, falling back tocanvas.client_widthonly for the very first frame. This avoids any transient single-frame(0, 0)read even if the DOM layout has not yet committed the post-resize size on the current RAF tick.Verification
cargo check,cargo clippy,euv fmt, andcargo fmtall clean.cargo build --target wasm32-unknown-unknown --release -p euv-example+wasm-bindgenproduced the wasm artifact.[4, 14](all detected blobs in the inclusive range).(x/canvas_w, y/canvas_h)before and after).WebGL: INVALID_OPERATION: uniform2fheadless-Chrome quirk is unaffected — out of scope for this PR.Screenshots
/tmp/fix-2d-initial.png— 2D tab initial state, 4 balls in identical CSS range/tmp/fix-2d-pre-fs.png/fix-2d-fs.png/fix-2d-post-fs.png— fullscreen cycle on the 2D tab/tmp/fix-gl-pre-fs.png/fix-gl-fs.png/fix-gl-post-fs.png— fullscreen cycle on the GL tab/tmp/fix-gpu-canvas-only.png— GPU tab initial state (renders empty in headless due to pre-existing WebGPU uniform2f bug; ball list reports 4)