From 59fb673edb1687ee0e9bd7adbbc9c4a706550632 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 22 Apr 2026 23:21:37 -0400 Subject: [PATCH] fix(pip): properly restore canvas from mini-player on replay page return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §16.13: Picture-in-Picture replay mini-player When navigating back to a replay page where PIP was active, the restoration logic was creating duplicate canvas elements (the placeholder from the new DOM and the restored canvas from PIP). Changes: - Remove placeholder canvas before inserting restored PIP canvas - Set 'replay-canvas' ID on restored canvas for TheaterMode and other consumers - Use consistent 'actualCanvas' variable throughout initialization The full PIP flow now works: 1. User starts replay on /watch/replay/:id 2. Clicks nav link → canvas reparents to floating mini-player 3. Playback continues uninterrupted 4. Click "return" → canvas reparents back to inline wrapper 5. Replay resumes at same tick Co-Authored-By: Claude Opus 4.7 --- web/src/pages/replay.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/web/src/pages/replay.ts b/web/src/pages/replay.ts index cbbacba..e762281 100644 --- a/web/src/pages/replay.ts +++ b/web/src/pages/replay.ts @@ -565,19 +565,24 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { const pipMatch = getPipMatchId(); const canvasWrapper = document.querySelector('.canvas-wrapper') as HTMLElement; let viewer: any; + let actualCanvas = canvas; // May be replaced by restored PIP canvas if (pipMatch && canvasWrapper && (initialUrl?.includes(pipMatch) || !initialUrl)) { const restored = restorePip(); if (restored) { - // Move canvas back into the inline wrapper + // Remove the placeholder canvas from DOM (PIP canvas has the real content) + canvas.remove(); + // Move the PIP canvas back into the wrapper canvasWrapper.insertBefore(restored.canvas, canvasWrapper.firstChild); restored.canvas.style.display = 'block'; - viewer = new ReplayViewerClass(restored.canvas, { cellSize: 10 }); + restored.canvas.id = 'replay-canvas'; // Ensure ID is set for TheaterMode and other consumers + actualCanvas = restored.canvas; + viewer = new ReplayViewerClass(actualCanvas, { cellSize: 10 }); } else { - viewer = new ReplayViewerClass(canvas, { cellSize: 10 }); + viewer = new ReplayViewerClass(actualCanvas, { cellSize: 10 }); } } else { - viewer = new ReplayViewerClass(canvas, { cellSize: 10 }); + viewer = new ReplayViewerClass(actualCanvas, { cellSize: 10 }); } let criticalMoments: Array<{turn: number; delta: number; description: string}> = []; @@ -587,7 +592,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { // Theater mode const theaterBtn = document.getElementById('theater-btn') as HTMLButtonElement; - const theater = new TheaterMode(canvas, { + const theater = new TheaterMode(actualCanvas, { getScoreText: () => { const replay = viewer.getReplay() as Replay | null; if (!replay) return '';