1
0

Seperation

This commit is contained in:
Aiden
2026-06-10 11:26:29 +10:00
parent fd82e1666f
commit 899027e531
5 changed files with 553 additions and 464 deletions

View File

@@ -0,0 +1,168 @@
import type { ProjectionMode } from './config.js';
type CameraControlsCallbacks = {
hideControls: () => void;
isEnabled: () => boolean;
showControls: () => void;
};
const MOUSE_SENSITIVITY = 0.002;
const TOUCH_SENSITIVITY = 0.003;
const MOMENTUM_DAMPING = 0.8;
const MAX_PITCH = Math.PI * (45 / 180);
const MAX_YAW = Math.PI * (45 / 180);
export class FallbackCameraControls {
private camera: any;
private readonly callbacks: CameraControlsCallbacks;
private cameraRotation = { yaw: 0, pitch: 0 };
private cameraVelocity = { yaw: 0, pitch: 0 };
private dragging = false;
private lastMouseX = 0;
private lastMouseY = 0;
private lastTouchX = 0;
private lastTouchY = 0;
constructor(camera: any, callbacks: CameraControlsCallbacks) {
this.camera = camera;
this.callbacks = callbacks;
}
get isDragging(): boolean {
return this.dragging;
}
reset(): void {
this.cameraRotation = { yaw: 0, pitch: 0 };
this.cameraVelocity = { yaw: 0, pitch: 0 };
this.dragging = false;
if (this.camera) {
this.camera.rotation.set(0, 0, 0);
}
}
updateCameraRotation(): void {
if (!this.camera) return;
this.cameraRotation.yaw += this.cameraVelocity.yaw;
this.cameraRotation.pitch += this.cameraVelocity.pitch;
this.cameraRotation.pitch = Math.max(-MAX_PITCH, Math.min(MAX_PITCH, this.cameraRotation.pitch));
this.cameraRotation.yaw = Math.max(-MAX_YAW, Math.min(MAX_YAW, this.cameraRotation.yaw));
this.cameraVelocity.yaw *= MOMENTUM_DAMPING;
this.cameraVelocity.pitch *= MOMENTUM_DAMPING;
if (Math.abs(this.cameraVelocity.yaw) < 0.001) this.cameraVelocity.yaw = 0;
if (Math.abs(this.cameraVelocity.pitch) < 0.001) this.cameraVelocity.pitch = 0;
this.camera.rotation.set(this.cameraRotation.pitch, this.cameraRotation.yaw, 0);
}
addEventListeners(canvas: HTMLElement, projectionMode: ProjectionMode): void {
canvas.addEventListener('mousemove', this.onCanvasMouseMove);
if (projectionMode === 'vr180') {
canvas.addEventListener('mousedown', this.onMouseDown);
canvas.addEventListener('mousemove', this.onMouseMove);
canvas.addEventListener('mouseup', this.onMouseUp);
canvas.addEventListener('touchstart', this.onTouchStart, { passive: false });
canvas.addEventListener('touchmove', this.onTouchMove, { passive: false });
canvas.addEventListener('touchend', this.onTouchEnd, { passive: false });
} else {
canvas.addEventListener('touchstart', this.onCanvasTouchStart, { passive: true });
}
}
removeEventListeners(canvas: HTMLElement): void {
canvas.removeEventListener('mousemove', this.onCanvasMouseMove);
canvas.removeEventListener('mousedown', this.onMouseDown);
canvas.removeEventListener('mousemove', this.onMouseMove);
canvas.removeEventListener('mouseup', this.onMouseUp);
canvas.removeEventListener('touchstart', this.onTouchStart);
canvas.removeEventListener('touchmove', this.onTouchMove);
canvas.removeEventListener('touchend', this.onTouchEnd);
canvas.removeEventListener('touchstart', this.onCanvasTouchStart);
}
private readonly onCanvasMouseMove = (): void => {
if (this.callbacks.isEnabled() && !this.dragging) {
this.callbacks.showControls();
}
};
private readonly onCanvasTouchStart = (): void => {
if (this.callbacks.isEnabled()) {
this.callbacks.showControls();
}
};
private readonly onMouseDown = (event: MouseEvent): void => {
if (!this.callbacks.isEnabled()) return;
this.dragging = true;
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
this.cameraVelocity.yaw = 0;
this.cameraVelocity.pitch = 0;
this.callbacks.hideControls();
};
private readonly onMouseMove = (event: MouseEvent): void => {
if (!this.callbacks.isEnabled() || !this.dragging) return;
const deltaX = event.clientX - this.lastMouseX;
const deltaY = event.clientY - this.lastMouseY;
this.cameraVelocity.yaw = deltaX * MOUSE_SENSITIVITY;
this.cameraVelocity.pitch = deltaY * MOUSE_SENSITIVITY;
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
};
private readonly onMouseUp = (): void => {
if (!this.callbacks.isEnabled()) return;
this.dragging = false;
this.callbacks.showControls();
};
private readonly onTouchStart = (event: TouchEvent): void => {
if (!this.callbacks.isEnabled()) return;
event.preventDefault();
if (event.touches.length === 1) {
this.dragging = true;
this.lastTouchX = event.touches[0].clientX;
this.lastTouchY = event.touches[0].clientY;
this.cameraVelocity.yaw = 0;
this.cameraVelocity.pitch = 0;
this.callbacks.hideControls();
}
};
private readonly onTouchMove = (event: TouchEvent): void => {
if (!this.callbacks.isEnabled() || !this.dragging) return;
event.preventDefault();
if (event.touches.length === 1) {
const deltaX = event.touches[0].clientX - this.lastTouchX;
const deltaY = event.touches[0].clientY - this.lastTouchY;
this.cameraVelocity.yaw = deltaX * TOUCH_SENSITIVITY;
this.cameraVelocity.pitch = deltaY * TOUCH_SENSITIVITY;
this.lastTouchX = event.touches[0].clientX;
this.lastTouchY = event.touches[0].clientY;
}
};
private readonly onTouchEnd = (event: TouchEvent): void => {
if (!this.callbacks.isEnabled()) return;
event.preventDefault();
this.dragging = false;
this.callbacks.showControls();
};
}

13
src/vr180player/time.ts Normal file
View File

@@ -0,0 +1,13 @@
export function formatTime(seconds: number): string {
if (!isFinite(seconds)) return '00:00:00';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}

View File

