Presentation start is off-center on small screens, and slides don't re-center on window resize #147
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#147
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
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:
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:
focusFramewould on a fresh slide change.Root cause
A) Off-center start
crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js,startPresentation(line 82):wb-presentinghides 7+ chrome elements (board_view.html:7-15), which expands the canvas container. ButfocusFrame(line 198) readsstage.width()/stage.height()synchronously to compute centering at lines 210-217:The stage's internal
width/heightare kept in sync with the container by aResizeObserverincanvas.js:248, but ResizeObserver fires asynchronously on the next layout flush — AFTERfocusFramehas 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:248does calldrawGridwhen the container resizes, but it does NOT re-invokefocusFrameon 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 instopPresentation), after toggling thewb-presentingclass but before callingfocusFrame/ restoringpreviousView, synchronously snap Konva's stage dimensions to the (newly reflowed) container:Reading
offsetWidthforces 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-timeResizeObserveron the stage container that, only whilepresentationMode === true, callsfocusFrame(frames[currentFrameIndex])on every observed resize. The observer can be created lazily on firststartPresentationand torn down on module unload (or kept indefinitely — it is cheap and gated by thepresentationModeflag).Debounce is unnecessary —
focusFrameis 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. ModifystartPresentation,stopPresentation; add a presentation-mode-gated resize listener.Acceptance criteria
applyRemoteSlidecontinues to work and other clients receiving slide-change broadcasts re-center via the samefocusFramepath.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
focusFrameruns before thewb-presentingchrome-hiding has triggered the canvas's asyncResizeObserver, 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 instartPresentation/stopPresentation, and by re-invokingfocusFrameon container resize while in presentation mode — all confined toframes.js.Requirements
wb-presentingclass toggling and Konva stage dimensions when entering and exiting presentation mode._focusedScreenRectvia_emit) must update its rect on every resize so the spotlight follows the re-fitted slide.ResizeObserverincanvas.jscontinues to handle non-presentation resize.canvas.js. Do not touchsetZoom. Do not touch the existingResizeObserverincanvas.js. Do not touch collab/sync code (applyRemoteSlide,_broadcastSlide,_pendingRemoteSlide).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
_resizeObserverslot and_syncStageSize()helperFiles:
frames.jsvar _resizeObserver = null;alongside the existing module-scope state._syncStageSize()that:WhiteboardCanvas.getStage().stage.container().offsetWidth/offsetHeight(the read forces a synchronous layout reflow so the values reflect any just-applied class change).stage.width(...)andstage.height(...)with those values.stageorstage.container()is falsy.Dependencies: none
Step 2: Use
_syncStageSize()instartPresentationFiles:
frames.jsdocument.body.classList.add('wb-presenting');(line 100) and beforefocusFrame(frames[0]);(line 101), insert_syncStageSize();and_ensureResizeObserver();(helper from Step 4).focusFrame's reads ofstage.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()instopPresentationFiles:
frames.jsdocument.body.classList.remove('wb-presenting');(line 118) and before thepreviousViewrestoration block, insert_syncStageSize();.setZoom(...)+stage.position(...)from the snapshot; snapping the stage size first keepssetZoom's "center aboutstage.width()/2, stage.height()/2" math (canvas.js:267) consistent.Dependencies: Step 1
Step 4: Lazy-init
ResizeObserverwithpresentationModegateFiles:
frames.js_ensureResizeObserver():_resizeObserver != null, return.typeof ResizeObserver === 'undefined', return (defensive for older browsers; bug A's stage-size snap still works without the observer)._resizeObserver = new ResizeObserver(function() { ... })with a callback that:presentationMode === false(gated callback — listener stays alive but is a no-op when idle).frames.length === 0(defensive)._syncStageSize()to flush new container dimensions into Konva.focusFrame(frames[currentFrameIndex])to re-fit the active slide._emit()so the host can reposition the spotlight overlay using the freshly-recomputed_focusedScreenRect._resizeObserver.observe(WhiteboardCanvas.getStage().container()).startPresentation(Step 2). Never torn down — one ResizeObserver, gated by a boolean.Dependencies: Step 1
Acceptance Criteria
canvas.jsResizeObserver (no double redraws, nofocusFrameinvocation outside presentation).applyRemoteSlideand_broadcastSlidecontinue to work; the new resize handler does not emit slide events.ResizeObserver(defensive check returns early; bug A's stage-size snap still works).Notes
container.offsetWidthfrom JS forces the browser to flush pending style/layout (the just-addedwb-presentingclass) before returning the value. This is what makes a synchronous_syncStageSize()call afterclassList.add(...)work without waiting for the canvas's asyncResizeObserverto fire.startPresentation, never tear down. Prevents observer-leak / double-register bugs from rapid toggle sequences.ResizeObserveralready delivers at most one callback per layout cycle.focusFrameis cheap (one AABB compute +setZoom+drawGrid)._emit()is host-only: it invokes the registered_onChangecallback (host template's spotlight reposition path), not collab. Safe to call on resize.canvas.jsResizeObserverremains the sole source of resize behavior outside presentation mode._emit()is called on resize, never_broadcastSlide(). Index/total are unchanged on resize.Test Results
cargo test --workspace --lib— PASScargo fmt --check— PASScargo clippy --workspace --all-targets -- -D warnings— PASSNotes
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.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:_resizeObserverslot._syncStageSize()helper — readsstage.container().offsetWidth/Height(the read forces a synchronous layout reflow, so values reflect any just-applied class change) and applies them to Konva viastage.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 bypresentationModeandframes.length; on resize it calls_syncStageSize(), thenfocusFrame(frames[currentFrameIndex])to re-fit, then_emit()so the host's spotlight overlay tracks the new rect.startPresentation: afterclassList.add('wb-presenting')and beforefocusFrame(frames[0]), calls_syncStageSize()then_ensureResizeObserver(). Bug A fix.stopPresentation: afterclassList.remove('wb-presenting')and before thepreviousViewrestoration block, calls_syncStageSize()sosetZoom's "center aboutstage.width()/2, stage.height()/2" math sees the post-chrome dimensions.No changes to
canvas.js, no changes tosetZoom, no changes to collab/sync (applyRemoteSlide,_broadcastSlide,_pendingRemoteSlideleft untouched). The existingResizeObserverincanvas.jscontinues 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— PASScargo fmt --check— PASScargo clippy --workspace --all-targets -- -D warnings— PASSManual Verification Required
focusFrameinvocation).applyRemoteSlideand re-fits to its own viewport; the new resize handler emits no slide events.