forked from EXT/VR180-Web-Player
Further refactor
This commit is contained in:
69
src/vr180player/content-scene.ts
Normal file
69
src/vr180player/content-scene.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
||||||
|
import {
|
||||||
|
PLANE_DISTANCE,
|
||||||
|
PLANE_HEIGHT,
|
||||||
|
PLANE_WIDTH,
|
||||||
|
type ProjectionMode
|
||||||
|
} from './config.js';
|
||||||
|
|
||||||
|
type ContentBeforeRender = (
|
||||||
|
renderer: any,
|
||||||
|
scene: any,
|
||||||
|
activeCamera: any,
|
||||||
|
geometry: any,
|
||||||
|
material: any,
|
||||||
|
group: any
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type ContentScene = {
|
||||||
|
activeContentMesh: any;
|
||||||
|
fallbackCamera: any;
|
||||||
|
material: any;
|
||||||
|
planeMesh: any;
|
||||||
|
vr180Mesh: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createContentScene(
|
||||||
|
scene: any,
|
||||||
|
projectionMode: ProjectionMode,
|
||||||
|
onBeforeRender: ContentBeforeRender
|
||||||
|
): ContentScene {
|
||||||
|
const sphereGeometry = new THREE.SphereGeometry(
|
||||||
|
500,
|
||||||
|
64,
|
||||||
|
32,
|
||||||
|
-Math.PI / 2,
|
||||||
|
Math.PI,
|
||||||
|
0,
|
||||||
|
Math.PI
|
||||||
|
);
|
||||||
|
sphereGeometry.scale(-1, 1, 1);
|
||||||
|
|
||||||
|
const material = new THREE.MeshBasicMaterial({ map: null });
|
||||||
|
const vr180Mesh = new THREE.Mesh(sphereGeometry, material);
|
||||||
|
vr180Mesh.name = 'vr180Mesh';
|
||||||
|
vr180Mesh.rotation.y = Math.PI / 2;
|
||||||
|
vr180Mesh.visible = false;
|
||||||
|
vr180Mesh.onBeforeRender = onBeforeRender;
|
||||||
|
scene.add(vr180Mesh);
|
||||||
|
|
||||||
|
const planeGeometry = new THREE.PlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT);
|
||||||
|
const planeMesh = new THREE.Mesh(planeGeometry, material);
|
||||||
|
planeMesh.name = 'vrSbsPlaneMesh';
|
||||||
|
planeMesh.position.set(0, 1.6, -PLANE_DISTANCE);
|
||||||
|
planeMesh.visible = false;
|
||||||
|
planeMesh.onBeforeRender = onBeforeRender;
|
||||||
|
scene.add(planeMesh);
|
||||||
|
|
||||||
|
const fallbackCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
|
fallbackCamera.position.set(0, 1.6, 0.1);
|
||||||
|
fallbackCamera.rotation.set(0, 0, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeContentMesh: projectionMode === 'plane' ? planeMesh : vr180Mesh,
|
||||||
|
fallbackCamera,
|
||||||
|
material,
|
||||||
|
planeMesh,
|
||||||
|
vr180Mesh
|
||||||
|
};
|
||||||
|
}
|
||||||
200
src/vr180player/two-d-control-panel.ts
Normal file
200
src/vr180player/two-d-control-panel.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import { setLucideIcon } from './icons.js';
|
||||||
|
import { formatTime } from './time.js';
|
||||||
|
|
||||||
|
type TwoDControlPanelCallbacks = {
|
||||||
|
onForward: () => void;
|
||||||
|
onMute: () => void;
|
||||||
|
onPlayPause: () => void;
|
||||||
|
onRewind: () => void;
|
||||||
|
onSeek: (progress: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TwoDControlPanelOptions = {
|
||||||
|
callbacks: TwoDControlPanelCallbacks;
|
||||||
|
fullscreenTarget: HTMLElement;
|
||||||
|
getIsActive: () => boolean;
|
||||||
|
playerContainer: HTMLElement;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTROL_PANEL_HIDE_DELAY = 3000;
|
||||||
|
|
||||||
|
export class TwoDControlPanel {
|
||||||
|
private readonly callbacks: TwoDControlPanelCallbacks;
|
||||||
|
private readonly fullscreenTarget: HTMLElement;
|
||||||
|
private readonly getIsActive: () => boolean;
|
||||||
|
private readonly playerContainer: HTMLElement;
|
||||||
|
private controlPanel: HTMLElement | null;
|
||||||
|
private currentTimeDisplay: HTMLElement | null;
|
||||||
|
private hideTimeout: number | undefined;
|
||||||
|
private playedBar: HTMLElement | null;
|
||||||
|
private progressBar: HTMLElement | null;
|
||||||
|
private totalTimeDisplay: HTMLElement | null;
|
||||||
|
private playButton: HTMLButtonElement | null;
|
||||||
|
private muteButton: HTMLButtonElement | null;
|
||||||
|
|
||||||
|
constructor({ callbacks, fullscreenTarget, getIsActive, playerContainer, title }: TwoDControlPanelOptions) {
|
||||||
|
this.callbacks = callbacks;
|
||||||
|
this.fullscreenTarget = fullscreenTarget;
|
||||||
|
this.getIsActive = getIsActive;
|
||||||
|
this.playerContainer = playerContainer;
|
||||||
|
|
||||||
|
this.controlPanel = playerContainer.querySelector('.vrwp-panel');
|
||||||
|
const videoTitle = playerContainer.querySelector<HTMLElement>('.vrwp-video-title');
|
||||||
|
this.currentTimeDisplay = playerContainer.querySelector('.vrwp-current-time');
|
||||||
|
this.totalTimeDisplay = playerContainer.querySelector('.vrwp-total-time');
|
||||||
|
this.progressBar = playerContainer.querySelector('.vrwp-bar');
|
||||||
|
this.playedBar = playerContainer.querySelector('.vrwp-played');
|
||||||
|
this.playButton = playerContainer.querySelector('.vrwp-play-toggle');
|
||||||
|
this.muteButton = playerContainer.querySelector('.vrwp-mute');
|
||||||
|
|
||||||
|
if (!this.controlPanel) {
|
||||||
|
console.error('VR_WEB_PLAYER_DOM: 2D control panel was not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoTitle) {
|
||||||
|
videoTitle.textContent = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bindControls(playerContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
show(): void {
|
||||||
|
if (!this.getIsActive() || !this.controlPanel) return;
|
||||||
|
|
||||||
|
this.clearHideTimeout();
|
||||||
|
this.controlPanel.classList.add('visible');
|
||||||
|
this.hideTimeout = window.setTimeout(() => this.hide(), CONTROL_PANEL_HIDE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
showPersistent(): void {
|
||||||
|
if (!this.getIsActive() || !this.controlPanel) return;
|
||||||
|
|
||||||
|
this.clearHideTimeout();
|
||||||
|
this.controlPanel.classList.add('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(): void {
|
||||||
|
if (!this.controlPanel) return;
|
||||||
|
|
||||||
|
this.clearHideTimeout();
|
||||||
|
this.controlPanel.classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
position(canvas: HTMLElement): void {
|
||||||
|
if (!this.getIsActive() || !this.controlPanel) return;
|
||||||
|
|
||||||
|
const canvasRect = canvas.getBoundingClientRect();
|
||||||
|
const containerRect = this.playerContainer.getBoundingClientRect();
|
||||||
|
const bottomOffset = canvasRect.height * 0.1;
|
||||||
|
const panelHeight = this.controlPanel.offsetHeight;
|
||||||
|
const topPosition = (canvasRect.bottom - containerRect.top) - bottomOffset - panelHeight;
|
||||||
|
|
||||||
|
this.controlPanel.style.position = 'absolute';
|
||||||
|
this.controlPanel.style.top = `${topPosition}px`;
|
||||||
|
this.controlPanel.style.bottom = 'auto';
|
||||||
|
this.controlPanel.style.left = '50%';
|
||||||
|
this.controlPanel.style.transform = 'translateX(-50%)';
|
||||||
|
this.controlPanel.style.zIndex = '1000';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMuteButton(isMuted: boolean): void {
|
||||||
|
if (!this.getIsActive() || !this.muteButton) return;
|
||||||
|
|
||||||
|
if (isMuted) {
|
||||||
|
this.muteButton.classList.remove('muted');
|
||||||
|
this.muteButton.classList.add('unmuted');
|
||||||
|
setLucideIcon(this.muteButton, 'volume-x');
|
||||||
|
} else {
|
||||||
|
this.muteButton.classList.remove('unmuted');
|
||||||
|
this.muteButton.classList.add('muted');
|
||||||
|
setLucideIcon(this.muteButton, 'volume-2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlaybackButton(isPausedOrEnded: boolean): void {
|
||||||
|
if (!this.getIsActive() || !this.playButton) return;
|
||||||
|
|
||||||
|
if (isPausedOrEnded) {
|
||||||
|
this.playButton.classList.remove('playing');
|
||||||
|
this.playButton.classList.add('paused');
|
||||||
|
setLucideIcon(this.playButton, 'play');
|
||||||
|
} else {
|
||||||
|
this.playButton.classList.remove('paused');
|
||||||
|
this.playButton.classList.add('playing');
|
||||||
|
setLucideIcon(this.playButton, 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTime(currentTime: number, duration: number): void {
|
||||||
|
if (!this.getIsActive()) return;
|
||||||
|
|
||||||
|
if (this.currentTimeDisplay) {
|
||||||
|
this.currentTimeDisplay.textContent = formatTime(currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.totalTimeDisplay && isFinite(duration)) {
|
||||||
|
this.totalTimeDisplay.textContent = formatTime(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.playedBar && isFinite(duration) && duration > 0) {
|
||||||
|
const progress = (currentTime / duration) * 100;
|
||||||
|
this.playedBar.style.width = `${progress}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindControls(playerContainer: HTMLElement): void {
|
||||||
|
playerContainer.querySelector('.vrwp-fullscreen')?.addEventListener('click', () => {
|
||||||
|
this.toggleFullscreen();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerContainer.querySelector('.vrwp-back')?.addEventListener('click', () => {
|
||||||
|
this.callbacks.onRewind();
|
||||||
|
this.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.playButton?.addEventListener('click', () => {
|
||||||
|
this.callbacks.onPlayPause();
|
||||||
|
this.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerContainer.querySelector('.vrwp-forward')?.addEventListener('click', () => {
|
||||||
|
this.callbacks.onForward();
|
||||||
|
this.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.muteButton?.addEventListener('click', () => {
|
||||||
|
this.callbacks.onMute();
|
||||||
|
this.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.progressBar?.addEventListener('click', (event) => {
|
||||||
|
const rect = this.progressBar?.getBoundingClientRect();
|
||||||
|
if (rect && rect.width > 0) {
|
||||||
|
this.callbacks.onSeek((event.clientX - rect.left) / rect.width);
|
||||||
|
}
|
||||||
|
this.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearHideTimeout(): void {
|
||||||
|
if (this.hideTimeout !== undefined) {
|
||||||
|
clearTimeout(this.hideTimeout);
|
||||||
|
this.hideTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleFullscreen(): void {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
this.fullscreenTarget.requestFullscreen().catch((err) => {
|
||||||
|
console.error('Error attempting to enable fullscreen:', err);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.exitFullscreen().catch((err) => {
|
||||||
|
console.error('Error attempting to exit fullscreen:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/vr180player/video-events.ts
Normal file
54
src/vr180player/video-events.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
type VideoEventCallbacks = {
|
||||||
|
onEnded: () => void;
|
||||||
|
onPlaybackStateChange: () => void;
|
||||||
|
onTimelineChange: () => void;
|
||||||
|
onVolumeChange: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BindVideoEventsOptions = VideoEventCallbacks & {
|
||||||
|
playButton: HTMLButtonElement | undefined;
|
||||||
|
video: HTMLVideoElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function bindVideoEvents({
|
||||||
|
onEnded,
|
||||||
|
onPlaybackStateChange,
|
||||||
|
onTimelineChange,
|
||||||
|
onVolumeChange,
|
||||||
|
playButton,
|
||||||
|
video
|
||||||
|
}: BindVideoEventsOptions): void {
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
if (isFinite(video.duration) && playButton) {
|
||||||
|
playButton.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTimelineChange();
|
||||||
|
onPlaybackStateChange();
|
||||||
|
onVolumeChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
video.oncanplaythrough = () => {
|
||||||
|
if (playButton && video.readyState >= video.HAVE_FUTURE_DATA) {
|
||||||
|
playButton.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.ontimeupdate = () => {
|
||||||
|
if (isFinite(video.duration)) {
|
||||||
|
onTimelineChange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.onplaying = onPlaybackStateChange;
|
||||||
|
video.onpause = onPlaybackStateChange;
|
||||||
|
video.onerror = (event) => {
|
||||||
|
const videoError = video.error;
|
||||||
|
const errorDetail = videoError ? `Code: ${videoError.code}, Message: ${videoError.message}` : 'Unknown error';
|
||||||
|
console.error('VIDEO_ERROR_EVENT:', event, 'Details:', errorDetail);
|
||||||
|
if (playButton) playButton.disabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('ended', onEnded);
|
||||||
|
video.addEventListener('volumechange', onVolumeChange);
|
||||||
|
}
|
||||||
109
src/vr180player/vr-controller-interactions.ts
Normal file
109
src/vr180player/vr-controller-interactions.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
||||||
|
import {
|
||||||
|
getSeekProgressFromIntersection,
|
||||||
|
type VrControlPanel
|
||||||
|
} from './vr-control-panel.js';
|
||||||
|
|
||||||
|
type VrControllerSelectionOptions = {
|
||||||
|
exitVr: () => void;
|
||||||
|
forward: () => void;
|
||||||
|
hidePanel: () => void;
|
||||||
|
isPanelVisible: () => boolean;
|
||||||
|
raycaster: any;
|
||||||
|
rewind: () => void;
|
||||||
|
seek: (progress: number) => void;
|
||||||
|
showPanel: () => void;
|
||||||
|
toggleMute: () => void;
|
||||||
|
togglePlayPause: () => void;
|
||||||
|
uiElements: any[];
|
||||||
|
vrPanel: VrControlPanel | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tempMatrix = new THREE.Matrix4();
|
||||||
|
|
||||||
|
export function createVrController(scene: any, renderer: any, onSelectStart: (event: any) => void): {
|
||||||
|
controller: any;
|
||||||
|
raycaster: any;
|
||||||
|
} {
|
||||||
|
const controller = renderer.xr.getController(0);
|
||||||
|
controller.addEventListener('selectstart', onSelectStart);
|
||||||
|
|
||||||
|
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 });
|
||||||
|
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
|
||||||
|
new THREE.Vector3(0, 0, 0),
|
||||||
|
new THREE.Vector3(0, 0, -5)
|
||||||
|
]);
|
||||||
|
controller.add(new THREE.Line(lineGeometry, lineMaterial));
|
||||||
|
scene.add(controller);
|
||||||
|
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
raycaster.near = 0.1;
|
||||||
|
raycaster.far = 5;
|
||||||
|
|
||||||
|
return { controller, raycaster };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleVrControllerSelect(event: any, options: VrControllerSelectionOptions): void {
|
||||||
|
const controller = event.target;
|
||||||
|
if (!options.raycaster) return;
|
||||||
|
|
||||||
|
controller.updateMatrixWorld();
|
||||||
|
tempMatrix.identity().extractRotation(controller.matrixWorld);
|
||||||
|
options.raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
|
||||||
|
options.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
|
||||||
|
|
||||||
|
const directIntersects = options.raycaster.intersectObjects(options.uiElements, true);
|
||||||
|
if (directIntersects.length === 0) {
|
||||||
|
togglePanel(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstIntersected = directIntersects[0].object;
|
||||||
|
const intersectionPoint = directIntersects[0].point;
|
||||||
|
|
||||||
|
if (firstIntersected.name === 'vrPlayPauseButton') {
|
||||||
|
options.togglePlayPause();
|
||||||
|
options.showPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstIntersected.name === 'vrRewindButton') {
|
||||||
|
options.rewind();
|
||||||
|
options.showPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstIntersected.name === 'vrForwardButton') {
|
||||||
|
options.forward();
|
||||||
|
options.showPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstIntersected.name === 'vrExitButton') {
|
||||||
|
options.exitVr();
|
||||||
|
options.showPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstIntersected.name === 'vrVolumeButton') {
|
||||||
|
options.toggleMute();
|
||||||
|
options.showPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstIntersected.name === 'seekBarHitArea') {
|
||||||
|
options.showPanel();
|
||||||
|
options.seek(getSeekProgressFromIntersection(options.vrPanel, intersectionPoint));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePanel(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePanel(options: VrControllerSelectionOptions): void {
|
||||||
|
if (options.isPanelVisible()) {
|
||||||
|
options.hidePanel();
|
||||||
|
} else {
|
||||||
|
options.showPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/vr180player/vr-panel-visibility.ts
Normal file
111
src/vr180player/vr-panel-visibility.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
hideVrPanelImmediately,
|
||||||
|
setVrPanelOpacity,
|
||||||
|
type VrControlPanel
|
||||||
|
} from './vr-control-panel.js';
|
||||||
|
|
||||||
|
const FADE_DURATION_MS = 200;
|
||||||
|
const AUTO_HIDE_DELAY_MS = 10000;
|
||||||
|
|
||||||
|
export class VrPanelVisibility {
|
||||||
|
private hideTimeout: number | undefined;
|
||||||
|
private lastFadeTimestamp = 0;
|
||||||
|
private opacity = 0;
|
||||||
|
private panel: VrControlPanel | undefined;
|
||||||
|
private targetOpacity = 0;
|
||||||
|
private fading = false;
|
||||||
|
|
||||||
|
get isFading(): boolean {
|
||||||
|
return this.fading;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVisible(): boolean {
|
||||||
|
return !!(this.panel?.group.visible && this.opacity > 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanel(panel: VrControlPanel): void {
|
||||||
|
this.panel = panel;
|
||||||
|
this.hideImmediately();
|
||||||
|
}
|
||||||
|
|
||||||
|
show(): void {
|
||||||
|
if (this.panel) this.panel.group.visible = true;
|
||||||
|
this.clearHideTimeout();
|
||||||
|
|
||||||
|
if (this.targetOpacity !== 1.0 || this.opacity < 1.0) {
|
||||||
|
this.targetOpacity = 1.0;
|
||||||
|
this.startFade();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideTimeout = window.setTimeout(() => this.hide(), AUTO_HIDE_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(): void {
|
||||||
|
this.clearHideTimeout();
|
||||||
|
|
||||||
|
if (this.targetOpacity !== 0.0 || this.opacity > 0.0) {
|
||||||
|
this.targetOpacity = 0.0;
|
||||||
|
this.startFade();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideImmediately(): void {
|
||||||
|
this.clearHideTimeout();
|
||||||
|
this.targetOpacity = 0;
|
||||||
|
this.opacity = 0;
|
||||||
|
this.fading = false;
|
||||||
|
hideVrPanelImmediately(this.panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFade(timestamp: number): void {
|
||||||
|
if (!this.panel) return;
|
||||||
|
if (this.lastFadeTimestamp === 0) this.lastFadeTimestamp = timestamp;
|
||||||
|
|
||||||
|
const deltaTime = (timestamp - this.lastFadeTimestamp) / 1000;
|
||||||
|
this.lastFadeTimestamp = timestamp;
|
||||||
|
const fadeSpeed = 1 / (FADE_DURATION_MS / 1000);
|
||||||
|
let opacityChanged = false;
|
||||||
|
|
||||||
|
if (this.opacity < this.targetOpacity) {
|
||||||
|
this.opacity += fadeSpeed * deltaTime;
|
||||||
|
if (this.opacity >= this.targetOpacity) {
|
||||||
|
this.opacity = this.targetOpacity;
|
||||||
|
this.fading = false;
|
||||||
|
}
|
||||||
|
opacityChanged = true;
|
||||||
|
} else if (this.opacity > this.targetOpacity) {
|
||||||
|
this.opacity -= fadeSpeed * deltaTime;
|
||||||
|
if (this.opacity <= this.targetOpacity) {
|
||||||
|
this.opacity = this.targetOpacity;
|
||||||
|
this.fading = false;
|
||||||
|
if (this.opacity === 0) this.panel.group.visible = false;
|
||||||
|
}
|
||||||
|
opacityChanged = true;
|
||||||
|
} else {
|
||||||
|
this.fading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opacityChanged) {
|
||||||
|
setVrPanelOpacity(this.panel, this.opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fading) {
|
||||||
|
requestAnimationFrame((nextTimestamp) => this.updateFade(nextTimestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearHideTimeout(): void {
|
||||||
|
if (this.hideTimeout !== undefined) {
|
||||||
|
clearTimeout(this.hideTimeout);
|
||||||
|
this.hideTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startFade(): void {
|
||||||
|
if (!this.fading) {
|
||||||
|
this.fading = true;
|
||||||
|
this.lastFadeTimestamp = 0;
|
||||||
|
requestAnimationFrame((timestamp) => this.updateFade(timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,11 @@ import {
|
|||||||
DEFAULT_PROJECTION,
|
DEFAULT_PROJECTION,
|
||||||
PLANE_2D_DISTANCE,
|
PLANE_2D_DISTANCE,
|
||||||
PLANE_DISTANCE,
|
PLANE_DISTANCE,
|
||||||
PLANE_HEIGHT,
|
|
||||||
PLANE_WIDTH,
|
|
||||||
PLAYER_SELECTOR,
|
PLAYER_SELECTOR,
|
||||||
type ProjectionMode,
|
type ProjectionMode,
|
||||||
VALID_PROJECTIONS
|
VALID_PROJECTIONS
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
import { createContentScene } from './content-scene.js';
|
||||||
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom.js';
|
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom.js';
|
||||||
import {
|
import {
|
||||||
applySbsTextureWindow as applySbsTextureWindowCore,
|
applySbsTextureWindow as applySbsTextureWindowCore,
|
||||||
@@ -17,19 +16,21 @@ import {
|
|||||||
showActiveContentMesh as showActiveContentMeshCore
|
showActiveContentMesh as showActiveContentMeshCore
|
||||||
} from './projection.js';
|
} from './projection.js';
|
||||||
import { createVideoTexture as createVideoTextureCore } from './three-utils.js';
|
import { createVideoTexture as createVideoTextureCore } from './three-utils.js';
|
||||||
import { setLucideIcon } from './icons.js';
|
|
||||||
import { FallbackCameraControls } from './fallback-camera-controls.js';
|
import { FallbackCameraControls } from './fallback-camera-controls.js';
|
||||||
import { formatTime } from './time.js';
|
import {
|
||||||
|
createVrController,
|
||||||
|
handleVrControllerSelect
|
||||||
|
} from './vr-controller-interactions.js';
|
||||||
|
import { bindVideoEvents } from './video-events.js';
|
||||||
import {
|
import {
|
||||||
createVrControlPanel,
|
createVrControlPanel,
|
||||||
getSeekProgressFromIntersection,
|
|
||||||
hideVrPanelImmediately,
|
|
||||||
setVrPanelOpacity,
|
|
||||||
type VrControlPanel,
|
type VrControlPanel,
|
||||||
updateVrPlayPauseButtonIcon,
|
updateVrPlayPauseButtonIcon,
|
||||||
updateVrSeekBarAppearance,
|
updateVrSeekBarAppearance,
|
||||||
updateVrVolumeButtonIcon
|
updateVrVolumeButtonIcon
|
||||||
} from './vr-control-panel.js';
|
} from './vr-control-panel.js';
|
||||||
|
import { VrPanelVisibility } from './vr-panel-visibility.js';
|
||||||
|
import { TwoDControlPanel } from './two-d-control-panel.js';
|
||||||
|
|
||||||
const _playerBase = new URL('.', import.meta.url).href;
|
const _playerBase = new URL('.', import.meta.url).href;
|
||||||
|
|
||||||
@@ -37,36 +38,21 @@ let playerContainer, projectionMode: ProjectionMode = DEFAULT_PROJECTION;
|
|||||||
let scene, camera, renderer, video, videoTexture, sphereMaterial;
|
let scene, camera, renderer, video, videoTexture, sphereMaterial;
|
||||||
let vr180Mesh, planeMesh, activeContentMesh;
|
let vr180Mesh, planeMesh, activeContentMesh;
|
||||||
let xrSession = null;
|
let xrSession = null;
|
||||||
let controller1, raycaster, uiElements = [];
|
let raycaster, uiElements = [];
|
||||||
const tempMatrix = new THREE.Matrix4();
|
|
||||||
let videoElement, playBtn;
|
let videoElement, playBtn;
|
||||||
let frameCounter = 0;
|
let frameCounter = 0;
|
||||||
|
|
||||||
// 2D Control Panel Elements
|
|
||||||
let controlPanel, videoTitle, currentTimeDisplay, totalTimeDisplay, progressBar, playedBar;
|
|
||||||
let fullscreenBtn, backBtn, play2Btn, forwardBtn, muteBtn;
|
|
||||||
let controlPanelTimeout;
|
|
||||||
let isControlPanelVisible = false;
|
|
||||||
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;
|
let vrPanel: VrControlPanel | undefined;
|
||||||
|
let twoDControls: TwoDControlPanel | undefined;
|
||||||
|
const vrPanelVisibility = new VrPanelVisibility();
|
||||||
|
|
||||||
// 2D Camera Controls
|
// 2D Camera Controls
|
||||||
let camera2D;
|
let camera2D;
|
||||||
let fallbackCameraControls: FallbackCameraControls | undefined;
|
let fallbackCameraControls: FallbackCameraControls | undefined;
|
||||||
|
|
||||||
// Panel fade animation variables
|
|
||||||
let panelOpacity = 0;
|
|
||||||
let panelTargetOpacity = 0;
|
|
||||||
let isPanelFading = false;
|
|
||||||
let panelHideTimeout = null;
|
|
||||||
let lastFadeTimestamp = 0;
|
|
||||||
const FADE_DURATION_MS = 200;
|
|
||||||
const AUTO_HIDE_DELAY_MS = 10000;
|
|
||||||
|
|
||||||
injectPlayerStyles(_playerBase);
|
injectPlayerStyles(_playerBase);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -221,48 +207,16 @@ function init() {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
video = videoElement;
|
video = videoElement;
|
||||||
|
const contentScene = createContentScene(scene, projectionMode, (renderer, scene, activeCamera, geometry, material, group) => {
|
||||||
const sphereRadius = 500;
|
|
||||||
let thetaStart = 0;
|
|
||||||
let thetaLength = Math.PI;
|
|
||||||
|
|
||||||
const sphereGeometry = new THREE.SphereGeometry(
|
|
||||||
sphereRadius, 64, 32,
|
|
||||||
-Math.PI / 2,
|
|
||||||
Math.PI,
|
|
||||||
thetaStart,
|
|
||||||
thetaLength
|
|
||||||
);
|
|
||||||
sphereGeometry.scale(-1, 1, 1);
|
|
||||||
sphereMaterial = new THREE.MeshBasicMaterial({ map: null });
|
|
||||||
vr180Mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
|
|
||||||
vr180Mesh.name = "vr180Mesh";
|
|
||||||
|
|
||||||
vr180Mesh.rotation.y = Math.PI / 2;
|
|
||||||
scene.add(vr180Mesh);
|
|
||||||
vr180Mesh.visible = false;
|
|
||||||
|
|
||||||
vr180Mesh.onBeforeRender = function (renderer, scene, activeCamera, geometry, material, group) {
|
|
||||||
applySbsTextureWindow(renderer, activeCamera, material);
|
applySbsTextureWindow(renderer, activeCamera, material);
|
||||||
};
|
});
|
||||||
|
sphereMaterial = contentScene.material;
|
||||||
const planeGeometry = new THREE.PlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT);
|
vr180Mesh = contentScene.vr180Mesh;
|
||||||
planeMesh = new THREE.Mesh(planeGeometry, sphereMaterial);
|
planeMesh = contentScene.planeMesh;
|
||||||
planeMesh.name = "vrSbsPlaneMesh";
|
activeContentMesh = contentScene.activeContentMesh;
|
||||||
planeMesh.position.set(0, 1.6, -PLANE_DISTANCE);
|
|
||||||
planeMesh.onBeforeRender = function (renderer, scene, activeCamera, geometry, material, group) {
|
|
||||||
applySbsTextureWindow(renderer, activeCamera, material);
|
|
||||||
};
|
|
||||||
scene.add(planeMesh);
|
|
||||||
planeMesh.visible = false;
|
|
||||||
|
|
||||||
activeContentMesh = projectionMode === 'plane' ? planeMesh : vr180Mesh;
|
|
||||||
uiElements.push(activeContentMesh);
|
uiElements.push(activeContentMesh);
|
||||||
|
|
||||||
// Initialize 2D camera
|
camera2D = contentScene.fallbackCamera;
|
||||||
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, {
|
fallbackCameraControls = new FallbackCameraControls(camera2D, {
|
||||||
hideControls: hide2DControlPanel,
|
hideControls: hide2DControlPanel,
|
||||||
isEnabled: () => is2DMode,
|
isEnabled: () => is2DMode,
|
||||||
@@ -277,19 +231,10 @@ function init() {
|
|||||||
try { // Phase 2: VR Control Panel UI
|
try { // Phase 2: VR Control Panel UI
|
||||||
vrPanel = createVrControlPanel(scene, getVideoTitle());
|
vrPanel = createVrControlPanel(scene, getVideoTitle());
|
||||||
vrControlPanel = vrPanel.group;
|
vrControlPanel = vrPanel.group;
|
||||||
|
vrPanelVisibility.setPanel(vrPanel);
|
||||||
uiElements.push(...vrPanel.interactables);
|
uiElements.push(...vrPanel.interactables);
|
||||||
|
|
||||||
panelOpacity = 0;
|
raycaster = createVrController(scene, renderer, onSelectStartVR).raycaster;
|
||||||
panelTargetOpacity = 0;
|
|
||||||
|
|
||||||
controller1 = renderer.xr.getController(0);
|
|
||||||
controller1.addEventListener('selectstart', onSelectStartVR);
|
|
||||||
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 });
|
|
||||||
const lineGeometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -5)]);
|
|
||||||
controller1.add(new THREE.Line(lineGeometry, lineMaterial));
|
|
||||||
scene.add(controller1);
|
|
||||||
raycaster = new THREE.Raycaster();
|
|
||||||
raycaster.near = 0.1; raycaster.far = 5;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("INIT_ERROR (Phase 2 - VR Controls Setup):", e);
|
console.error("INIT_ERROR (Phase 2 - VR Controls Setup):", e);
|
||||||
}
|
}
|
||||||
@@ -301,46 +246,23 @@ function init() {
|
|||||||
window.addEventListener('resize', onWindowResize);
|
window.addEventListener('resize', onWindowResize);
|
||||||
|
|
||||||
if (video) {
|
if (video) {
|
||||||
video.onloadedmetadata = () => {
|
bindVideoEvents({
|
||||||
if (isFinite(video.duration) && playBtn) {
|
onEnded: onVideoEnded,
|
||||||
// Enable button for both VR and non-VR scenarios when video is ready
|
onPlaybackStateChange: () => {
|
||||||
playBtn.disabled = false;
|
updateVRPlayPauseButtonIcon();
|
||||||
}
|
update2DPlayPauseButton();
|
||||||
updateSeekBarAppearance();
|
},
|
||||||
updateVRPlayPauseButtonIcon();
|
onTimelineChange: () => {
|
||||||
updateVRVolumeButtonIcon();
|
|
||||||
update2DControlPanel();
|
|
||||||
update2DMuteButton();
|
|
||||||
};
|
|
||||||
video.oncanplaythrough = () => {
|
|
||||||
if (playBtn && video.readyState >= video.HAVE_FUTURE_DATA) {
|
|
||||||
// Enable button for both VR and non-VR scenarios when video is ready to play
|
|
||||||
playBtn.disabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
video.ontimeupdate = () => {
|
|
||||||
if (isFinite(video.duration)) {
|
|
||||||
updateSeekBarAppearance();
|
updateSeekBarAppearance();
|
||||||
update2DControlPanel();
|
update2DControlPanel();
|
||||||
}
|
},
|
||||||
};
|
onVolumeChange: () => {
|
||||||
video.onplaying = () => {
|
updateVRVolumeButtonIcon();
|
||||||
updateVRPlayPauseButtonIcon();
|
update2DMuteButton();
|
||||||
update2DPlayPauseButton();
|
},
|
||||||
};
|
playButton: playBtn,
|
||||||
video.onpause = () => {
|
video
|
||||||
updateVRPlayPauseButtonIcon();
|
});
|
||||||
update2DPlayPauseButton();
|
|
||||||
};
|
|
||||||
video.onerror = (e) => {
|
|
||||||
const videoError = video.error;
|
|
||||||
const errorDetail = videoError ? `Code: ${videoError.code}, Message: ${videoError.message}` : 'Unknown error';
|
|
||||||
console.error("VIDEO_ERROR_EVENT:", e, "Details:", errorDetail);
|
|
||||||
if (playBtn) playBtn.disabled = true;
|
|
||||||
};
|
|
||||||
video.addEventListener('ended', onVideoEnded);
|
|
||||||
video.addEventListener('volumechange', updateVRVolumeButtonIcon);
|
|
||||||
video.addEventListener('volumechange', update2DMuteButton);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 2D control panel
|
// Initialize 2D control panel
|
||||||
@@ -373,60 +295,15 @@ function updateSeekBarAppearance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function animatePanelFade(timestamp) {
|
function animatePanelFade(timestamp) {
|
||||||
if (!vrControlPanel) return;
|
vrPanelVisibility.updateFade(timestamp);
|
||||||
if (lastFadeTimestamp === 0) lastFadeTimestamp = timestamp;
|
|
||||||
const deltaTime = (timestamp - lastFadeTimestamp) / 1000;
|
|
||||||
lastFadeTimestamp = timestamp;
|
|
||||||
const FADE_SPEED = 1 / (FADE_DURATION_MS / 1000);
|
|
||||||
let opacityChanged = false;
|
|
||||||
if (panelOpacity < panelTargetOpacity) {
|
|
||||||
panelOpacity += FADE_SPEED * deltaTime;
|
|
||||||
if (panelOpacity >= panelTargetOpacity) {
|
|
||||||
panelOpacity = panelTargetOpacity;
|
|
||||||
isPanelFading = false;
|
|
||||||
}
|
|
||||||
opacityChanged = true;
|
|
||||||
} else if (panelOpacity > panelTargetOpacity) {
|
|
||||||
panelOpacity -= FADE_SPEED * deltaTime;
|
|
||||||
if (panelOpacity <= panelTargetOpacity) {
|
|
||||||
panelOpacity = panelTargetOpacity;
|
|
||||||
isPanelFading = false;
|
|
||||||
if (panelOpacity === 0) vrControlPanel.visible = false;
|
|
||||||
}
|
|
||||||
opacityChanged = true;
|
|
||||||
} else {
|
|
||||||
isPanelFading = false;
|
|
||||||
}
|
|
||||||
if (opacityChanged) {
|
|
||||||
setVrPanelOpacity(vrPanel, panelOpacity);
|
|
||||||
}
|
|
||||||
if (isPanelFading) requestAnimationFrame(animatePanelFade);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPanel() {
|
function showPanel() {
|
||||||
if (vrControlPanel) vrControlPanel.visible = true;
|
vrPanelVisibility.show();
|
||||||
clearTimeout(panelHideTimeout);
|
|
||||||
if (panelTargetOpacity !== 1.0 || panelOpacity < 1.0) {
|
|
||||||
panelTargetOpacity = 1.0;
|
|
||||||
if (!isPanelFading) {
|
|
||||||
isPanelFading = true;
|
|
||||||
lastFadeTimestamp = 0;
|
|
||||||
requestAnimationFrame(animatePanelFade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panelHideTimeout = setTimeout(hidePanel, AUTO_HIDE_DELAY_MS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hidePanel() {
|
function hidePanel() {
|
||||||
clearTimeout(panelHideTimeout);
|
vrPanelVisibility.hide();
|
||||||
if (panelTargetOpacity !== 0.0 || panelOpacity > 0.0) {
|
|
||||||
panelTargetOpacity = 0.0;
|
|
||||||
if (!isPanelFading) {
|
|
||||||
isPanelFading = true;
|
|
||||||
lastFadeTimestamp = 0;
|
|
||||||
requestAnimationFrame(animatePanelFade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowResize() {
|
function onWindowResize() {
|
||||||
@@ -500,209 +377,74 @@ function render2D() {
|
|||||||
|
|
||||||
// 2D Control Panel Functions
|
// 2D Control Panel Functions
|
||||||
function init2DControlPanel() {
|
function init2DControlPanel() {
|
||||||
// Get references to 2D control elements
|
twoDControls = new TwoDControlPanel({
|
||||||
controlPanel = playerContainer.querySelector('.vrwp-panel');
|
callbacks: {
|
||||||
videoTitle = playerContainer.querySelector('.vrwp-video-title');
|
onForward: () => {
|
||||||
currentTimeDisplay = playerContainer.querySelector('.vrwp-current-time');
|
if (video && isFinite(video.duration)) {
|
||||||
totalTimeDisplay = playerContainer.querySelector('.vrwp-total-time');
|
video.currentTime = Math.min(video.duration, video.currentTime + 15);
|
||||||
progressBar = playerContainer.querySelector('.vrwp-bar');
|
}
|
||||||
playedBar = playerContainer.querySelector('.vrwp-played');
|
},
|
||||||
fullscreenBtn = playerContainer.querySelector('.vrwp-fullscreen');
|
onMute: () => {
|
||||||
backBtn = playerContainer.querySelector('.vrwp-back');
|
if (video) {
|
||||||
play2Btn = playerContainer.querySelector('.vrwp-play-toggle');
|
video.muted = !video.muted;
|
||||||
forwardBtn = playerContainer.querySelector('.vrwp-forward');
|
}
|
||||||
muteBtn = playerContainer.querySelector('.vrwp-mute');
|
},
|
||||||
|
onPlayPause: togglePlayPause,
|
||||||
if (!controlPanel) {
|
onRewind: () => {
|
||||||
console.error("2D Control panel not found");
|
if (video) {
|
||||||
return;
|
video.currentTime = Math.max(0, video.currentTime - 15);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Set initial video title
|
onSeek: (progress) => {
|
||||||
if (videoTitle && video) {
|
if (video && isFinite(video.duration)) {
|
||||||
videoTitle.textContent = getVideoTitle();
|
video.currentTime = progress * video.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners for 2D controls
|
|
||||||
if (fullscreenBtn) {
|
|
||||||
fullscreenBtn.addEventListener('click', toggle2DFullscreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backBtn) {
|
|
||||||
backBtn.addEventListener('click', () => {
|
|
||||||
if (video) {
|
|
||||||
video.currentTime = Math.max(0, video.currentTime - 15);
|
|
||||||
}
|
}
|
||||||
show2DControlPanel();
|
},
|
||||||
});
|
fullscreenTarget: playerContainer,
|
||||||
}
|
getIsActive: () => is2DMode,
|
||||||
|
playerContainer,
|
||||||
if (play2Btn) {
|
title: getVideoTitle()
|
||||||
play2Btn.addEventListener('click', () => {
|
});
|
||||||
togglePlayPause();
|
|
||||||
show2DControlPanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forwardBtn) {
|
|
||||||
forwardBtn.addEventListener('click', () => {
|
|
||||||
if (video && isFinite(video.duration)) {
|
|
||||||
video.currentTime = Math.min(video.duration, video.currentTime + 15);
|
|
||||||
}
|
|
||||||
show2DControlPanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (muteBtn) {
|
|
||||||
muteBtn.addEventListener('click', () => {
|
|
||||||
if (video) {
|
|
||||||
video.muted = !video.muted;
|
|
||||||
}
|
|
||||||
show2DControlPanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressBar) {
|
|
||||||
progressBar.addEventListener('click', (e) => {
|
|
||||||
if (video && isFinite(video.duration)) {
|
|
||||||
const rect = progressBar.getBoundingClientRect();
|
|
||||||
const clickX = e.clientX - rect.left;
|
|
||||||
const progress = clickX / rect.width;
|
|
||||||
video.currentTime = progress * video.duration;
|
|
||||||
}
|
|
||||||
show2DControlPanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function show2DControlPanel() {
|
function show2DControlPanel() {
|
||||||
if (!is2DMode || !controlPanel) return;
|
twoDControls?.show();
|
||||||
|
|
||||||
clearTimeout(controlPanelTimeout);
|
|
||||||
controlPanel.classList.add('visible');
|
|
||||||
isControlPanelVisible = true;
|
|
||||||
|
|
||||||
controlPanelTimeout = setTimeout(hide2DControlPanel, CONTROL_PANEL_HIDE_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide2DControlPanel() {
|
function hide2DControlPanel() {
|
||||||
if (!controlPanel) return;
|
twoDControls?.hide();
|
||||||
|
|
||||||
clearTimeout(controlPanelTimeout);
|
|
||||||
controlPanel.classList.remove('visible');
|
|
||||||
isControlPanelVisible = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update2DControlPanel() {
|
function update2DControlPanel() {
|
||||||
if (!is2DMode || !video) return;
|
if (!is2DMode || !video) return;
|
||||||
|
|
||||||
// Update time displays
|
twoDControls?.updateTime(video.currentTime, video.duration);
|
||||||
if (currentTimeDisplay) {
|
|
||||||
currentTimeDisplay.textContent = formatTime(video.currentTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalTimeDisplay && isFinite(video.duration)) {
|
|
||||||
totalTimeDisplay.textContent = formatTime(video.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update progress bar
|
|
||||||
if (playedBar && isFinite(video.duration) && video.duration > 0) {
|
|
||||||
const progress = (video.currentTime / video.duration) * 100;
|
|
||||||
playedBar.style.width = `${progress}%`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update2DPlayPauseButton() {
|
function update2DPlayPauseButton() {
|
||||||
if (!is2DMode || !play2Btn || !video) return;
|
if (!is2DMode || !video) return;
|
||||||
|
|
||||||
if (video.paused || video.ended) {
|
twoDControls?.updatePlaybackButton(video.paused || video.ended);
|
||||||
play2Btn.classList.remove('playing');
|
|
||||||
play2Btn.classList.add('paused');
|
|
||||||
setLucideIcon(play2Btn, 'play');
|
|
||||||
} else {
|
|
||||||
play2Btn.classList.remove('paused');
|
|
||||||
play2Btn.classList.add('playing');
|
|
||||||
setLucideIcon(play2Btn, 'pause');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update2DMuteButton() {
|
function update2DMuteButton() {
|
||||||
if (!is2DMode || !muteBtn || !video) return;
|
if (!is2DMode || !video) return;
|
||||||
|
|
||||||
if (video.muted) {
|
twoDControls?.updateMuteButton(video.muted);
|
||||||
// Video is muted, show unmute icon (user can click to unmute)
|
|
||||||
muteBtn.classList.remove('muted');
|
|
||||||
muteBtn.classList.add('unmuted');
|
|
||||||
setLucideIcon(muteBtn, 'volume-x');
|
|
||||||
} else {
|
|
||||||
// Video is unmuted, show mute icon (user can click to mute)
|
|
||||||
muteBtn.classList.remove('unmuted');
|
|
||||||
muteBtn.classList.add('muted');
|
|
||||||
setLucideIcon(muteBtn, 'volume-2');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle2DFullscreen() {
|
|
||||||
if (!document.fullscreenElement) {
|
|
||||||
// Enter fullscreen
|
|
||||||
const container = playerContainer;
|
|
||||||
if (container && container.requestFullscreen) {
|
|
||||||
container.requestFullscreen().catch(err => {
|
|
||||||
console.error('Error attempting to enable fullscreen:', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Exit fullscreen
|
|
||||||
if (document.exitFullscreen) {
|
|
||||||
document.exitFullscreen().catch(err => {
|
|
||||||
console.error('Error attempting to exit fullscreen:', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle2DVideoEnd() {
|
function handle2DVideoEnd() {
|
||||||
if (!is2DMode || !video) return;
|
if (!is2DMode || !video) return;
|
||||||
|
|
||||||
// Keep video at last frame (don't reset currentTime)
|
twoDControls?.showPersistent();
|
||||||
// Video is already paused by onVideoEnded()
|
|
||||||
|
|
||||||
// Show control panel and keep it visible (no auto-hide timeout)
|
|
||||||
if (controlPanel) {
|
|
||||||
clearTimeout(controlPanelTimeout);
|
|
||||||
controlPanel.classList.add('visible');
|
|
||||||
isControlPanelVisible = true;
|
|
||||||
// Don't set timeout - panel stays visible until user interacts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update play button to show replay state
|
|
||||||
update2DPlayPauseButton();
|
update2DPlayPauseButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function position2DControlPanel() {
|
function position2DControlPanel() {
|
||||||
if (!is2DMode || !controlPanel || !renderer) return;
|
if (!renderer) return;
|
||||||
|
|
||||||
// Get the canvas dimensions and position
|
twoDControls?.position(renderer.domElement);
|
||||||
const canvas = renderer.domElement;
|
|
||||||
const canvasRect = canvas.getBoundingClientRect();
|
|
||||||
const containerRect = playerContainer.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Calculate 10% from the bottom of the canvas
|
|
||||||
const bottomOffset = canvasRect.height * 0.1;
|
|
||||||
|
|
||||||
// Get the panel's height
|
|
||||||
const panelHeight = controlPanel.offsetHeight;
|
|
||||||
|
|
||||||
// Calculate the top position: canvas bottom minus offset minus panel height, relative to container
|
|
||||||
const topPosition = (canvasRect.bottom - containerRect.top) - bottomOffset - panelHeight;
|
|
||||||
|
|
||||||
// Position the panel so its bottom edge is 10% from canvas bottom
|
|
||||||
controlPanel.style.position = 'absolute';
|
|
||||||
controlPanel.style.top = `${topPosition}px`;
|
|
||||||
controlPanel.style.bottom = 'auto'; // Clear any previous bottom positioning
|
|
||||||
controlPanel.style.left = '50%';
|
|
||||||
controlPanel.style.transform = 'translateX(-50%)';
|
|
||||||
controlPanel.style.zIndex = '1000'; // Ensure it's above the canvas
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hidePlayButton() {
|
function hidePlayButton() {
|
||||||
@@ -812,44 +554,39 @@ function onVideoEnded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSelectStartVR(event) {
|
function onSelectStartVR(event) {
|
||||||
const controller = event.target;
|
handleVrControllerSelect(event, {
|
||||||
if (!raycaster) return;
|
exitVr: () => {
|
||||||
controller.updateMatrixWorld();
|
if (xrSession) actualSessionToggle();
|
||||||
tempMatrix.identity().extractRotation(controller.matrixWorld);
|
},
|
||||||
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
|
forward: () => {
|
||||||
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
|
if (video && isFinite(video.duration)) {
|
||||||
const allInteractables = [...uiElements];
|
video.currentTime = Math.min(video.duration, video.currentTime + 15);
|
||||||
const directIntersects = raycaster.intersectObjects(allInteractables, true);
|
updateSeekBarAppearance();
|
||||||
if (directIntersects.length > 0) {
|
}
|
||||||
const firstIntersected = directIntersects[0].object;
|
},
|
||||||
const intersectionPoint = directIntersects[0].point;
|
hidePanel,
|
||||||
if (firstIntersected.name === "vrPlayPauseButton") {
|
isPanelVisible: () => vrPanelVisibility.isVisible,
|
||||||
togglePlayPause(); showPanel();
|
raycaster,
|
||||||
} else if (firstIntersected.name === "vrRewindButton") {
|
rewind: () => {
|
||||||
if (video) { video.currentTime = Math.max(0, video.currentTime - 15); updateSeekBarAppearance(); }
|
if (video) {
|
||||||
showPanel();
|
video.currentTime = Math.max(0, video.currentTime - 15);
|
||||||
} else if (firstIntersected.name === "vrForwardButton") {
|
updateSeekBarAppearance();
|
||||||
if (video && isFinite(video.duration)) { video.currentTime = Math.min(video.duration, video.currentTime + 15); updateSeekBarAppearance(); }
|
}
|
||||||
showPanel();
|
},
|
||||||
} else if (firstIntersected.name === "vrExitButton") {
|
seek: (progress) => {
|
||||||
if (xrSession) actualSessionToggle(); // Should trigger exit
|
if (video && isFinite(video.duration)) {
|
||||||
showPanel(); // Keep panel briefly visible or hide, depending on desired UX
|
video.currentTime = progress * video.duration;
|
||||||
} else if (firstIntersected.name === "vrVolumeButton") {
|
updateSeekBarAppearance();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showPanel,
|
||||||
|
toggleMute: () => {
|
||||||
if (video) video.muted = !video.muted;
|
if (video) video.muted = !video.muted;
|
||||||
showPanel();
|
},
|
||||||
} else if (firstIntersected.name === "seekBarHitArea" && video && isFinite(video.duration)) {
|
togglePlayPause,
|
||||||
showPanel();
|
uiElements,
|
||||||
const newTime = getSeekProgressFromIntersection(vrPanel, intersectionPoint) * video.duration;
|
vrPanel
|
||||||
video.currentTime = newTime;
|
});
|
||||||
updateSeekBarAppearance();
|
|
||||||
} else if (firstIntersected.name === "vrControlPanelBackground" || firstIntersected === activeContentMesh) {
|
|
||||||
if (vrControlPanel && vrControlPanel.visible && panelOpacity > 0.01) hidePanel(); else showPanel();
|
|
||||||
} else {
|
|
||||||
if (vrControlPanel && vrControlPanel.visible && panelOpacity > 0.01) hidePanel(); else showPanel();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (vrControlPanel && vrControlPanel.visible && panelOpacity > 0.01) hidePanel(); else showPanel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEnterVRButtonClick() {
|
async function handleEnterVRButtonClick() {
|
||||||
@@ -982,11 +719,7 @@ async function actualSessionToggle() {
|
|||||||
xrSession = null;
|
xrSession = null;
|
||||||
|
|
||||||
if (vrControlPanel) {
|
if (vrControlPanel) {
|
||||||
clearTimeout(panelHideTimeout);
|
vrPanelVisibility.hideImmediately();
|
||||||
panelTargetOpacity = 0;
|
|
||||||
panelOpacity = 0;
|
|
||||||
hideVrPanelImmediately(vrPanel);
|
|
||||||
isPanelFading = false;
|
|
||||||
}
|
}
|
||||||
sessionToClose.end().catch(err => {
|
sessionToClose.end().catch(err => {
|
||||||
console.error("Error calling .end() on session:", err);
|
console.error("Error calling .end() on session:", err);
|
||||||
@@ -1034,16 +767,13 @@ async function actualSessionToggle() {
|
|||||||
updateVRPlayPauseButtonIcon();
|
updateVRPlayPauseButtonIcon();
|
||||||
updateVRVolumeButtonIcon();
|
updateVRVolumeButtonIcon();
|
||||||
if (vrControlPanel) {
|
if (vrControlPanel) {
|
||||||
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
vrPanelVisibility.hideImmediately();
|
||||||
clearTimeout(panelHideTimeout);
|
|
||||||
hideVrPanelImmediately(vrPanel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await renderer.xr.setSession(xrSession);
|
await renderer.xr.setSession(xrSession);
|
||||||
isXrLoopActive = true;
|
isXrLoopActive = true;
|
||||||
renderer.setAnimationLoop(renderXR);
|
renderer.setAnimationLoop(renderXR);
|
||||||
frameCounter = 0;
|
frameCounter = 0;
|
||||||
lastFadeTimestamp = performance.now();
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const sessionStartError = "XR_ERROR: Failed to start VR session: " + (err.message || String(err));
|
const sessionStartError = "XR_ERROR: Failed to start VR session: " + (err.message || String(err));
|
||||||
@@ -1054,9 +784,7 @@ 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) {
|
||||||
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
vrPanelVisibility.hideImmediately();
|
||||||
clearTimeout(panelHideTimeout);
|
|
||||||
hideVrPanelImmediately(vrPanel);
|
|
||||||
}
|
}
|
||||||
if (xrSession) {
|
if (xrSession) {
|
||||||
xrSession.removeEventListener('end', onVRSessionEnd);
|
xrSession.removeEventListener('end', onVRSessionEnd);
|
||||||
@@ -1104,10 +832,7 @@ function onVRSessionEnd(event) {
|
|||||||
}
|
}
|
||||||
hideContentMeshes();
|
hideContentMeshes();
|
||||||
if (vrControlPanel) {
|
if (vrControlPanel) {
|
||||||
clearTimeout(panelHideTimeout);
|
vrPanelVisibility.hideImmediately();
|
||||||
isPanelFading = false;
|
|
||||||
panelOpacity = 0; panelTargetOpacity = 0;
|
|
||||||
hideVrPanelImmediately(vrPanel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endedSession && typeof endedSession.removeEventListener === 'function') {
|
if (endedSession && typeof endedSession.removeEventListener === 'function') {
|
||||||
@@ -1128,10 +853,6 @@ function onVRSessionEnd(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleControllerInteractions() {
|
|
||||||
if (!renderer || !renderer.xr || !renderer.xr.isPresenting || !controller1) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderXR(timestamp, frame) {
|
function renderXR(timestamp, frame) {
|
||||||
if (!isXrLoopActive) {
|
if (!isXrLoopActive) {
|
||||||
return;
|
return;
|
||||||
@@ -1147,7 +868,7 @@ function renderXR(timestamp, frame) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPanelFading) {
|
if (vrPanelVisibility.isFading) {
|
||||||
animatePanelFade(timestamp);
|
animatePanelFade(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1168,7 +889,6 @@ function renderXR(timestamp, frame) {
|
|||||||
if (videoTexture && video && !video.paused && !video.ended) {
|
if (videoTexture && video && !video.paused && !video.ended) {
|
||||||
videoTexture.needsUpdate = true;
|
videoTexture.needsUpdate = true;
|
||||||
}
|
}
|
||||||
handleControllerInteractions();
|
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error));
|
const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error));
|
||||||
|
|||||||
Reference in New Issue
Block a user