@@ -0,0 +1,322 @@
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
import { drawLucideIcon } from './icons.js';
import { createLucideButtonTexture, drawRoundedRect } from './three-utils.js';
type ButtonLayout = {
centerX: number;
centerY: number;
name: string;
size: number;
texture: any;
};
export type VrControlPanel = {
exitButtonMesh: any;
forwardButtonMesh: any;
group: any;
interactables: any[];
playPauseButtonCanvas: HTMLCanvasElement;
playPauseButtonContext: CanvasRenderingContext2D | null;
playPauseButtonMesh: any;
playPauseButtonTexture: any;
rewindButtonMesh: any;
seekBarHitAreaMesh: any;
seekBarProgressMesh: any;
seekBarTrackMesh: any;
volumeButtonCanvas: HTMLCanvasElement;
volumeButtonContext: CanvasRenderingContext2D | null;
volumeButtonMesh: any;
volumeButtonTexture: any;
};
const FIGMA_PANEL_WIDTH_PX = 450;
const FIGMA_PANEL_HEIGHT_PX = 132;
const FIGMA_CORNER_RADIUS_PX = 30;
const FIGMA_TITLE_FONT_SIZE_PX = 14;
const FIGMA_TITLE_MARGIN_TOP_PX = 20;
const FIGMA_SEEK_BAR_WIDTH_PX = 386;
const FIGMA_SEEK_BAR_HEIGHT_PX = 5;
const FIGMA_SEEK_BAR_Y_OFFSET_FROM_PANEL_CENTER_PX = (FIGMA_PANEL_HEIGHT_PX / 2) - 54;
const FIGMA_PLAYPAUSE_BUTTON_SIZE_PX = 44;
const FIGMA_PLAYPAUSE_BUTTON_X_PX = 225;
const FIGMA_PLAYPAUSE_BUTTON_Y_PX = 90;
const FIGMA_REWIND_BUTTON_SIZE_PX = 44;
const FIGMA_REWIND_BUTTON_X_PX = 169;
const FIGMA_REWIND_BUTTON_Y_PX = 90;
const FIGMA_FORWARD_BUTTON_SIZE_PX = 44;
const FIGMA_FORWARD_BUTTON_X_PX = 281;
const FIGMA_FORWARD_BUTTON_Y_PX = 90;
const FIGMA_EXIT_BUTTON_SIZE_PX = 44;
const FIGMA_EXIT_BUTTON_X_PX = 42;
const FIGMA_EXIT_BUTTON_Y_PX = 90;
const FIGMA_VOLUME_BUTTON_SIZE_PX = 44;
const FIGMA_VOLUME_BUTTON_X_PX = 408;
const FIGMA_VOLUME_BUTTON_Y_PX = 90;
const WORLD_PANEL_WIDTH = 1.5;
const SCALE_FACTOR = WORLD_PANEL_WIDTH / FIGMA_PANEL_WIDTH_PX;
const WORLD_PANEL_HEIGHT = FIGMA_PANEL_HEIGHT_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_WIDTH = FIGMA_SEEK_BAR_WIDTH_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_TRACK_HEIGHT = FIGMA_SEEK_BAR_HEIGHT_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_PROGRESS_HEIGHT = (FIGMA_SEEK_BAR_HEIGHT_PX - 1) * SCALE_FACTOR;
const WORLD_SEEK_BAR_Y_OFFSET = FIGMA_SEEK_BAR_Y_OFFSET_FROM_PANEL_CENTER_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER = 5;
const PANEL_TEXTURE_WIDTH = 1024;
const PANEL_TEXTURE_HEIGHT = Math.round(PANEL_TEXTURE_WIDTH * (FIGMA_PANEL_HEIGHT_PX / FIGMA_PANEL_WIDTH_PX));
const VR_BUTTON_TEXTURE_SIZE = 128;
const VR_BUTTON_ICON_SIZE = 82;
export function createVrControlPanel(scene: any, title: string): VrControlPanel {
const group = new THREE.Group();
group.position.set(0, 0.5, -1.8);
group.rotation.x = 0;
scene.add(group);
const interactables: any[] = [];
const panelMesh = createPanelBackground(title);
group.add(panelMesh);
interactables.push(panelMesh);
const seekBarTrackMaterial = new THREE.MeshBasicMaterial({ color: 0x767676, transparent: true, opacity: 0 });
const seekBarTrackGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_TRACK_HEIGHT);
const seekBarTrackMesh = new THREE.Mesh(seekBarTrackGeometry, seekBarTrackMaterial);
seekBarTrackMesh.name = 'seekBarTrackVisual';
seekBarTrackMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
seekBarTrackMesh.position.z = 0.01;
seekBarTrackMesh.renderOrder = 1;
group.add(seekBarTrackMesh);
const seekBarProgressMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0 });
const seekBarProgressGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_PROGRESS_HEIGHT);
const seekBarProgressMesh = new THREE.Mesh(seekBarProgressGeometry, seekBarProgressMaterial);
seekBarProgressMesh.name = 'seekBarProgressVisual';
seekBarProgressMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET + SCALE_FACTOR;
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
seekBarProgressMesh.position.z = 0.015;
seekBarProgressMesh.scale.x = 0.001;
seekBarProgressMesh.renderOrder = 2;
group.add(seekBarProgressMesh);
const seekBarHitAreaGeometry = new THREE.PlaneGeometry(
WORLD_SEEK_BAR_WIDTH,
WORLD_SEEK_BAR_TRACK_HEIGHT * WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER
);
const seekBarHitAreaMaterial = new THREE.MeshBasicMaterial({ visible: false, transparent: true, opacity: 0 });
const seekBarHitAreaMesh = new THREE.Mesh(seekBarHitAreaGeometry, seekBarHitAreaMaterial);
seekBarHitAreaMesh.name = 'seekBarHitArea';
seekBarHitAreaMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
seekBarHitAreaMesh.position.z = 0.012;
seekBarHitAreaMesh.renderOrder = 2;
group.add(seekBarHitAreaMesh);
interactables.push(seekBarHitAreaMesh);
const playPauseButtonCanvas = document.createElement('canvas');
playPauseButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
playPauseButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
const playPauseButtonContext = playPauseButtonCanvas.getContext('2d');
const playPauseButtonTexture = new THREE.CanvasTexture(playPauseButtonCanvas);
playPauseButtonTexture.minFilter = THREE.LinearFilter;
const playPauseButtonMesh = createButtonMesh({
centerX: FIGMA_PLAYPAUSE_BUTTON_X_PX,
centerY: FIGMA_PLAYPAUSE_BUTTON_Y_PX,
name: 'vrPlayPauseButton',
size: FIGMA_PLAYPAUSE_BUTTON_SIZE_PX,
texture: playPauseButtonTexture
});
group.add(playPauseButtonMesh);
interactables.push(playPauseButtonMesh);
const rewindButtonMesh = createButtonMesh({
centerX: FIGMA_REWIND_BUTTON_X_PX,
centerY: FIGMA_REWIND_BUTTON_Y_PX,
name: 'vrRewindButton',
size: FIGMA_REWIND_BUTTON_SIZE_PX,
texture: createLucideButtonTexture('rotate-ccw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, VR_BUTTON_ICON_SIZE, '15')
});
group.add(rewindButtonMesh);
interactables.push(rewindButtonMesh);
const forwardButtonMesh = createButtonMesh({
centerX: FIGMA_FORWARD_BUTTON_X_PX,
centerY: FIGMA_FORWARD_BUTTON_Y_PX,
name: 'vrForwardButton',
size: FIGMA_FORWARD_BUTTON_SIZE_PX,
texture: createLucideButtonTexture('rotate-cw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, VR_BUTTON_ICON_SIZE, '15')
});
group.add(forwardButtonMesh);
interactables.push(forwardButtonMesh);
const exitButtonMesh = createButtonMesh({
centerX: FIGMA_EXIT_BUTTON_X_PX,
centerY: FIGMA_EXIT_BUTTON_Y_PX,
name: 'vrExitButton',
size: FIGMA_EXIT_BUTTON_SIZE_PX,
texture: createLucideButtonTexture('log-out', '#ffffff', VR_BUTTON_TEXTURE_SIZE, VR_BUTTON_ICON_SIZE)
});
group.add(exitButtonMesh);
interactables.push(exitButtonMesh);
const volumeButtonCanvas = document.createElement('canvas');
volumeButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
volumeButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
const volumeButtonContext = volumeButtonCanvas.getContext('2d');
const volumeButtonTexture = new THREE.CanvasTexture(volumeButtonCanvas);
volumeButtonTexture.minFilter = THREE.LinearFilter;
const volumeButtonMesh = createButtonMesh({
centerX: FIGMA_VOLUME_BUTTON_X_PX,
centerY: FIGMA_VOLUME_BUTTON_Y_PX,
name: 'vrVolumeButton',
size: FIGMA_VOLUME_BUTTON_SIZE_PX,
texture: volumeButtonTexture
});
group.add(volumeButtonMesh);
interactables.push(volumeButtonMesh);
group.visible = false;
return {
exitButtonMesh,
forwardButtonMesh,
group,
interactables,
playPauseButtonCanvas,
playPauseButtonContext,
playPauseButtonMesh,
playPauseButtonTexture,
rewindButtonMesh,
seekBarHitAreaMesh,
seekBarProgressMesh,
seekBarTrackMesh,
volumeButtonCanvas,
volumeButtonContext,
volumeButtonMesh,
volumeButtonTexture
};
}
export function updateVrPlayPauseButtonIcon(panel: VrControlPanel | undefined, isPausedOrEnded: boolean): void {
if (!panel?.playPauseButtonContext || !panel.playPauseButtonTexture) return;
const ctx = panel.playPauseButtonContext;
const canvas = panel.playPauseButtonCanvas;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const iconOffset = (canvas.width - VR_BUTTON_ICON_SIZE) / 2;
drawLucideIcon(ctx, isPausedOrEnded ? 'play' : 'pause', iconOffset, iconOffset, VR_BUTTON_ICON_SIZE, '#ffffff', 2);
panel.playPauseButtonTexture.needsUpdate = true;
}
export function updateVrVolumeButtonIcon(panel: VrControlPanel | undefined, isMuted: boolean): void {
if (!panel?.volumeButtonContext || !panel.volumeButtonTexture) return;
const ctx = panel.volumeButtonContext;
const canvas = panel.volumeButtonCanvas;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const iconOffset = (canvas.width - VR_BUTTON_ICON_SIZE) / 2;
drawLucideIcon(ctx, isMuted ? 'volume-x' : 'volume-2', iconOffset, iconOffset, VR_BUTTON_ICON_SIZE, '#ffffff', 2);
panel.volumeButtonTexture.needsUpdate = true;
}
export function updateVrSeekBarAppearance(panel: VrControlPanel | undefined, progress: number | null): void {
if (!panel?.seekBarProgressMesh) return;
if (progress === null) {
panel.seekBarProgressMesh.scale.x = 0.0001;
panel.seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
return;
}
const normalizedProgress = Math.max(0.0001, Math.min(1, progress));
panel.seekBarProgressMesh.scale.x = normalizedProgress;
panel.seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2 + (WORLD_SEEK_BAR_WIDTH * normalizedProgress) / 2;
}
export function setVrPanelOpacity(panel: VrControlPanel | undefined, opacity: number): void {
if (!panel) return;
panel.group.children.forEach((child: any) => {
if (child.material && Object.prototype.hasOwnProperty.call(child.material, 'opacity')) {
child.material.opacity = opacity;
}
});
}
export function hideVrPanelImmediately(panel: VrControlPanel | undefined): void {
if (!panel) return;
setVrPanelOpacity(panel, 0);
panel.group.visible = false;
}
export function getSeekProgressFromIntersection(panel: VrControlPanel | undefined, intersectionPoint: any): number {
if (!panel?.seekBarTrackMesh) return 0;
const localPoint = panel.seekBarTrackMesh.worldToLocal(intersectionPoint.clone());
const normalizedPosition = (localPoint.x + WORLD_SEEK_BAR_WIDTH / 2) / WORLD_SEEK_BAR_WIDTH;
return Math.max(0, Math.min(1, normalizedPosition));
}
function createPanelBackground(title: string): any {
const panelCanvas = document.createElement('canvas');
panelCanvas.width = PANEL_TEXTURE_WIDTH;
panelCanvas.height = PANEL_TEXTURE_HEIGHT;
const panelCtx = panelCanvas.getContext('2d');
if (!panelCtx) {
throw new Error('Unable to create 2D canvas context for VR control panel.');
}
panelCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
const textureCornerRadius = FIGMA_CORNER_RADIUS_PX * (PANEL_TEXTURE_WIDTH / FIGMA_PANEL_WIDTH_PX);
drawRoundedRect(panelCtx, 0, 0, PANEL_TEXTURE_WIDTH, PANEL_TEXTURE_HEIGHT, textureCornerRadius, true, false);
const titleFontSizeTexturePx = Math.round(FIGMA_TITLE_FONT_SIZE_PX * (PANEL_TEXTURE_HEIGHT / FIGMA_PANEL_HEIGHT_PX));
panelCtx.fillStyle = '#ffffff';
panelCtx.font = `500 ${titleFontSizeTexturePx}px Helvetica, Arial, sans-serif`;
panelCtx.textAlign = 'center';
panelCtx.textBaseline = 'top';
const titleMarginTopTexturePx = FIGMA_TITLE_MARGIN_TOP_PX * (PANEL_TEXTURE_HEIGHT / FIGMA_PANEL_HEIGHT_PX);
panelCtx.fillText(title, PANEL_TEXTURE_WIDTH / 2, titleMarginTopTexturePx);
const panelTexture = new THREE.CanvasTexture(panelCanvas);
panelTexture.minFilter = THREE.LinearFilter;
panelTexture.needsUpdate = true;
const panelMaterial = new THREE.MeshBasicMaterial({
map: panelTexture,
transparent: true,
opacity: 0,
depthWrite: false
});
const panelGeometry = new THREE.PlaneGeometry(WORLD_PANEL_WIDTH, WORLD_PANEL_HEIGHT);
const panelMesh = new THREE.Mesh(panelGeometry, panelMaterial);
panelMesh.name = 'vrControlPanelBackground';
panelMesh.renderOrder = 0;
return panelMesh;
}
function createButtonMesh({ centerX, centerY, name, size, texture }: ButtonLayout): any {
const buttonWorldSize = size * SCALE_FACTOR;
const buttonMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0,
depthWrite: false
});
const buttonGeometry = new THREE.PlaneGeometry(buttonWorldSize, buttonWorldSize);
const buttonMesh = new THREE.Mesh(buttonGeometry, buttonMaterial);
buttonMesh.name = name;
buttonMesh.renderOrder = 3;
const worldButtonOffsetX = (centerX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldButtonOffsetY = -(centerY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
buttonMesh.position.set(worldButtonOffsetX, worldButtonOffsetY, 0.02);
return buttonMesh;
}

View File

@@ -16,12 +16,20 @@ import {
positionPlaneForPresentation as positionPlaneForPresentationCore,
showActiveContentMesh as showActiveContentMeshCore
} from './projection.js';
import { createVideoTexture as createVideoTextureCore } from './three-utils.js';
import { setLucideIcon } from './icons.js';
import { FallbackCameraControls } from './fallback-camera-controls.js';
import { formatTime } from './time.js';
import {
createLucideButtonTexture,
createVideoTexture as createVideoTextureCore,
drawRoundedRect
} from './three-utils.js';
import { drawLucideIcon, setLucideIcon } from './icons.js';
createVrControlPanel,
getSeekProgressFromIntersection,
hideVrPanelImmediately,
setVrPanelOpacity,
type VrControlPanel,
updateVrPlayPauseButtonIcon,
updateVrSeekBarAppearance,
updateVrVolumeButtonIcon
} from './vr-control-panel.js';
const _playerBase = new URL('.', import.meta.url).href;
@@ -44,68 +52,11 @@ const CONTROL_PANEL_HIDE_DELAY = 3000; // 3 seconds
let isXrLoopActive = false;
let is2DMode = false;
let vrControlPanel;
let vrPanel: VrControlPanel | undefined;
// 2D Camera Controls
let camera2D;
let cameraRotation = { yaw: 0, pitch: 0 };
let cameraVelocity = { yaw: 0, pitch: 0 };
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;
let lastTouchX = 0;
let lastTouchY = 0;
// 2D Control Constants
const MOUSE_SENSITIVITY = 0.002;
const TOUCH_SENSITIVITY = 0.003;
const MOMENTUM_DAMPING = 0.8; // Reduced from 0.9 for 50% less inertia
const MAX_PITCH = Math.PI * (45 / 180); // ~45 degrees - edge of VR180 content aligns with viewport edge
const MAX_YAW = Math.PI * (45 / 180); // ~45 degrees - edge of VR180 content aligns with viewport edge
// Figma design constants (for layout reference)
const FIGMA_PANEL_WIDTH_PX = 450;
const FIGMA_PANEL_HEIGHT_PX = 132;
const FIGMA_CORNER_RADIUS_PX = 30;
const FIGMA_TITLE_FONT_SIZE_PX = 14;
const FIGMA_TITLE_MARGIN_TOP_PX = 20;
const FIGMA_SEEK_BAR_WIDTH_PX = 386;
const FIGMA_SEEK_BAR_HEIGHT_PX = 5;
const FIGMA_SEEK_BAR_Y_OFFSET_FROM_PANEL_CENTER_PX = (FIGMA_PANEL_HEIGHT_PX / 2) - 54;
const FIGMA_PLAYPAUSE_BUTTON_SIZE_PX = 44;
const FIGMA_PLAYPAUSE_BUTTON_X_PX = 225;
const FIGMA_PLAYPAUSE_BUTTON_Y_PX = 90;
const FIGMA_REWIND_BUTTON_SIZE_PX = 44;
const FIGMA_REWIND_BUTTON_X_PX = 169;
const FIGMA_REWIND_BUTTON_Y_PX = 90;
const FIGMA_FORWARD_BUTTON_SIZE_PX = 44;
const FIGMA_FORWARD_BUTTON_X_PX = 281;
const FIGMA_FORWARD_BUTTON_Y_PX = 90;
const FIGMA_EXIT_BUTTON_SIZE_PX = 44;
const FIGMA_EXIT_BUTTON_X_PX = 42;
const FIGMA_EXIT_BUTTON_Y_PX = 90;
const FIGMA_VOLUME_BUTTON_SIZE_PX = 44;
const FIGMA_VOLUME_BUTTON_X_PX = 408;
const FIGMA_VOLUME_BUTTON_Y_PX = 90;
// World space dimensions derived from Figma constants
const WORLD_PANEL_WIDTH = 1.5;
const SCALE_FACTOR = WORLD_PANEL_WIDTH / FIGMA_PANEL_WIDTH_PX;
const WORLD_PANEL_HEIGHT = FIGMA_PANEL_HEIGHT_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_WIDTH = FIGMA_SEEK_BAR_WIDTH_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_TRACK_HEIGHT = (FIGMA_SEEK_BAR_HEIGHT_PX) * SCALE_FACTOR;
const WORLD_SEEK_BAR_PROGRESS_HEIGHT = (FIGMA_SEEK_BAR_HEIGHT_PX - 1) * SCALE_FACTOR;
const WORLD_SEEK_BAR_Y_OFFSET = FIGMA_SEEK_BAR_Y_OFFSET_FROM_PANEL_CENTER_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER = 5;
const WORLD_SEEK_BAR_HIT_AREA_HEIGHT = WORLD_SEEK_BAR_TRACK_HEIGHT * WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER;
const PANEL_TEXTURE_WIDTH = 1024;
const PANEL_TEXTURE_HEIGHT = Math.round(PANEL_TEXTURE_WIDTH * (FIGMA_PANEL_HEIGHT_PX / FIGMA_PANEL_WIDTH_PX));
let fallbackCameraControls: FallbackCameraControls | undefined;
// Panel fade animation variables
let panelOpacity = 0;
@@ -116,17 +67,6 @@ let lastFadeTimestamp = 0;
const FADE_DURATION_MS = 200;
const AUTO_HIDE_DELAY_MS = 10000;
// VR UI Meshes
let seekBarTrackMesh, seekBarProgressMesh, seekBarHitAreaMesh;
let vrPlayPauseButtonMesh, vrPlayPauseButtonCanvas, vrPlayPauseButtonContext, vrPlayPauseButtonTexture;
let vrRewindButtonMesh;
let vrForwardButtonMesh;
let vrExitButtonMesh;
let vrVolumeButtonMesh, vrVolumeButtonCanvas, vrVolumeButtonContext, vrVolumeButtonTexture;
const VR_BUTTON_TEXTURE_SIZE = 128;
injectPlayerStyles(_playerBase);
document.addEventListener('DOMContentLoaded', () => {
@@ -225,6 +165,12 @@ function createVideoTexture() {
return videoTexture;
}
function getVideoTitle() {
return videoElement.getAttribute('title') ||
videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
"Video Title";
}
function init() {
try {
@@ -317,6 +263,11 @@ function init() {
camera2D = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera2D.position.set(0, 1.6, 0.1);
camera2D.rotation.set(0, 0, 0);
fallbackCameraControls = new FallbackCameraControls(camera2D, {
hideControls: hide2DControlPanel,
isEnabled: () => is2DMode,
showControls: show2DControlPanel
});
} catch (e) {
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
renderer = null;
@@ -324,170 +275,10 @@ function init() {
}
try { // Phase 2: VR Control Panel UI
vrControlPanel = new THREE.Group();
vrControlPanel.position.set(0, 0.5, -1.8);
vrControlPanel.rotation.x = 0;
scene.add(vrControlPanel);
vrPanel = createVrControlPanel(scene, getVideoTitle());
vrControlPanel = vrPanel.group;
uiElements.push(...vrPanel.interactables);
const panelCanvas = document.createElement('canvas');
panelCanvas.width = PANEL_TEXTURE_WIDTH;
panelCanvas.height = PANEL_TEXTURE_HEIGHT;
const panelCtx = panelCanvas.getContext('2d');
panelCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
const textureCornerRadius = FIGMA_CORNER_RADIUS_PX * (PANEL_TEXTURE_WIDTH / FIGMA_PANEL_WIDTH_PX);
drawRoundedRect(panelCtx, 0, 0, PANEL_TEXTURE_WIDTH, PANEL_TEXTURE_HEIGHT, textureCornerRadius, true, false);
const videoTitle = videoElement.getAttribute('title') || videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') || "Video Title";
const titleFontSizeTexturePx = Math.round(FIGMA_TITLE_FONT_SIZE_PX * (PANEL_TEXTURE_HEIGHT / FIGMA_PANEL_HEIGHT_PX));
panelCtx.fillStyle = '#ffffff';
panelCtx.font = `500 ${titleFontSizeTexturePx}px Helvetica, Arial, sans-serif`;
panelCtx.textAlign = 'center';
panelCtx.textBaseline = 'top';
const titleMarginTopTexturePx = FIGMA_TITLE_MARGIN_TOP_PX * (PANEL_TEXTURE_HEIGHT / FIGMA_PANEL_HEIGHT_PX);
panelCtx.fillText(videoTitle, PANEL_TEXTURE_WIDTH / 2, titleMarginTopTexturePx);
const panelTexture = new THREE.CanvasTexture(panelCanvas);
panelTexture.minFilter = THREE.LinearFilter;
panelTexture.needsUpdate = true;
const panelMaterial = new THREE.MeshBasicMaterial({
map: panelTexture,
transparent: true,
opacity: 0,
depthWrite: false
});
const panelGeometry = new THREE.PlaneGeometry(WORLD_PANEL_WIDTH, WORLD_PANEL_HEIGHT);
const panelMesh = new THREE.Mesh(panelGeometry, panelMaterial);
panelMesh.name = "vrControlPanelBackground";
panelMesh.renderOrder = 0;
vrControlPanel.add(panelMesh);
uiElements.push(panelMesh);
const seekBarTrackMaterial = new THREE.MeshBasicMaterial({ color: 0x767676, transparent: true, opacity: 0 });
const seekBarTrackGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_TRACK_HEIGHT);
seekBarTrackMesh = new THREE.Mesh(seekBarTrackGeometry, seekBarTrackMaterial);
seekBarTrackMesh.name = "seekBarTrackVisual";
seekBarTrackMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
seekBarTrackMesh.position.z = 0.01;
seekBarTrackMesh.renderOrder = 1;
vrControlPanel.add(seekBarTrackMesh);
const seekBarProgressMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0 });
const seekBarProgressGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_PROGRESS_HEIGHT);
seekBarProgressMesh = new THREE.Mesh(seekBarProgressGeometry, seekBarProgressMaterial);
seekBarProgressMesh.name = "seekBarProgressVisual";
seekBarProgressMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET + (1 * SCALE_FACTOR);
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
seekBarProgressMesh.position.z = 0.015;
seekBarProgressMesh.scale.x = 0.001;
seekBarProgressMesh.renderOrder = 2;
vrControlPanel.add(seekBarProgressMesh);
const seekBarHitAreaGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_TRACK_HEIGHT * WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER);
const seekBarHitAreaMaterial = new THREE.MeshBasicMaterial({ visible: false, transparent: true, opacity: 0 });
seekBarHitAreaMesh = new THREE.Mesh(seekBarHitAreaGeometry, seekBarHitAreaMaterial);
seekBarHitAreaMesh.name = "seekBarHitArea";
seekBarHitAreaMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
seekBarHitAreaMesh.position.z = 0.012;
seekBarHitAreaMesh.renderOrder = 2;
vrControlPanel.add(seekBarHitAreaMesh);
uiElements.push(seekBarHitAreaMesh);
vrPlayPauseButtonCanvas = document.createElement('canvas');
vrPlayPauseButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
vrPlayPauseButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
vrPlayPauseButtonContext = vrPlayPauseButtonCanvas.getContext('2d');
vrPlayPauseButtonTexture = new THREE.CanvasTexture(vrPlayPauseButtonCanvas);
vrPlayPauseButtonTexture.minFilter = THREE.LinearFilter;
const playPauseButtonMaterial = new THREE.MeshBasicMaterial({
map: vrPlayPauseButtonTexture,
transparent: true,
opacity: 0,
depthWrite: false
});
const playPauseButtonWorldSize = FIGMA_PLAYPAUSE_BUTTON_SIZE_PX * SCALE_FACTOR;
const playPauseButtonGeometry = new THREE.PlaneGeometry(playPauseButtonWorldSize, playPauseButtonWorldSize);
vrPlayPauseButtonMesh = new THREE.Mesh(playPauseButtonGeometry, playPauseButtonMaterial);
vrPlayPauseButtonMesh.name = "vrPlayPauseButton";
vrPlayPauseButtonMesh.renderOrder = 3;
const figmaPlayPauseButtonCenterX = FIGMA_PLAYPAUSE_BUTTON_X_PX;
const figmaPlayPauseButtonCenterY = FIGMA_PLAYPAUSE_BUTTON_Y_PX;
const worldPlayPauseButtonOffsetX = (figmaPlayPauseButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldPlayPauseButtonOffsetY = -(figmaPlayPauseButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrPlayPauseButtonMesh.position.set(worldPlayPauseButtonOffsetX, worldPlayPauseButtonOffsetY, 0.02);
vrControlPanel.add(vrPlayPauseButtonMesh);
uiElements.push(vrPlayPauseButtonMesh);
const rewindButtonWorldSize = FIGMA_REWIND_BUTTON_SIZE_PX * SCALE_FACTOR;
const rewindButtonTexture = createLucideButtonTexture('rotate-ccw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, 82, '15');
const rewindButtonMaterial = new THREE.MeshBasicMaterial({ map: rewindButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const rewindButtonGeometry = new THREE.PlaneGeometry(rewindButtonWorldSize, rewindButtonWorldSize);
vrRewindButtonMesh = new THREE.Mesh(rewindButtonGeometry, rewindButtonMaterial);
vrRewindButtonMesh.name = "vrRewindButton";
vrRewindButtonMesh.renderOrder = 3;
const figmaRewindButtonCenterX = FIGMA_REWIND_BUTTON_X_PX;
const figmaRewindButtonCenterY = FIGMA_REWIND_BUTTON_Y_PX;
const worldRewindButtonOffsetX = (figmaRewindButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldRewindButtonOffsetY = -(figmaRewindButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrRewindButtonMesh.position.set(worldRewindButtonOffsetX, worldRewindButtonOffsetY, 0.02);
vrControlPanel.add(vrRewindButtonMesh);
uiElements.push(vrRewindButtonMesh);
const forwardButtonWorldSize = FIGMA_FORWARD_BUTTON_SIZE_PX * SCALE_FACTOR;
const forwardButtonTexture = createLucideButtonTexture('rotate-cw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, 82, '15');
const forwardButtonMaterial = new THREE.MeshBasicMaterial({ map: forwardButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const forwardButtonGeometry = new THREE.PlaneGeometry(forwardButtonWorldSize, forwardButtonWorldSize);
vrForwardButtonMesh = new THREE.Mesh(forwardButtonGeometry, forwardButtonMaterial);
vrForwardButtonMesh.name = "vrForwardButton";
vrForwardButtonMesh.renderOrder = 3;
const figmaForwardButtonCenterX = FIGMA_FORWARD_BUTTON_X_PX;
const figmaForwardButtonCenterY = FIGMA_FORWARD_BUTTON_Y_PX;
const worldForwardButtonOffsetX = (figmaForwardButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldForwardButtonOffsetY = -(figmaForwardButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrForwardButtonMesh.position.set(worldForwardButtonOffsetX, worldForwardButtonOffsetY, 0.02);
vrControlPanel.add(vrForwardButtonMesh);
uiElements.push(vrForwardButtonMesh);
const exitButtonWorldSize = FIGMA_EXIT_BUTTON_SIZE_PX * SCALE_FACTOR;
const exitButtonTexture = createLucideButtonTexture('log-out', '#ffffff', VR_BUTTON_TEXTURE_SIZE, 82);
const exitButtonMaterial = new THREE.MeshBasicMaterial({ map: exitButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const exitButtonGeometry = new THREE.PlaneGeometry(exitButtonWorldSize, exitButtonWorldSize);
vrExitButtonMesh = new THREE.Mesh(exitButtonGeometry, exitButtonMaterial);
vrExitButtonMesh.name = "vrExitButton";
vrExitButtonMesh.renderOrder = 3;
const figmaExitButtonCenterX = FIGMA_EXIT_BUTTON_X_PX;
const figmaExitButtonCenterY = FIGMA_EXIT_BUTTON_Y_PX;
const worldExitButtonOffsetX = (figmaExitButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldExitButtonOffsetY = -(figmaExitButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrExitButtonMesh.position.set(worldExitButtonOffsetX, worldExitButtonOffsetY, 0.02);
vrControlPanel.add(vrExitButtonMesh);
uiElements.push(vrExitButtonMesh);
vrVolumeButtonCanvas = document.createElement('canvas');
vrVolumeButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
vrVolumeButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
vrVolumeButtonContext = vrVolumeButtonCanvas.getContext('2d');
vrVolumeButtonTexture = new THREE.CanvasTexture(vrVolumeButtonCanvas);
vrVolumeButtonTexture.minFilter = THREE.LinearFilter;
const volumeButtonMaterial = new THREE.MeshBasicMaterial({ map: vrVolumeButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const volumeButtonWorldSize = FIGMA_VOLUME_BUTTON_SIZE_PX * SCALE_FACTOR;
const volumeButtonGeometry = new THREE.PlaneGeometry(volumeButtonWorldSize, volumeButtonWorldSize);
vrVolumeButtonMesh = new THREE.Mesh(volumeButtonGeometry, volumeButtonMaterial);
vrVolumeButtonMesh.name = "vrVolumeButton";
vrVolumeButtonMesh.renderOrder = 3;
const figmaVolumeButtonCenterX = FIGMA_VOLUME_BUTTON_X_PX;
const figmaVolumeButtonCenterY = FIGMA_VOLUME_BUTTON_Y_PX;
const worldVolumeButtonOffsetX = (figmaVolumeButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldVolumeButtonOffsetY = -(figmaVolumeButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrVolumeButtonMesh.position.set(worldVolumeButtonOffsetX, worldVolumeButtonOffsetY, 0.02);
vrControlPanel.add(vrVolumeButtonMesh);
uiElements.push(vrVolumeButtonMesh);
vrControlPanel.visible = false;
panelOpacity = 0;
panelTargetOpacity = 0;
@@ -561,40 +352,24 @@ function init() {
function updateVRPlayPauseButtonIcon() {
if (!vrPlayPauseButtonContext || !vrPlayPauseButtonTexture || !video) {
if (!video) {
return;
}
const ctx = vrPlayPauseButtonContext;
const canvas = vrPlayPauseButtonCanvas;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const iconSize = 82;
const iconOffset = (canvas.width - iconSize) / 2;
drawLucideIcon(ctx, video.paused || video.ended ? 'play' : 'pause', iconOffset, iconOffset, iconSize, '#ffffff', 2);
vrPlayPauseButtonTexture.needsUpdate = true;
updateVrPlayPauseButtonIcon(vrPanel, video.paused || video.ended);
}
function updateVRVolumeButtonIcon() {
if (!vrVolumeButtonContext || !vrVolumeButtonTexture || !video) {
if (!video) {
return;
}
const ctx = vrVolumeButtonContext;
const canvas = vrVolumeButtonCanvas;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const iconSize = 82;
const iconOffset = (canvas.width - iconSize) / 2;
drawLucideIcon(ctx, video.muted || video.volume === 0 ? 'volume-x' : 'volume-2', iconOffset, iconOffset, iconSize, '#ffffff', 2);
vrVolumeButtonTexture.needsUpdate = true;
updateVrVolumeButtonIcon(vrPanel, video.muted || video.volume === 0);
}
function updateSeekBarAppearance() {
if (video && isFinite(video.duration) && video.duration > 0 && seekBarProgressMesh) {
const progress = video.currentTime / video.duration;
seekBarProgressMesh.scale.x = Math.max(0.0001, progress);
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2 + (WORLD_SEEK_BAR_WIDTH * progress) / 2;
} else if (seekBarProgressMesh) {
seekBarProgressMesh.scale.x = 0.0001;
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
}
const progress = video && isFinite(video.duration) && video.duration > 0
? video.currentTime / video.duration
: null;
updateVrSeekBarAppearance(vrPanel, progress);
}
function animatePanelFade(timestamp) {
@@ -623,11 +398,7 @@ function animatePanelFade(timestamp) {
isPanelFading = false;
}
if (opacityChanged) {
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = panelOpacity;
}
});
setVrPanelOpacity(vrPanel, panelOpacity);
}
if (isPanelFading) requestAnimationFrame(animatePanelFade);
}
@@ -710,113 +481,12 @@ function onWindowResize() {
}
}
// 2D Camera Control Functions
function updateCameraRotation() {
if (!camera2D) return;
// Apply momentum
cameraRotation.yaw += cameraVelocity.yaw;
cameraRotation.pitch += cameraVelocity.pitch;
// Constrain pitch (vertical rotation)
cameraRotation.pitch = Math.max(-MAX_PITCH, Math.min(MAX_PITCH, cameraRotation.pitch));
// Constrain yaw (horizontal rotation) to VR180 content boundaries
cameraRotation.yaw = Math.max(-MAX_YAW, Math.min(MAX_YAW, cameraRotation.yaw));
// Apply damping to velocity
cameraVelocity.yaw *= MOMENTUM_DAMPING;
cameraVelocity.pitch *= MOMENTUM_DAMPING;
// Stop very small velocities
if (Math.abs(cameraVelocity.yaw) < 0.001) cameraVelocity.yaw = 0;
if (Math.abs(cameraVelocity.pitch) < 0.001) cameraVelocity.pitch = 0;
// Apply rotation to camera
camera2D.rotation.set(cameraRotation.pitch, cameraRotation.yaw, 0);
}
// Mouse Controls
function onMouseDown(event) {
if (!is2DMode) return;
isDragging = true;
lastMouseX = event.clientX;
lastMouseY = event.clientY;
cameraVelocity.yaw = 0;
cameraVelocity.pitch = 0;
// Hide controls when dragging starts
hide2DControlPanel();
}
function onMouseMove(event) {
if (!is2DMode || !isDragging) return;
const deltaX = event.clientX - lastMouseX;
const deltaY = event.clientY - lastMouseY;
cameraVelocity.yaw = deltaX * MOUSE_SENSITIVITY;
cameraVelocity.pitch = deltaY * MOUSE_SENSITIVITY;
lastMouseX = event.clientX;
lastMouseY = event.clientY;
}
function onMouseUp(event) {
if (!is2DMode) return;
isDragging = false;
// Show controls when dragging ends
show2DControlPanel();
}
// Touch Controls
function onTouchStart(event) {
if (!is2DMode) return;
event.preventDefault();
if (event.touches.length === 1) {
isDragging = true;
lastTouchX = event.touches[0].clientX;
lastTouchY = event.touches[0].clientY;
cameraVelocity.yaw = 0;
cameraVelocity.pitch = 0;
// Hide controls when dragging starts
hide2DControlPanel();
}
}
function onTouchMove(event) {
if (!is2DMode || !isDragging) return;
event.preventDefault();
if (event.touches.length === 1) {
const deltaX = event.touches[0].clientX - lastTouchX;
const deltaY = event.touches[0].clientY - lastTouchY;
cameraVelocity.yaw = deltaX * TOUCH_SENSITIVITY;
cameraVelocity.pitch = deltaY * TOUCH_SENSITIVITY;
lastTouchX = event.touches[0].clientX;
lastTouchY = event.touches[0].clientY;
}
}
function onTouchEnd(event) {
if (!is2DMode) return;
event.preventDefault();
isDragging = false;
// Show controls when dragging ends
show2DControlPanel();
}
// 2D Render Loop
function render2D() {
if (!is2DMode) return;
if (projectionMode === 'vr180') {
updateCameraRotation();
fallbackCameraControls?.updateCameraRotation();
} else if (camera2D) {
camera2D.rotation.set(0, 0, 0);
}
@@ -850,10 +520,7 @@ function init2DControlPanel() {
// Set initial video title
if (videoTitle && video) {
const title = video.getAttribute('title') ||
video.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
"Demo Video";
videoTitle.textContent = title;
videoTitle.textContent = getVideoTitle();
}
// Add event listeners for 2D controls
@@ -926,30 +593,6 @@ function hide2DControlPanel() {
isControlPanelVisible = false;
}
function onCanvasMouseMove() {
if (is2DMode && !isDragging) {
show2DControlPanel();
}
}
function onCanvasTouchStart() {
if (is2DMode) {
show2DControlPanel();
}
}
function on2DMouseMove() {
if (is2DMode) {
show2DControlPanel();
}
}
function on2DTouchStart() {
if (is2DMode && !isDragging) {
show2DControlPanel();
}
}
function update2DControlPanel() {
if (!is2DMode || !video) return;
@@ -1062,20 +705,6 @@ function position2DControlPanel() {
controlPanel.style.zIndex = '1000'; // Ensure it's above the canvas
}
function formatTime(seconds) {
if (!isFinite(seconds)) return '00:00:00';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
}
function hidePlayButton() {
if (playBtn) {
playBtn.classList.add('hidden');
@@ -1140,9 +769,7 @@ function resetToOriginalState() {
hide2DControlPanel();
// Reset camera rotation
cameraRotation = { yaw: 0, pitch: 0 };
cameraVelocity = { yaw: 0, pitch: 0 };
isDragging = false;
fallbackCameraControls?.reset();
positionPlaneForPresentation(false);
// Hide WebGL canvas and show video element
@@ -1212,9 +839,7 @@ function onSelectStartVR(event) {
showPanel();
} else if (firstIntersected.name === "seekBarHitArea" && video && isFinite(video.duration)) {
showPanel();
const localPoint = seekBarTrackMesh.worldToLocal(intersectionPoint.clone());
const normalizedPosition = (localPoint.x + WORLD_SEEK_BAR_WIDTH / 2) / WORLD_SEEK_BAR_WIDTH;
const newTime = Math.max(0, Math.min(1, normalizedPosition)) * video.duration;
const newTime = getSeekProgressFromIntersection(vrPanel, intersectionPoint) * video.duration;
video.currentTime = newTime;
updateSeekBarAppearance();
} else if (firstIntersected.name === "vrControlPanelBackground" || firstIntersected === activeContentMesh) {
@@ -1322,39 +947,13 @@ function start2DMode() {
}
function add2DEventListeners() {
// Canvas-specific mouse movement for showing controls
renderer.domElement.addEventListener('mousemove', onCanvasMouseMove);
if (projectionMode === 'vr180') {
// Mouse events
renderer.domElement.addEventListener('mousedown', onMouseDown);
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('mouseup', onMouseUp);
// Touch events
renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });
renderer.domElement.addEventListener('touchend', onTouchEnd, { passive: false });
} else {
renderer.domElement.addEventListener('touchstart', onCanvasTouchStart, { passive: true });
}
fallbackCameraControls?.addEventListeners(renderer.domElement, projectionMode);
}
function remove2DEventListeners() {
if (!renderer || !renderer.domElement) return;
renderer.domElement.removeEventListener('mousemove', onCanvasMouseMove);
// Mouse events
renderer.domElement.removeEventListener('mousedown', onMouseDown);
renderer.domElement.removeEventListener('mousemove', onMouseMove);
renderer.domElement.removeEventListener('mouseup', onMouseUp);
// Touch events
renderer.domElement.removeEventListener('touchstart', onTouchStart);
renderer.domElement.removeEventListener('touchmove', onTouchMove);
renderer.domElement.removeEventListener('touchend', onTouchEnd);
renderer.domElement.removeEventListener('touchstart', onCanvasTouchStart);
fallbackCameraControls?.removeEventListeners(renderer.domElement);
// Fullscreen events
document.removeEventListener('fullscreenchange', onFullscreenChange);
@@ -1386,12 +985,7 @@ async function actualSessionToggle() {
clearTimeout(panelHideTimeout);
panelTargetOpacity = 0;
panelOpacity = 0;
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = 0;
}
});
vrControlPanel.visible = false;
hideVrPanelImmediately(vrPanel);
isPanelFading = false;
}
sessionToClose.end().catch(err => {
@@ -1440,14 +1034,9 @@ async function actualSessionToggle() {
updateVRPlayPauseButtonIcon();
updateVRVolumeButtonIcon();
if (vrControlPanel) {
vrControlPanel.visible = false;
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
clearTimeout(panelHideTimeout);
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = 0;
}
});
hideVrPanelImmediately(vrPanel);
}
await renderer.xr.setSession(xrSession);
@@ -1465,8 +1054,9 @@ async function actualSessionToggle() {
if (sphereMaterial) { sphereMaterial.map = null; sphereMaterial.needsUpdate = true; }
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
if (vrControlPanel) {
vrControlPanel.visible = false; panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
clearTimeout(panelHideTimeout);
hideVrPanelImmediately(vrPanel);
}
if (xrSession) {
xrSession.removeEventListener('end', onVRSessionEnd);
@@ -1517,12 +1107,7 @@ function onVRSessionEnd(event) {
clearTimeout(panelHideTimeout);
isPanelFading = false;
panelOpacity = 0; panelTargetOpacity = 0;
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = 0;
}
});
vrControlPanel.visible = false;
hideVrPanelImmediately(vrPanel);
}
if (endedSession && typeof endedSession.removeEventListener === 'function') {