forked from EXT/VR180-Web-Player
Seperation
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>VR Web Player</title>
|
<title>VR Web Player</title>
|
||||||
|
<link rel="stylesheet" href="./vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
|||||||
168
src/vr180player/fallback-camera-controls.ts
Normal file
168
src/vr180player/fallback-camera-controls.ts
Normal 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
13
src/vr180player/time.ts
Normal 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')}`;
|
||||||
|
}
|
||||||
322
src/vr180player/vr-control-panel.ts
Normal file
322
src/vr180player/vr-control-panel.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -16,12 +16,20 @@ import {
|
|||||||
positionPlaneForPresentation as positionPlaneForPresentationCore,
|
positionPlaneForPresentation as positionPlaneForPresentationCore,
|
||||||
showActiveContentMesh as showActiveContentMeshCore
|
showActiveContentMesh as showActiveContentMeshCore
|
||||||
} from './projection.js';
|
} 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 {
|
import {
|
||||||
createLucideButtonTexture,
|
createVrControlPanel,
|
||||||
createVideoTexture as createVideoTextureCore,
|
getSeekProgressFromIntersection,
|
||||||
drawRoundedRect
|
hideVrPanelImmediately,
|
||||||
} from './three-utils.js';
|
setVrPanelOpacity,
|
||||||
import { drawLucideIcon, setLucideIcon } from './icons.js';
|
type VrControlPanel,
|
||||||
|
updateVrPlayPauseButtonIcon,
|
||||||
|
updateVrSeekBarAppearance,
|
||||||
|
updateVrVolumeButtonIcon
|
||||||
|
} from './vr-control-panel.js';
|
||||||
|
|
||||||
const _playerBase = new URL('.', import.meta.url).href;
|
const _playerBase = new URL('.', import.meta.url).href;
|
||||||
|
|
||||||
@@ -44,68 +52,11 @@ const CONTROL_PANEL_HIDE_DELAY = 3000; // 3 seconds
|
|||||||
let isXrLoopActive = false;
|
let isXrLoopActive = false;
|
||||||
let is2DMode = false;
|
let is2DMode = false;
|
||||||
let vrControlPanel;
|
let vrControlPanel;
|
||||||
|
let vrPanel: VrControlPanel | undefined;
|
||||||
|
|
||||||
// 2D Camera Controls
|
// 2D Camera Controls
|
||||||
let camera2D;
|
let camera2D;
|
||||||
let cameraRotation = { yaw: 0, pitch: 0 };
|
let fallbackCameraControls: FallbackCameraControls | undefined;
|
||||||
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));
|
|
||||||
|
|
||||||
// Panel fade animation variables
|
// Panel fade animation variables
|
||||||
let panelOpacity = 0;
|
let panelOpacity = 0;
|
||||||
@@ -116,17 +67,6 @@ let lastFadeTimestamp = 0;
|
|||||||
const FADE_DURATION_MS = 200;
|
const FADE_DURATION_MS = 200;
|
||||||
const AUTO_HIDE_DELAY_MS = 10000;
|
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);
|
injectPlayerStyles(_playerBase);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -225,6 +165,12 @@ function createVideoTexture() {
|
|||||||
return videoTexture;
|
return videoTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVideoTitle() {
|
||||||
|
return videoElement.getAttribute('title') ||
|
||||||
|
videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
|
||||||
|
"Video Title";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
try {
|
try {
|
||||||
@@ -317,6 +263,11 @@ function init() {
|
|||||||
camera2D = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
camera2D = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
camera2D.position.set(0, 1.6, 0.1);
|
camera2D.position.set(0, 1.6, 0.1);
|
||||||
camera2D.rotation.set(0, 0, 0);
|
camera2D.rotation.set(0, 0, 0);
|
||||||
|
fallbackCameraControls = new FallbackCameraControls(camera2D, {
|
||||||
|
hideControls: hide2DControlPanel,
|
||||||
|
isEnabled: () => is2DMode,
|
||||||
|
showControls: show2DControlPanel
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
|
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
|
||||||
renderer = null;
|
renderer = null;
|
||||||
@@ -324,170 +275,10 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try { // Phase 2: VR Control Panel UI
|
try { // Phase 2: VR Control Panel UI
|
||||||
vrControlPanel = new THREE.Group();
|
vrPanel = createVrControlPanel(scene, getVideoTitle());
|
||||||
vrControlPanel.position.set(0, 0.5, -1.8);
|
vrControlPanel = vrPanel.group;
|
||||||
vrControlPanel.rotation.x = 0;
|
uiElements.push(...vrPanel.interactables);
|
||||||
scene.add(vrControlPanel);
|
|
||||||
|
|
||||||
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;
|
panelOpacity = 0;
|
||||||
panelTargetOpacity = 0;
|
panelTargetOpacity = 0;
|
||||||
|
|
||||||
@@ -561,40 +352,24 @@ function init() {
|
|||||||
|
|
||||||
|
|
||||||
function updateVRPlayPauseButtonIcon() {
|
function updateVRPlayPauseButtonIcon() {
|
||||||
if (!vrPlayPauseButtonContext || !vrPlayPauseButtonTexture || !video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ctx = vrPlayPauseButtonContext;
|
updateVrPlayPauseButtonIcon(vrPanel, video.paused || video.ended);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVRVolumeButtonIcon() {
|
function updateVRVolumeButtonIcon() {
|
||||||
if (!vrVolumeButtonContext || !vrVolumeButtonTexture || !video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ctx = vrVolumeButtonContext;
|
updateVrVolumeButtonIcon(vrPanel, video.muted || video.volume === 0);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSeekBarAppearance() {
|
function updateSeekBarAppearance() {
|
||||||
if (video && isFinite(video.duration) && video.duration > 0 && seekBarProgressMesh) {
|
const progress = video && isFinite(video.duration) && video.duration > 0
|
||||||
const progress = video.currentTime / video.duration;
|
? video.currentTime / video.duration
|
||||||
seekBarProgressMesh.scale.x = Math.max(0.0001, progress);
|
: null;
|
||||||
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2 + (WORLD_SEEK_BAR_WIDTH * progress) / 2;
|
updateVrSeekBarAppearance(vrPanel, progress);
|
||||||
} else if (seekBarProgressMesh) {
|
|
||||||
seekBarProgressMesh.scale.x = 0.0001;
|
|
||||||
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function animatePanelFade(timestamp) {
|
function animatePanelFade(timestamp) {
|
||||||
@@ -623,11 +398,7 @@ function animatePanelFade(timestamp) {
|
|||||||
isPanelFading = false;
|
isPanelFading = false;
|
||||||
}
|
}
|
||||||
if (opacityChanged) {
|
if (opacityChanged) {
|
||||||
vrControlPanel.children.forEach(child => {
|
setVrPanelOpacity(vrPanel, panelOpacity);
|
||||||
if (child.material && child.material.hasOwnProperty('opacity')) {
|
|
||||||
child.material.opacity = panelOpacity;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (isPanelFading) requestAnimationFrame(animatePanelFade);
|
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
|
// 2D Render Loop
|
||||||
function render2D() {
|
function render2D() {
|
||||||
if (!is2DMode) return;
|
if (!is2DMode) return;
|
||||||
|
|
||||||
if (projectionMode === 'vr180') {
|
if (projectionMode === 'vr180') {
|
||||||
updateCameraRotation();
|
fallbackCameraControls?.updateCameraRotation();
|
||||||
} else if (camera2D) {
|
} else if (camera2D) {
|
||||||
camera2D.rotation.set(0, 0, 0);
|
camera2D.rotation.set(0, 0, 0);
|
||||||
}
|
}
|
||||||
@@ -850,10 +520,7 @@ function init2DControlPanel() {
|
|||||||
|
|
||||||
// Set initial video title
|
// Set initial video title
|
||||||
if (videoTitle && video) {
|
if (videoTitle && video) {
|
||||||
const title = video.getAttribute('title') ||
|
videoTitle.textContent = getVideoTitle();
|
||||||
video.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
|
|
||||||
"Demo Video";
|
|
||||||
videoTitle.textContent = title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners for 2D controls
|
// Add event listeners for 2D controls
|
||||||
@@ -926,30 +593,6 @@ function hide2DControlPanel() {
|
|||||||
isControlPanelVisible = false;
|
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() {
|
function update2DControlPanel() {
|
||||||
if (!is2DMode || !video) return;
|
if (!is2DMode || !video) return;
|
||||||
|
|
||||||
@@ -1062,20 +705,6 @@ function position2DControlPanel() {
|
|||||||
controlPanel.style.zIndex = '1000'; // Ensure it's above the canvas
|
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() {
|
function hidePlayButton() {
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
playBtn.classList.add('hidden');
|
playBtn.classList.add('hidden');
|
||||||
@@ -1140,9 +769,7 @@ function resetToOriginalState() {
|
|||||||
hide2DControlPanel();
|
hide2DControlPanel();
|
||||||
|
|
||||||
// Reset camera rotation
|
// Reset camera rotation
|
||||||
cameraRotation = { yaw: 0, pitch: 0 };
|
fallbackCameraControls?.reset();
|
||||||
cameraVelocity = { yaw: 0, pitch: 0 };
|
|
||||||
isDragging = false;
|
|
||||||
positionPlaneForPresentation(false);
|
positionPlaneForPresentation(false);
|
||||||
|
|
||||||
// Hide WebGL canvas and show video element
|
// Hide WebGL canvas and show video element
|
||||||
@@ -1212,9 +839,7 @@ function onSelectStartVR(event) {
|
|||||||
showPanel();
|
showPanel();
|
||||||
} else if (firstIntersected.name === "seekBarHitArea" && video && isFinite(video.duration)) {
|
} else if (firstIntersected.name === "seekBarHitArea" && video && isFinite(video.duration)) {
|
||||||
showPanel();
|
showPanel();
|
||||||
const localPoint = seekBarTrackMesh.worldToLocal(intersectionPoint.clone());
|
const newTime = getSeekProgressFromIntersection(vrPanel, intersectionPoint) * video.duration;
|
||||||
const normalizedPosition = (localPoint.x + WORLD_SEEK_BAR_WIDTH / 2) / WORLD_SEEK_BAR_WIDTH;
|
|
||||||
const newTime = Math.max(0, Math.min(1, normalizedPosition)) * video.duration;
|
|
||||||
video.currentTime = newTime;
|
video.currentTime = newTime;
|
||||||
updateSeekBarAppearance();
|
updateSeekBarAppearance();
|
||||||
} else if (firstIntersected.name === "vrControlPanelBackground" || firstIntersected === activeContentMesh) {
|
} else if (firstIntersected.name === "vrControlPanelBackground" || firstIntersected === activeContentMesh) {
|
||||||
@@ -1322,39 +947,13 @@ function start2DMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function add2DEventListeners() {
|
function add2DEventListeners() {
|
||||||
// Canvas-specific mouse movement for showing controls
|
fallbackCameraControls?.addEventListeners(renderer.domElement, projectionMode);
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove2DEventListeners() {
|
function remove2DEventListeners() {
|
||||||
if (!renderer || !renderer.domElement) return;
|
if (!renderer || !renderer.domElement) return;
|
||||||
|
|
||||||
renderer.domElement.removeEventListener('mousemove', onCanvasMouseMove);
|
fallbackCameraControls?.removeEventListeners(renderer.domElement);
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Fullscreen events
|
// Fullscreen events
|
||||||
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
||||||
@@ -1386,12 +985,7 @@ async function actualSessionToggle() {
|
|||||||
clearTimeout(panelHideTimeout);
|
clearTimeout(panelHideTimeout);
|
||||||
panelTargetOpacity = 0;
|
panelTargetOpacity = 0;
|
||||||
panelOpacity = 0;
|
panelOpacity = 0;
|
||||||
vrControlPanel.children.forEach(child => {
|
hideVrPanelImmediately(vrPanel);
|
||||||
if (child.material && child.material.hasOwnProperty('opacity')) {
|
|
||||||
child.material.opacity = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
vrControlPanel.visible = false;
|
|
||||||
isPanelFading = false;
|
isPanelFading = false;
|
||||||
}
|
}
|
||||||
sessionToClose.end().catch(err => {
|
sessionToClose.end().catch(err => {
|
||||||
@@ -1440,14 +1034,9 @@ async function actualSessionToggle() {
|
|||||||
updateVRPlayPauseButtonIcon();
|
updateVRPlayPauseButtonIcon();
|
||||||
updateVRVolumeButtonIcon();
|
updateVRVolumeButtonIcon();
|
||||||
if (vrControlPanel) {
|
if (vrControlPanel) {
|
||||||
vrControlPanel.visible = false;
|
|
||||||
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
||||||
clearTimeout(panelHideTimeout);
|
clearTimeout(panelHideTimeout);
|
||||||
vrControlPanel.children.forEach(child => {
|
hideVrPanelImmediately(vrPanel);
|
||||||
if (child.material && child.material.hasOwnProperty('opacity')) {
|
|
||||||
child.material.opacity = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await renderer.xr.setSession(xrSession);
|
await renderer.xr.setSession(xrSession);
|
||||||
@@ -1465,8 +1054,9 @@ async function actualSessionToggle() {
|
|||||||
if (sphereMaterial) { sphereMaterial.map = null; sphereMaterial.needsUpdate = true; }
|
if (sphereMaterial) { sphereMaterial.map = null; sphereMaterial.needsUpdate = true; }
|
||||||
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
|
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
|
||||||
if (vrControlPanel) {
|
if (vrControlPanel) {
|
||||||
vrControlPanel.visible = false; panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
||||||
clearTimeout(panelHideTimeout);
|
clearTimeout(panelHideTimeout);
|
||||||
|
hideVrPanelImmediately(vrPanel);
|
||||||
}
|
}
|
||||||
if (xrSession) {
|
if (xrSession) {
|
||||||
xrSession.removeEventListener('end', onVRSessionEnd);
|
xrSession.removeEventListener('end', onVRSessionEnd);
|
||||||
@@ -1517,12 +1107,7 @@ function onVRSessionEnd(event) {
|
|||||||
clearTimeout(panelHideTimeout);
|
clearTimeout(panelHideTimeout);
|
||||||
isPanelFading = false;
|
isPanelFading = false;
|
||||||
panelOpacity = 0; panelTargetOpacity = 0;
|
panelOpacity = 0; panelTargetOpacity = 0;
|
||||||
vrControlPanel.children.forEach(child => {
|
hideVrPanelImmediately(vrPanel);
|
||||||
if (child.material && child.material.hasOwnProperty('opacity')) {
|
|
||||||
child.material.opacity = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
vrControlPanel.visible = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endedSession && typeof endedSession.removeEventListener === 'function') {
|
if (endedSession && typeof endedSession.removeEventListener === 'function') {
|
||||||
|
|||||||
Reference in New Issue
Block a user