From 4930d7a841d390fcf015ee9c3535782ed107f6e9 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 22 Apr 2026 14:28:54 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20integrate=20theater=20mode=20into?= =?UTF-8?q?=20replay=20viewer=20per=20=C2=A716.17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the existing TheaterMode component into the replay page — adds fullscreen toggle button on canvas, F key shortcut, auto-hiding controls with 3s inactivity, vignette pulse on critical moments, and mobile Fullscreen API support. Co-Authored-By: Claude Opus 4.7 --- web/src/pages/replay.ts | 52 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/web/src/pages/replay.ts b/web/src/pages/replay.ts index 23251ce..0ef226d 100644 --- a/web/src/pages/replay.ts +++ b/web/src/pages/replay.ts @@ -1,5 +1,5 @@ // Standalone replay viewer page - lazy loaded from app.ts -import type { Replay, GameEvent, DebugInfo, Position } from '../types'; +import type { Replay, GameEvent, DebugInfo, Position, ViewMode } from '../types'; import { fetchCommentary } from '../api-types'; import { AnnotationOverlay, @@ -25,6 +25,7 @@ import { type DirectorState, type DurationPreset, } from '../components/director'; +import { THEATER_STYLES, TheaterMode } from '../components/theater'; const loadReplayViewer = () => import('../replay-viewer'); @@ -56,9 +57,10 @@ function initReplayViewerWithClass(ReplayViewerClass: any, initialUrl?: string):
-
+
Load a replay file to view
+
@@ -248,6 +250,7 @@ function initReplayViewerWithClass(ReplayViewerClass: any, initialUrl?: string): HomeEnd First/Last 1-6 Follow Bot 0/Esc Exit Follow + F Theater Mode
@@ -399,6 +402,7 @@ function initReplayViewerWithClass(ReplayViewerClass: any, initialUrl?: string): + `; initReplayViewer(ReplayViewerClass, initialUrl); @@ -449,6 +453,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { const mobileTurnSlider = document.getElementById('mobile-turn-slider') as HTMLInputElement; const mobileSpeedBtn = document.getElementById('mobile-speed-btn') as HTMLButtonElement; const mobileTimeline = document.getElementById('mobile-timeline') as HTMLDivElement; + const viewModeSelect = document.getElementById('view-mode-select') as HTMLSelectElement; const mobileViewModeBtn = document.getElementById('mobile-view-mode-btn') as HTMLButtonElement; let viewer = new ReplayViewerClass(canvas, { cellSize: 10 }); @@ -456,6 +461,40 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { let commentaryEnabled = true; let debugPanelExpanded = false; + // Theater mode + const theaterBtn = document.getElementById('theater-btn') as HTMLButtonElement; + const theater = new TheaterMode(canvas, { + getScoreText: () => { + const replay = viewer.getReplay() as Replay | null; + if (!replay) return ''; + return replay.players.map((p: any, i: number) => `${p.name}: ${replay.result.scores?.[i] ?? 0}`).join(' '); + }, + getPlayerColors: () => { + const replay = viewer.getReplay() as Replay | null; + if (!replay) return []; + const palettes = ['#332288', '#88ccee', '#44aa99', '#117733', '#999933', '#ddcc77']; + return replay.players.map((_: any, i: number) => palettes[i] ?? '#888888'); + }, + getWinProb: () => { + const replay = viewer.getReplay() as Replay | null; + if (!replay?.win_prob) return []; + const turn = viewer.getTurn(); + return replay.win_prob[turn] ?? []; + }, + getTurn: () => viewer.getTurn(), + getTotalTurns: () => viewer.getTotalTurns(), + getIsPlaying: () => viewer.getIsPlaying(), + getSpeed: () => { + const el = document.getElementById('speed-slider') as HTMLInputElement; + return el ? parseInt(el.value, 10) : 100; + }, + togglePlay: () => viewer.togglePlay(), + setTurn: (t: number) => { viewer.setTurn(t); updateUI(); updateEventLog(); }, + exitTheater: () => {}, + onCriticalMoment: () => theater.pulseVignette(), + }); + theaterBtn.addEventListener('click', () => theater.toggle()); + // Director mode state let directorState: DirectorState = createDirectorState(); let directorConfig: DirectorConfig = loadDirectorConfig(); @@ -1122,6 +1161,10 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { viewer.setFogOfWar(value === '' ? null : parseInt(value, 10)); }); + viewModeSelect.addEventListener('change', () => { + viewer.setViewMode(viewModeSelect.value as ViewMode); + }); + cellSizeSelect.addEventListener('change', () => { const size = parseInt(cellSizeSelect.value, 10); const replay = viewer.getReplay(); @@ -1361,8 +1404,13 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { e.preventDefault(); navigateToNextCriticalMoment(); break; + case 'KeyF': + e.preventDefault(); + theater.toggle(); + break; case 'Digit0': case 'Escape': + if (theater.isActive()) break; // let theater handle its own escape e.preventDefault(); viewer.setFollowPlayer(null); break;