import type { ProjectionMode } from '../config.js'; import type { FallbackCameraControls } from './fallback-camera-controls.js'; import { hideRendererCanvas, resizeFallbackRenderer, showFallbackCanvas } from '../rendering/renderer-lifecycle.js'; import { TwoDControlPanel } from '../dom/two-d-control-panel.js'; import type { MediaCapabilities } from '../media/media-adapter.js'; type TwoDModeCallbacks = { createMediaTexture: () => any; forward: () => void; getIsLooping: () => boolean; positionPlaneForPresentation: (isFallback2D?: boolean) => void; rewind: () => void; seekToProgress: (progress: number) => void; showActiveContentMesh: () => void; toggleLoop: () => boolean; toggleMute: () => void; togglePlayPause: () => void; }; type TwoDModeOptions = { callbacks: TwoDModeCallbacks; fullscreenTarget: HTMLElement; getControlsAutoHideDelayMs?: () => number; mediaCapabilities: MediaCapabilities; getActiveContentMesh: () => any; getCamera: () => any; getCameraControls: () => FallbackCameraControls | undefined; getMaterial: () => any; getMediaElement: () => HTMLElement | undefined; getRenderer: () => any; getScene: () => any; getVideo: () => HTMLVideoElement | undefined; playerContainer: HTMLElement; projectionMode: ProjectionMode; title: string; }; const FULLSCREEN_RESIZE_DELAY = 100; export class TwoDMode { private readonly callbacks: TwoDModeCallbacks; private readonly controls: TwoDControlPanel; private readonly fullscreenTarget: HTMLElement; private readonly mediaCapabilities: MediaCapabilities; private readonly getActiveContentMesh: () => any; private readonly getCamera: () => any; private readonly getCameraControls: () => FallbackCameraControls | undefined; private readonly getMaterial: () => any; private readonly getMediaElement: () => HTMLElement | undefined; private readonly getRenderer: () => any; private readonly getScene: () => any; private readonly getVideo: () => HTMLVideoElement | undefined; private readonly playerContainer: HTMLElement; private readonly projectionMode: ProjectionMode; private active = false; constructor(options: TwoDModeOptions) { this.callbacks = options.callbacks; this.fullscreenTarget = options.fullscreenTarget; this.mediaCapabilities = options.mediaCapabilities; this.getActiveContentMesh = options.getActiveContentMesh; this.getCamera = options.getCamera; this.getCameraControls = options.getCameraControls; this.getMaterial = options.getMaterial; this.getMediaElement = options.getMediaElement; this.getRenderer = options.getRenderer; this.getScene = options.getScene; this.getVideo = options.getVideo; this.playerContainer = options.playerContainer; this.projectionMode = options.projectionMode; this.controls = new TwoDControlPanel({ callbacks: { getIsLooping: this.callbacks.getIsLooping, onForward: () => { this.callbacks.forward(); }, onMute: () => { this.callbacks.toggleMute(); }, onPlayPause: this.callbacks.togglePlayPause, onRewind: () => { this.callbacks.rewind(); }, onSeek: (progress) => { this.callbacks.seekToProgress(progress); }, onToggleLoop: this.callbacks.toggleLoop }, mediaCapabilities: this.mediaCapabilities, fullscreenTarget: this.fullscreenTarget, getAutoHideDelayMs: options.getControlsAutoHideDelayMs, getIsActive: () => this.active, playerContainer: this.playerContainer, title: options.title }); } get isActive(): boolean { return this.active; } start(): void { const mediaElement = this.getMediaElement(); const renderer = this.getRenderer(); const camera = this.getCamera(); if (!mediaElement || !renderer || !camera) { console.error("Required components not available for 2D mode"); return; } this.active = true; this.resizeCanvasFor2D(renderer, camera); const canvas = showFallbackCanvas(renderer); mediaElement.style.display = 'none'; const mediaTexture = this.callbacks.createMediaTexture(); this.callbacks.positionPlaneForPresentation(this.projectionMode === 'plane'); const material = this.getMaterial(); const activeContentMesh = this.getActiveContentMesh(); if (material && activeContentMesh) { material.map = mediaTexture; material.needsUpdate = true; this.callbacks.showActiveContentMesh(); } if (this.mediaCapabilities.playback) { this.callbacks.togglePlayPause(); } this.addEventListeners(canvas); this.controls.show(); this.positionControls(); this.render(); } stop(): void { this.active = false; const renderer = this.getRenderer(); if (renderer?.domElement) { this.removeEventListeners(renderer.domElement); hideRendererCanvas(renderer); } else { this.removeFullscreenEventListeners(); } this.controls.hide(); this.getCameraControls()?.reset(); this.callbacks.positionPlaneForPresentation(false); const mediaElement = this.getMediaElement(); if (mediaElement) { mediaElement.style.display = ''; } } resize(): boolean { if (!this.active) { return false; } const renderer = this.getRenderer(); const camera = this.getCamera(); if (renderer && camera) { this.resizeCanvasFor2D(renderer, camera); this.positionControls(); } return true; } showControls(): void { this.controls.show(); } hideControls(): void { this.controls.hide(); } updateTimeline(): void { if (!this.active) return; if (!this.mediaCapabilities.timeline) return; const video = this.getVideo(); if (video) { this.controls.updateTime(video.currentTime, video.duration); } } updatePlaybackButton(): void { if (!this.active) return; if (!this.mediaCapabilities.playback) return; const video = this.getVideo(); if (video) { this.controls.updatePlaybackButton(video.paused || video.ended); } } updateMuteButton(): void { if (!this.active) return; if (!this.mediaCapabilities.audio) return; const video = this.getVideo(); if (video) { this.controls.updateMuteButton(video.muted); } } handleVideoEnd(): void { if (!this.active) return; if (!this.mediaCapabilities.playback) return; this.controls.showPersistent(); this.updatePlaybackButton(); } private addEventListeners(canvas: HTMLElement): void { this.getCameraControls()?.addEventListeners(canvas, this.projectionMode); document.addEventListener('fullscreenchange', this.onFullscreenChange); document.addEventListener('webkitfullscreenchange', this.onFullscreenChange); document.addEventListener('mozfullscreenchange', this.onFullscreenChange); document.addEventListener('MSFullscreenChange', this.onFullscreenChange); } private removeEventListeners(canvas: HTMLElement): void { this.getCameraControls()?.removeEventListeners(canvas); this.removeFullscreenEventListeners(); } private removeFullscreenEventListeners(): void { document.removeEventListener('fullscreenchange', this.onFullscreenChange); document.removeEventListener('webkitfullscreenchange', this.onFullscreenChange); document.removeEventListener('mozfullscreenchange', this.onFullscreenChange); document.removeEventListener('MSFullscreenChange', this.onFullscreenChange); } private readonly onFullscreenChange = (): void => { if (!this.active) return; window.setTimeout(() => { this.resize(); }, FULLSCREEN_RESIZE_DELAY); }; positionControls(): void { const renderer = this.getRenderer(); if (renderer?.domElement) { this.controls.position(renderer.domElement); } } private readonly render = (): void => { if (!this.active) return; const camera = this.getCamera(); if (this.projectionMode === 'vr180') { this.getCameraControls()?.updateCameraRotation(); } else if (camera) { camera.rotation.set(0, 0, 0); } const renderer = this.getRenderer(); const scene = this.getScene(); if (renderer && camera && scene) { renderer.render(scene, camera); } requestAnimationFrame(this.render); }; private resizeCanvasFor2D(renderer: any, camera: any): void { resizeFallbackRenderer({ camera2D: camera, playerContainer: this.playerContainer, renderer }); } }