Skip to content

fix(example/game-2d): keep ball state consistent across tabs and fullscreen - #142

Merged
eastspire merged 1 commit into
euv-dev:masterfrom
eastspire:fix/game-2d-ball-consistency-fullscreen
Sep 4, 2026
Merged

fix(example/game-2d): keep ball state consistent across tabs and fullscreen#142
eastspire merged 1 commit into
euv-dev:masterfrom
eastspire:fix/game-2d-ball-consistency-fullscreen

Conversation

@eastspire

Copy link
Copy Markdown
Collaborator

Summary

Two long-standing inconsistencies in the game_2d example:

  1. 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, so getBoundingClientRect returned 0×0 and random_ball_radius fell back to the design reference). The user reported: "三个tab小球退出全屏和进入全屏初始状态需要保持一致" — the three tabs' initial ball state should stay consistent.

  2. 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_radius in example/src/page/game_2d/hook/fn.rs no longer scales by the live canvas width. It returns a constant 4-14 CSS-pixel radius against the design reference GAME_2D_CANVAS_WIDTH. All three tabs now spawn identical ball radii. rescale_balls_to_canvas already 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_canvas2d used to drop canvas_cache to None 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. resolve_wall_collision clamped every ball to (radius, radius) for that single frame, which is exactly the "balls reset to initial spawn positions" regression. canvas_cache is now left populated: the canvas DOM element survives the fullscreen transition unchanged (the euv engine re-keys it rather than recreating it), so its live clientWidth / clientHeight already 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 to canvas.client_width only 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, and cargo fmt all clean.
  • cargo build --target wasm32-unknown-unknown --release -p euv-example + wasm-bindgen produced the wasm artifact.
  • Playwright (DPR=2, 1280×1400 viewport) confirmed:
    • 2D tab auto-spawns 4 balls with CSS-px radii in [4, 14] (all detected blobs in the inclusive range).
    • 2D tab fullscreen enter: 5/5 balls preserve their fractional position within 1e-3 (same (x/canvas_w, y/canvas_h) before and after).
    • 2D tab fullscreen exit: 5/5 balls land back within 1 physical-px of their pre-fullscreen viewport positions.
    • GL tab fullscreen cycle preserves ball positions visually (see screenshots).
    • The pre-existing WebGL: INVALID_OPERATION: uniform2f headless-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)

…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)
@eastspire
eastspire merged commit f12d7ed into euv-dev:master Sep 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant