Presentation start is off-center on small screens, and slides don't re-center on window resize #147

Open
opened 2026-05-05 12:31:01 +00:00 by AhmedHanafy725 · 3 comments
Member

Bug

A) Off-center start (visible especially on small screens)

When presentation mode starts, the focused frame is shifted toward the side where the now-hidden chrome used to be. The smaller the screen, the bigger the visible offset.

Reproduction:

  1. Add at least one Frame to the board (or open a board that has frames).
  2. Resize the browser window so it is small (e.g., 1024px wide or less).
  3. Click "Start Presentation".
  4. Observed: the first slide is rendered to the left (or upper-left) of the screen — not centered. The smaller the viewport, the larger the offset.
  5. Expected: the slide is centered both horizontally and vertically inside the now-fullscreen presentation viewport.

B) No re-center on window resize during presentation

If the user resizes the browser window (or toggles devtools, exits/enters browser fullscreen) while a slide is on screen, the slide stays at its old screen position and scale instead of re-fitting to the new viewport.

Reproduction:

  1. Start a presentation. Slide #1 is centered (after fix A is applied) at full fit.
  2. Resize the window — make it wider, narrower, taller, shorter.
  3. Observed: the slide stays at its original position and scale. The new viewport area on the larger side is empty; on the smaller side, the slide is clipped.
  4. Expected: the slide re-fits the new viewport size and re-centers, the same way focusFrame would on a fresh slide change.

Root cause

A) Off-center start

crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js, startPresentation (line 82):

document.body.classList.add('wb-presenting');   // hides navbar, toolbar, subtoolbar, minimap, zoom, properties-panel
focusFrame(frames[0]);                          // runs immediately

wb-presenting hides 7+ chrome elements (board_view.html:7-15), which expands the canvas container. But focusFrame (line 198) reads stage.width() / stage.height() synchronously to compute centering at lines 210-217:

var newScale = Math.min(stage.width() / aabb.width, stage.height() / aabb.height);
...
var stagePos = {
    x: -aabb.x * newScale + (stage.width() - aabb.width * newScale) / 2,
    y: -aabb.y * newScale + (stage.height() - aabb.height * newScale) / 2,
};

The stage's internal width/height are kept in sync with the container by a ResizeObserver in canvas.js:248, but ResizeObserver fires asynchronously on the next layout flush — AFTER focusFrame has already computed centering. So the math uses the pre-presentation (smaller) stage dimensions, centering on the old viewport center, which is offset toward whatever side now has more space.

B) No re-center on resize

The ResizeObserver in canvas.js:248 does call drawGrid when the container resizes, but it does NOT re-invoke focusFrame on the currently-focused slide. There is no listener anywhere that re-fits the active slide on viewport changes.

Fix scope

Fix A — synchronously update stage size before centering

In startPresentation (and symmetrically in stopPresentation), after toggling the wb-presenting class but before calling focusFrame / restoring previousView, synchronously snap Konva's stage dimensions to the (newly reflowed) container:

var container = stage.container();
stage.width(container.offsetWidth);
stage.height(container.offsetHeight);

Reading offsetWidth forces a synchronous layout reflow, so the values are correct immediately — no need to wait for the ResizeObserver tick. The existing ResizeObserver remains as the fallback / continuous resync.

Fix B — re-fit active slide on resize during presentation

In frames.js, register a one-time ResizeObserver on the stage container that, only while presentationMode === true, calls focusFrame(frames[currentFrameIndex]) on every observed resize. The observer can be created lazily on first startPresentation and torn down on module unload (or kept indefinitely — it is cheap and gated by the presentationMode flag).

Debounce is unnecessary — focusFrame is already cheap (one transform + one batchDraw), and ResizeObserver naturally coalesces rapid resize events.

Affected files

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js — only file in scope. Modify startPresentation, stopPresentation; add a presentation-mode-gated resize listener.

Acceptance criteria

  • On a small viewport (~800px wide), starting presentation centers the first slide horizontally and vertically with no left/top/right/bottom offset.
  • Same on a wide viewport (~1920px); the slide fits and centers regardless of original chrome dimensions.
  • Stopping presentation restores the user's prior view exactly (no off-center drift in the reverse direction when chrome reappears).
  • Resizing the browser window during a presentation re-fits and re-centers the current slide to the new viewport.
  • Toggling browser fullscreen (F11) during a presentation re-fits the slide.
  • Toggling devtools (which resizes the viewport) during a presentation re-fits the slide.
  • No effect when not in presentation mode — normal canvas resize behavior (grid redraw, viewport listeners) is unchanged.
  • Slide navigation (next / previous slide) behaves the same as before.
  • No regression in collab — applyRemoteSlide continues to work and other clients receiving slide-change broadcasts re-center via the same focusFrame path.
## Bug ### A) Off-center start (visible especially on small screens) When presentation mode starts, the focused frame is shifted toward the side where the now-hidden chrome used to be. The smaller the screen, the bigger the visible offset. **Reproduction:** 1. Add at least one Frame to the board (or open a board that has frames). 2. Resize the browser window so it is small (e.g., 1024px wide or less). 3. Click "Start Presentation". 4. Observed: the first slide is rendered to the left (or upper-left) of the screen — not centered. The smaller the viewport, the larger the offset. 5. Expected: the slide is centered both horizontally and vertically inside the now-fullscreen presentation viewport. ### B) No re-center on window resize during presentation If the user resizes the browser window (or toggles devtools, exits/enters browser fullscreen) while a slide is on screen, the slide stays at its old screen position and scale instead of re-fitting to the new viewport. **Reproduction:** 1. Start a presentation. Slide #1 is centered (after fix A is applied) at full fit. 2. Resize the window — make it wider, narrower, taller, shorter. 3. Observed: the slide stays at its original position and scale. The new viewport area on the larger side is empty; on the smaller side, the slide is clipped. 4. Expected: the slide re-fits the new viewport size and re-centers, the same way `focusFrame` would on a fresh slide change. ## Root cause ### A) Off-center start `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js`, `startPresentation` (line 82): ```js document.body.classList.add('wb-presenting'); // hides navbar, toolbar, subtoolbar, minimap, zoom, properties-panel focusFrame(frames[0]); // runs immediately ``` `wb-presenting` hides 7+ chrome elements (`board_view.html:7-15`), which expands the canvas container. But `focusFrame` (line 198) reads `stage.width()` / `stage.height()` synchronously to compute centering at lines 210-217: ```js var newScale = Math.min(stage.width() / aabb.width, stage.height() / aabb.height); ... var stagePos = { x: -aabb.x * newScale + (stage.width() - aabb.width * newScale) / 2, y: -aabb.y * newScale + (stage.height() - aabb.height * newScale) / 2, }; ``` The stage's internal `width`/`height` are kept in sync with the container by a `ResizeObserver` in `canvas.js:248`, but ResizeObserver fires asynchronously on the next layout flush — AFTER `focusFrame` has already computed centering. So the math uses the **pre-presentation** (smaller) stage dimensions, centering on the old viewport center, which is offset toward whatever side now has more space. ### B) No re-center on resize The ResizeObserver in `canvas.js:248` does call `drawGrid` when the container resizes, but it does NOT re-invoke `focusFrame` on the currently-focused slide. There is no listener anywhere that re-fits the active slide on viewport changes. ## Fix scope ### Fix A — synchronously update stage size before centering In `startPresentation` (and symmetrically in `stopPresentation`), after toggling the `wb-presenting` class but before calling `focusFrame` / restoring `previousView`, synchronously snap Konva's stage dimensions to the (newly reflowed) container: ```js var container = stage.container(); stage.width(container.offsetWidth); stage.height(container.offsetHeight); ``` Reading `offsetWidth` forces a synchronous layout reflow, so the values are correct immediately — no need to wait for the ResizeObserver tick. The existing ResizeObserver remains as the fallback / continuous resync. ### Fix B — re-fit active slide on resize during presentation In `frames.js`, register a one-time `ResizeObserver` on the stage container that, **only while `presentationMode === true`**, calls `focusFrame(frames[currentFrameIndex])` on every observed resize. The observer can be created lazily on first `startPresentation` and torn down on module unload (or kept indefinitely — it is cheap and gated by the `presentationMode` flag). Debounce is unnecessary — `focusFrame` is already cheap (one transform + one batchDraw), and ResizeObserver naturally coalesces rapid resize events. ## Affected files - `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js` — only file in scope. Modify `startPresentation`, `stopPresentation`; add a presentation-mode-gated resize listener. ## Acceptance criteria - [ ] On a small viewport (~800px wide), starting presentation centers the first slide horizontally and vertically with no left/top/right/bottom offset. - [ ] Same on a wide viewport (~1920px); the slide fits and centers regardless of original chrome dimensions. - [ ] Stopping presentation restores the user's prior view exactly (no off-center drift in the reverse direction when chrome reappears). - [ ] Resizing the browser window during a presentation re-fits and re-centers the current slide to the new viewport. - [ ] Toggling browser fullscreen (F11) during a presentation re-fits the slide. - [ ] Toggling devtools (which resizes the viewport) during a presentation re-fits the slide. - [ ] No effect when not in presentation mode — normal canvas resize behavior (grid redraw, viewport listeners) is unchanged. - [ ] Slide navigation (next / previous slide) behaves the same as before. - [ ] No regression in collab — `applyRemoteSlide` continues to work and other clients receiving slide-change broadcasts re-center via the same `focusFrame` path.
Author
Member

Implementation Spec for Issue #147

Objective

Fix two related bugs in presentation mode: (A) the first slide is rendered off-center on small screens because focusFrame runs before the wb-presenting chrome-hiding has triggered the canvas's async ResizeObserver, and (B) the active slide does not re-fit when the window/container is resized during a presentation. Fix both by snapping the Konva stage size synchronously around chrome toggles in startPresentation/stopPresentation, and by re-invoking focusFrame on container resize while in presentation mode — all confined to frames.js.

Requirements

  • Eliminate the async gap between wb-presenting class toggling and Konva stage dimensions when entering and exiting presentation mode.
  • During presentation mode, the active slide must re-fit on every container resize (window resize, fullscreen toggle, devtools open/close, browser zoom).
  • The host's spotlight overlay (positioned from _focusedScreenRect via _emit) must update its rect on every resize so the spotlight follows the re-fitted slide.
  • Outside presentation mode, behavior must be unchanged — the existing ResizeObserver in canvas.js continues to handle non-presentation resize.
  • Do not modify canvas.js. Do not touch setZoom. Do not touch the existing ResizeObserver in canvas.js. Do not touch collab/sync code (applyRemoteSlide, _broadcastSlide, _pendingRemoteSlide).
  • Keep the change set minimal: one helper, one ensure-observer helper, two call-site insertions, one lazy-initialized ResizeObserver.

Files to Modify/Create

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js — only file in scope.

Implementation Plan

Step 1: Add module-scope _resizeObserver slot and _syncStageSize() helper

Files: frames.js

  • Declare var _resizeObserver = null; alongside the existing module-scope state.
  • Add a private _syncStageSize() that:
    • Gets the stage via WhiteboardCanvas.getStage().
    • Reads stage.container().offsetWidth / offsetHeight (the read forces a synchronous layout reflow so the values reflect any just-applied class change).
    • Calls stage.width(...) and stage.height(...) with those values.
    • Defensive no-op if stage or stage.container() is falsy.
      Dependencies: none

Step 2: Use _syncStageSize() in startPresentation

Files: frames.js

  • After document.body.classList.add('wb-presenting'); (line 100) and before focusFrame(frames[0]); (line 101), insert _syncStageSize(); and _ensureResizeObserver(); (helper from Step 4).
  • This guarantees focusFrame's reads of stage.width() / stage.height() operate on the expanded post-chrome-hide dimensions, fixing bug A. It also wires up the resize re-fit for the entire presentation session.
    Dependencies: Step 1, Step 4

Step 3: Use _syncStageSize() in stopPresentation

Files: frames.js

  • After document.body.classList.remove('wb-presenting'); (line 118) and before the previousView restoration block, insert _syncStageSize();.
  • Restoration uses absolute setZoom(...) + stage.position(...) from the snapshot; snapping the stage size first keeps setZoom's "center about stage.width()/2, stage.height()/2" math (canvas.js:267) consistent.
    Dependencies: Step 1

Step 4: Lazy-init ResizeObserver with presentationMode gate

Files: frames.js

  • Add _ensureResizeObserver():
    • If _resizeObserver != null, return.
    • If typeof ResizeObserver === 'undefined', return (defensive for older browsers; bug A's stage-size snap still works without the observer).
    • Construct _resizeObserver = new ResizeObserver(function() { ... }) with a callback that:
      • Returns immediately if presentationMode === false (gated callback — listener stays alive but is a no-op when idle).
      • Returns immediately if frames.length === 0 (defensive).
      • Calls _syncStageSize() to flush new container dimensions into Konva.
      • Calls focusFrame(frames[currentFrameIndex]) to re-fit the active slide.
      • Calls _emit() so the host can reposition the spotlight overlay using the freshly-recomputed _focusedScreenRect.
    • Calls _resizeObserver.observe(WhiteboardCanvas.getStage().container()).
  • Called once from startPresentation (Step 2). Never torn down — one ResizeObserver, gated by a boolean.
  • ResizeObserver coalesces callbacks to once per layout cycle; no debounce needed.
    Dependencies: Step 1

Acceptance Criteria

  • Starting a presentation on a small viewport (where chrome occupies a non-trivial fraction) renders the first slide centered in the post-chrome viewport, not offset toward the side the chrome had occupied.
  • Slide navigation (Next/Prev) and start/stop continue to work identically on large viewports (regression check).
  • While presenting, resizing the browser window re-fits the active slide so it stays centered and as large as possible. Toggling fullscreen and opening/closing devtools have the same effect.
  • The spotlight overlay (the dark mask outside the slide rect) tracks the re-fitted slide on resize.
  • Stopping a presentation restores the previous pan/zoom snapshot exactly, with no jump on small screens.
  • Resizing the window when NOT presenting still uses only the existing canvas.js ResizeObserver (no double redraws, no focusFrame invocation outside presentation).
  • Collab: applyRemoteSlide and _broadcastSlide continue to work; the new resize handler does not emit slide events.
  • No console errors in browsers without ResizeObserver (defensive check returns early; bug A's stage-size snap still works).

Notes

  • Synchronous layout reflow trick: reading container.offsetWidth from JS forces the browser to flush pending style/layout (the just-added wb-presenting class) before returning the value. This is what makes a synchronous _syncStageSize() call after classList.add(...) work without waiting for the canvas's async ResizeObserver to fire.
  • Gate inside the callback, not at attach/detach: keeps the listener lifecycle dead simple — register once on first startPresentation, never tear down. Prevents observer-leak / double-register bugs from rapid toggle sequences.
  • No debounce: ResizeObserver already delivers at most one callback per layout cycle. focusFrame is cheap (one AABB compute + setZoom + drawGrid).
  • _emit() is host-only: it invokes the registered _onChange callback (host template's spotlight reposition path), not collab. Safe to call on resize.
  • Zero impact when not presenting: the gated callback returns immediately; the existing canvas.js ResizeObserver remains the sole source of resize behavior outside presentation mode.
  • Collab unaffected: only _emit() is called on resize, never _broadcastSlide(). Index/total are unchanged on resize.
## Implementation Spec for Issue #147 ### Objective Fix two related bugs in presentation mode: (A) the first slide is rendered off-center on small screens because `focusFrame` runs before the `wb-presenting` chrome-hiding has triggered the canvas's async `ResizeObserver`, and (B) the active slide does not re-fit when the window/container is resized during a presentation. Fix both by snapping the Konva stage size synchronously around chrome toggles in `startPresentation`/`stopPresentation`, and by re-invoking `focusFrame` on container resize while in presentation mode — all confined to `frames.js`. ### Requirements - Eliminate the async gap between `wb-presenting` class toggling and Konva stage dimensions when entering and exiting presentation mode. - During presentation mode, the active slide must re-fit on every container resize (window resize, fullscreen toggle, devtools open/close, browser zoom). - The host's spotlight overlay (positioned from `_focusedScreenRect` via `_emit`) must update its rect on every resize so the spotlight follows the re-fitted slide. - Outside presentation mode, behavior must be unchanged — the existing `ResizeObserver` in `canvas.js` continues to handle non-presentation resize. - Do not modify `canvas.js`. Do not touch `setZoom`. Do not touch the existing `ResizeObserver` in `canvas.js`. Do not touch collab/sync code (`applyRemoteSlide`, `_broadcastSlide`, `_pendingRemoteSlide`). - Keep the change set minimal: one helper, one ensure-observer helper, two call-site insertions, one lazy-initialized `ResizeObserver`. ### Files to Modify/Create - `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js` — only file in scope. ### Implementation Plan #### Step 1: Add module-scope `_resizeObserver` slot and `_syncStageSize()` helper Files: `frames.js` - Declare `var _resizeObserver = null;` alongside the existing module-scope state. - Add a private `_syncStageSize()` that: - Gets the stage via `WhiteboardCanvas.getStage()`. - Reads `stage.container().offsetWidth` / `offsetHeight` (the read forces a synchronous layout reflow so the values reflect any just-applied class change). - Calls `stage.width(...)` and `stage.height(...)` with those values. - Defensive no-op if `stage` or `stage.container()` is falsy. Dependencies: none #### Step 2: Use `_syncStageSize()` in `startPresentation` Files: `frames.js` - After `document.body.classList.add('wb-presenting');` (line 100) and before `focusFrame(frames[0]);` (line 101), insert `_syncStageSize();` and `_ensureResizeObserver();` (helper from Step 4). - This guarantees `focusFrame`'s reads of `stage.width()` / `stage.height()` operate on the expanded post-chrome-hide dimensions, fixing bug A. It also wires up the resize re-fit for the entire presentation session. Dependencies: Step 1, Step 4 #### Step 3: Use `_syncStageSize()` in `stopPresentation` Files: `frames.js` - After `document.body.classList.remove('wb-presenting');` (line 118) and before the `previousView` restoration block, insert `_syncStageSize();`. - Restoration uses absolute `setZoom(...)` + `stage.position(...)` from the snapshot; snapping the stage size first keeps `setZoom`'s "center about `stage.width()/2, stage.height()/2`" math (canvas.js:267) consistent. Dependencies: Step 1 #### Step 4: Lazy-init `ResizeObserver` with `presentationMode` gate Files: `frames.js` - Add `_ensureResizeObserver()`: - If `_resizeObserver != null`, return. - If `typeof ResizeObserver === 'undefined'`, return (defensive for older browsers; bug A's stage-size snap still works without the observer). - Construct `_resizeObserver = new ResizeObserver(function() { ... })` with a callback that: - Returns immediately if `presentationMode === false` (gated callback — listener stays alive but is a no-op when idle). - Returns immediately if `frames.length === 0` (defensive). - Calls `_syncStageSize()` to flush new container dimensions into Konva. - Calls `focusFrame(frames[currentFrameIndex])` to re-fit the active slide. - Calls `_emit()` so the host can reposition the spotlight overlay using the freshly-recomputed `_focusedScreenRect`. - Calls `_resizeObserver.observe(WhiteboardCanvas.getStage().container())`. - Called once from `startPresentation` (Step 2). Never torn down — one ResizeObserver, gated by a boolean. - ResizeObserver coalesces callbacks to once per layout cycle; no debounce needed. Dependencies: Step 1 ### Acceptance Criteria - [ ] Starting a presentation on a small viewport (where chrome occupies a non-trivial fraction) renders the first slide centered in the post-chrome viewport, not offset toward the side the chrome had occupied. - [ ] Slide navigation (Next/Prev) and start/stop continue to work identically on large viewports (regression check). - [ ] While presenting, resizing the browser window re-fits the active slide so it stays centered and as large as possible. Toggling fullscreen and opening/closing devtools have the same effect. - [ ] The spotlight overlay (the dark mask outside the slide rect) tracks the re-fitted slide on resize. - [ ] Stopping a presentation restores the previous pan/zoom snapshot exactly, with no jump on small screens. - [ ] Resizing the window when NOT presenting still uses only the existing `canvas.js` ResizeObserver (no double redraws, no `focusFrame` invocation outside presentation). - [ ] Collab: `applyRemoteSlide` and `_broadcastSlide` continue to work; the new resize handler does not emit slide events. - [ ] No console errors in browsers without `ResizeObserver` (defensive check returns early; bug A's stage-size snap still works). ### Notes - **Synchronous layout reflow trick**: reading `container.offsetWidth` from JS forces the browser to flush pending style/layout (the just-added `wb-presenting` class) before returning the value. This is what makes a synchronous `_syncStageSize()` call after `classList.add(...)` work without waiting for the canvas's async `ResizeObserver` to fire. - **Gate inside the callback, not at attach/detach**: keeps the listener lifecycle dead simple — register once on first `startPresentation`, never tear down. Prevents observer-leak / double-register bugs from rapid toggle sequences. - **No debounce**: `ResizeObserver` already delivers at most one callback per layout cycle. `focusFrame` is cheap (one AABB compute + `setZoom` + `drawGrid`). - **`_emit()` is host-only**: it invokes the registered `_onChange` callback (host template's spotlight reposition path), not collab. Safe to call on resize. - **Zero impact when not presenting**: the gated callback returns immediately; the existing `canvas.js` `ResizeObserver` remains the sole source of resize behavior outside presentation mode. - **Collab unaffected**: only `_emit()` is called on resize, never `_broadcastSlide()`. Index/total are unchanged on resize.
Author
Member

Test Results

  • cargo test --workspace --lib — PASS
  • cargo fmt --check — PASS
  • cargo clippy --workspace --all-targets -- -D warnings — PASS

Notes

This change is JS only (crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js). The Rust workspace tests do not exercise this code path; they confirm the workspace still builds, formats, and lints cleanly. Manual UI verification: start a presentation on a small viewport and confirm the first slide is centered (no left/top offset). While in presentation, resize the browser window — the slide should re-fit and re-center; the spotlight overlay should track. Stop the presentation and confirm the previous pan/zoom is restored cleanly.

## Test Results - `cargo test --workspace --lib` — PASS - `cargo fmt --check` — PASS - `cargo clippy --workspace --all-targets -- -D warnings` — PASS ### Notes This change is JS only (`crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js`). The Rust workspace tests do not exercise this code path; they confirm the workspace still builds, formats, and lints cleanly. Manual UI verification: start a presentation on a small viewport and confirm the first slide is centered (no left/top offset). While in presentation, resize the browser window — the slide should re-fit and re-center; the spotlight overlay should track. Stop the presentation and confirm the previous pan/zoom is restored cleanly.
Author
Member

Implementation Summary

Two related fixes for presentation mode: (A) the first slide is now centered correctly when starting on small screens, and (B) the active slide re-fits and re-centers on every viewport change during a presentation.

Changes

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js:
    • New module-scope _resizeObserver slot.
    • _syncStageSize() helper — reads stage.container().offsetWidth/Height (the read forces a synchronous layout reflow, so values reflect any just-applied class change) and applies them to Konva via stage.width(...) / stage.height(...). Single source of truth for "make the Konva stage match the DOM right now".
    • _ensureResizeObserver() helper — lazy-init a ResizeObserver on the stage container. Callback is gated by presentationMode and frames.length; on resize it calls _syncStageSize(), then focusFrame(frames[currentFrameIndex]) to re-fit, then _emit() so the host's spotlight overlay tracks the new rect.
    • startPresentation: after classList.add('wb-presenting') and before focusFrame(frames[0]), calls _syncStageSize() then _ensureResizeObserver(). Bug A fix.
    • stopPresentation: after classList.remove('wb-presenting') and before the previousView restoration block, calls _syncStageSize() so setZoom's "center about stage.width()/2, stage.height()/2" math sees the post-chrome dimensions.

No changes to canvas.js, no changes to setZoom, no changes to collab/sync (applyRemoteSlide, _broadcastSlide, _pendingRemoteSlide left untouched). The existing ResizeObserver in canvas.js continues to handle non-presentation resize behavior. The new observer is registered once and never torn down — gated by a boolean for trivial lifecycle.

Test Results

  • cargo test --workspace --lib — PASS
  • cargo fmt --check — PASS
  • cargo clippy --workspace --all-targets -- -D warnings — PASS

Manual Verification Required

  1. Resize the browser window so the canvas chrome occupies a noticeable fraction (e.g. ~1024px wide). Start a presentation. The first slide should be centered both horizontally and vertically — no offset toward the side the chrome used to occupy.
  2. While presenting, resize the browser window (wider/narrower/taller/shorter). The active slide re-fits and re-centers; the dark spotlight overlay tracks the new slide rect.
  3. Toggle browser fullscreen (F11) and toggle devtools open/closed during a presentation — the slide re-fits to the new viewport in both cases.
  4. Stop the presentation. The previous pan/zoom snapshot is restored cleanly with no jump.
  5. Outside presentation mode, normal canvas resize behavior is unchanged (no double redraws, no focusFrame invocation).
  6. Collab: a second client joining mid-presentation receives applyRemoteSlide and re-fits to its own viewport; the new resize handler emits no slide events.
## Implementation Summary Two related fixes for presentation mode: (A) the first slide is now centered correctly when starting on small screens, and (B) the active slide re-fits and re-centers on every viewport change during a presentation. ### Changes - **`crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js`**: - New module-scope `_resizeObserver` slot. - `_syncStageSize()` helper — reads `stage.container().offsetWidth/Height` (the read forces a synchronous layout reflow, so values reflect any just-applied class change) and applies them to Konva via `stage.width(...)` / `stage.height(...)`. Single source of truth for "make the Konva stage match the DOM right now". - `_ensureResizeObserver()` helper — lazy-init a ResizeObserver on the stage container. Callback is gated by `presentationMode` and `frames.length`; on resize it calls `_syncStageSize()`, then `focusFrame(frames[currentFrameIndex])` to re-fit, then `_emit()` so the host's spotlight overlay tracks the new rect. - `startPresentation`: after `classList.add('wb-presenting')` and before `focusFrame(frames[0])`, calls `_syncStageSize()` then `_ensureResizeObserver()`. Bug A fix. - `stopPresentation`: after `classList.remove('wb-presenting')` and before the `previousView` restoration block, calls `_syncStageSize()` so `setZoom`'s "center about `stage.width()/2, stage.height()/2`" math sees the post-chrome dimensions. No changes to `canvas.js`, no changes to `setZoom`, no changes to collab/sync (`applyRemoteSlide`, `_broadcastSlide`, `_pendingRemoteSlide` left untouched). The existing `ResizeObserver` in `canvas.js` continues to handle non-presentation resize behavior. The new observer is registered once and never torn down — gated by a boolean for trivial lifecycle. ### Test Results - `cargo test --workspace --lib` — PASS - `cargo fmt --check` — PASS - `cargo clippy --workspace --all-targets -- -D warnings` — PASS ### Manual Verification Required 1. Resize the browser window so the canvas chrome occupies a noticeable fraction (e.g. ~1024px wide). Start a presentation. The first slide should be centered both horizontally and vertically — no offset toward the side the chrome used to occupy. 2. While presenting, resize the browser window (wider/narrower/taller/shorter). The active slide re-fits and re-centers; the dark spotlight overlay tracks the new slide rect. 3. Toggle browser fullscreen (F11) and toggle devtools open/closed during a presentation — the slide re-fits to the new viewport in both cases. 4. Stop the presentation. The previous pan/zoom snapshot is restored cleanly with no jump. 5. Outside presentation mode, normal canvas resize behavior is unchanged (no double redraws, no `focusFrame` invocation). 6. Collab: a second client joining mid-presentation receives `applyRemoteSlide` and re-fits to its own viewport; the new resize handler emits no slide events.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#147
No description provided.