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,
|
||||
PLANE_2D_DISTANCE,
|
||||
PLANE_DISTANCE,
|
||||
PLANE_HEIGHT,
|
||||
PLANE_WIDTH,
|
||||
PLAYER_SELECTOR,
|
||||
type ProjectionMode,
|
||||
VALID_PROJECTIONS
|
||||
} from './config.js';
|
||||
import { createContentScene } from './content-scene.js';
|
||||
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom.js';
|
||||
import {
|
||||
applySbsTextureWindow as applySbsTextureWindowCore,
|
||||
@@ -17,19 +16,21 @@ import {
|
||||
showActiveContentMesh as showActiveContentMeshCore
|
||||
} from './projection.js';
|
||||
import { createVideoTexture as createVideoTextureCore } from './three-utils.js';
|
||||
import { setLucideIcon } from './icons.js';
|
||||
import { FallbackCameraControls } from './fallback-camera-controls.js';
|
||||
import { formatTime } from './time.js';
|
||||
import {
|
||||
createVrController,
|
||||
handleVrControllerSelect
|
||||
} from './vr-controller-interactions.js';
|
||||
import { bindVideoEvents } from './video-events.js';
|
||||
import {
|
||||
createVrControlPanel,
|
||||
getSeekProgressFromIntersection,
|
||||
hideVrPanelImmediately,
|
||||
setVrPanelOpacity,
|
||||
type VrControlPanel,
|
||||
updateVrPlayPauseButtonIcon,
|
||||
updateVrSeekBarAppearance,
|
||||
updateVrVolumeButtonIcon
|
||||
} 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;
|
||||
|
||||
@@ -37,36 +38,21 @@ let playerContainer, projectionMode: ProjectionMode = DEFAULT_PROJECTION;
|
||||
let scene, camera, renderer, video, videoTexture, sphereMaterial;
|
||||
let vr180Mesh, planeMesh, activeContentMesh;
|
||||
let xrSession = null;
|
||||
let controller1, raycaster, uiElements = [];
|
||||
const tempMatrix = new THREE.Matrix4();
|
||||
let raycaster, uiElements = [];
|
||||
let videoElement, playBtn;
|
||||
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 is2DMode = false;
|
||||
let vrControlPanel;
|
||||
let vrPanel: VrControlPanel | undefined;
|
||||
let twoDControls: TwoDControlPanel | undefined;
|
||||
const vrPanelVisibility = new VrPanelVisibility();
|
||||
|
||||
// 2D Camera Controls
|
||||
let camera2D;
|
||||
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);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -221,48 +207,16 @@ function init() {
|
||||
}, false);
|
||||
|
||||
video = videoElement;
|
||||
|
||||
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) {
|
||||
const contentScene = createContentScene(scene, projectionMode, (renderer, scene, activeCamera, geometry, material, group) => {
|
||||
applySbsTextureWindow(renderer, activeCamera, material);
|
||||
};
|
||||
|
||||
const planeGeometry = new THREE.PlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT);
|
||||
planeMesh = new THREE.Mesh(planeGeometry, sphereMaterial);
|
||||
planeMesh.name = "vrSbsPlaneMesh";
|
||||
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;
|
||||
});
|
||||
sphereMaterial = contentScene.material;
|
||||
vr180Mesh = contentScene.vr180Mesh;
|
||||
planeMesh = contentScene.planeMesh;
|
||||
activeContentMesh = contentScene.activeContentMesh;
|
||||
uiElements.push(activeContentMesh);
|
||||
|
||||
// Initialize 2D camera
|
||||
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);
|
||||
camera2D = contentScene.fallbackCamera;
|
||||
fallbackCameraControls = new FallbackCameraControls(camera2D, {
|
||||
hideControls: hide2DControlPanel,
|
||||
isEnabled: () => is2DMode,
|
||||
@@ -277,19 +231,10 @@ function init() {
|
||||
try { // Phase 2: VR Control Panel UI
|
||||
vrPanel = createVrControlPanel(scene, getVideoTitle());
|
||||
vrControlPanel = vrPanel.group;
|
||||
vrPanelVisibility.setPanel(vrPanel);
|
||||
uiElements.push(...vrPanel.interactables);
|
||||
|
||||
panelOpacity = 0;
|
||||
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;
|
||||
raycaster = createVrController(scene, renderer, onSelectStartVR).raycaster;
|
||||
} catch (e) {
|
||||
console.error("INIT_ERROR (Phase 2 - VR Controls Setup):", e);
|
||||
}
|
||||
@@ -301,46 +246,23 @@ function init() {
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
|
||||
if (video) {
|
||||
video.onloadedmetadata = () => {
|
||||
if (isFinite(video.duration) && playBtn) {
|
||||
// Enable button for both VR and non-VR scenarios when video is ready
|
||||
playBtn.disabled = false;
|
||||
}
|
||||
updateSeekBarAppearance();
|
||||
updateVRPlayPauseButtonIcon();
|
||||
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)) {
|
||||
bindVideoEvents({
|
||||
onEnded: onVideoEnded,
|
||||
onPlaybackStateChange: () => {
|
||||
updateVRPlayPauseButtonIcon();
|
||||
update2DPlayPauseButton();
|
||||
},
|
||||
onTimelineChange: () => {
|
||||
updateSeekBarAppearance();
|
||||
update2DControlPanel();
|
||||
}
|
||||
};
|
||||
video.onplaying = () => {
|
||||
updateVRPlayPauseButtonIcon();
|
||||
update2DPlayPauseButton();
|
||||
};
|
||||
video.onpause = () => {
|
||||
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);
|
||||
},
|
||||
onVolumeChange: () => {
|
||||
updateVRVolumeButtonIcon();
|
||||
update2DMuteButton();
|
||||
},
|
||||
playButton: playBtn,
|
||||
video
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize 2D control panel
|
||||
@@ -373,60 +295,15 @@ function updateSeekBarAppearance() {
|
||||
}
|
||||
|
||||
function animatePanelFade(timestamp) {
|
||||
if (!vrControlPanel) return;
|
||||
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);
|
||||
vrPanelVisibility.updateFade(timestamp);
|
||||
}
|
||||
|
||||
function showPanel() {
|
||||
if (vrControlPanel) vrControlPanel.visible = true;
|
||||
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);
|
||||
vrPanelVisibility.show();
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
clearTimeout(panelHideTimeout);
|
||||
if (panelTargetOpacity !== 0.0 || panelOpacity > 0.0) {
|
||||
panelTargetOpacity = 0.0;
|
||||
if (!isPanelFading) {
|
||||
isPanelFading = true;
|
||||
lastFadeTimestamp = 0;
|
||||
requestAnimationFrame(animatePanelFade);
|
||||
}
|
||||
}
|
||||
vrPanelVisibility.hide();
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
@@ -500,209 +377,74 @@ function render2D() {
|
||||
|
||||
// 2D Control Panel Functions
|
||||
function init2DControlPanel() {
|
||||
// Get references to 2D control elements
|
||||
controlPanel = playerContainer.querySelector('.vrwp-panel');
|
||||
videoTitle = playerContainer.querySelector('.vrwp-video-title');
|
||||
currentTimeDisplay = playerContainer.querySelector('.vrwp-current-time');
|
||||
totalTimeDisplay = playerContainer.querySelector('.vrwp-total-time');
|
||||
progressBar = playerContainer.querySelector('.vrwp-bar');
|
||||
playedBar = playerContainer.querySelector('.vrwp-played');
|
||||
fullscreenBtn = playerContainer.querySelector('.vrwp-fullscreen');
|
||||
backBtn = playerContainer.querySelector('.vrwp-back');
|
||||
play2Btn = playerContainer.querySelector('.vrwp-play-toggle');
|
||||
forwardBtn = playerContainer.querySelector('.vrwp-forward');
|
||||
muteBtn = playerContainer.querySelector('.vrwp-mute');
|
||||
|
||||
if (!controlPanel) {
|
||||
console.error("2D Control panel not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial video title
|
||||
if (videoTitle && video) {
|
||||
videoTitle.textContent = getVideoTitle();
|
||||
}
|
||||
|
||||
// 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);
|
||||
twoDControls = new TwoDControlPanel({
|
||||
callbacks: {
|
||||
onForward: () => {
|
||||
if (video && isFinite(video.duration)) {
|
||||
video.currentTime = Math.min(video.duration, video.currentTime + 15);
|
||||
}
|
||||
},
|
||||
onMute: () => {
|
||||
if (video) {
|
||||
video.muted = !video.muted;
|
||||
}
|
||||
},
|
||||
onPlayPause: togglePlayPause,
|
||||
onRewind: () => {
|
||||
if (video) {
|
||||
video.currentTime = Math.max(0, video.currentTime - 15);
|
||||
}
|
||||
},
|
||||
onSeek: (progress) => {
|
||||
if (video && isFinite(video.duration)) {
|
||||
video.currentTime = progress * video.duration;
|
||||
}
|
||||
}
|
||||
show2DControlPanel();
|
||||
});
|
||||
}
|
||||
|
||||
if (play2Btn) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
},
|
||||
fullscreenTarget: playerContainer,
|
||||
getIsActive: () => is2DMode,
|
||||
playerContainer,
|
||||
title: getVideoTitle()
|
||||
});
|
||||
}
|
||||
|
||||
function show2DControlPanel() {
|
||||
if (!is2DMode || !controlPanel) return;
|
||||
|
||||
clearTimeout(controlPanelTimeout);
|
||||
controlPanel.classList.add('visible');
|
||||
isControlPanelVisible = true;
|
||||
|
||||
controlPanelTimeout = setTimeout(hide2DControlPanel, CONTROL_PANEL_HIDE_DELAY);
|
||||
twoDControls?.show();
|
||||
}
|
||||
|
||||
function hide2DControlPanel() {
|
||||
if (!controlPanel) return;
|
||||
|
||||
clearTimeout(controlPanelTimeout);
|
||||
controlPanel.classList.remove('visible');
|
||||
isControlPanelVisible = false;
|
||||
twoDControls?.hide();
|
||||
}
|
||||
|
||||
function update2DControlPanel() {
|
||||
if (!is2DMode || !video) return;
|
||||
|
||||
// Update time displays
|
||||
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}%`;
|
||||
}
|
||||
twoDControls?.updateTime(video.currentTime, video.duration);
|
||||
}
|
||||
|
||||
function update2DPlayPauseButton() {
|
||||
if (!is2DMode || !play2Btn || !video) return;
|
||||
if (!is2DMode || !video) return;
|
||||
|
||||
if (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');
|
||||
}
|
||||
twoDControls?.updatePlaybackButton(video.paused || video.ended);
|
||||
}
|
||||
|
||||
function update2DMuteButton() {
|
||||
if (!is2DMode || !muteBtn || !video) return;
|
||||
if (!is2DMode || !video) return;
|
||||
|
||||
if (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);
|
||||
});
|
||||
}
|
||||
}
|
||||
twoDControls?.updateMuteButton(video.muted);
|
||||
}
|
||||
|
||||
function handle2DVideoEnd() {
|
||||
if (!is2DMode || !video) return;
|
||||
|
||||
// Keep video at last frame (don't reset currentTime)
|
||||
// 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
|
||||
twoDControls?.showPersistent();
|
||||
update2DPlayPauseButton();
|
||||
}
|
||||
|
||||
function position2DControlPanel() {
|
||||
if (!is2DMode || !controlPanel || !renderer) return;
|
||||
if (!renderer) return;
|
||||
|
||||
// Get the canvas dimensions and position
|
||||
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
|
||||
twoDControls?.position(renderer.domElement);
|
||||
}
|
||||
|
||||
function hidePlayButton() {
|
||||
@@ -812,44 +554,39 @@ function onVideoEnded() {
|
||||
}
|
||||
|
||||
function onSelectStartVR(event) {
|
||||
const controller = event.target;
|
||||
if (!raycaster) return;
|
||||
controller.updateMatrixWorld();
|
||||
tempMatrix.identity().extractRotation(controller.matrixWorld);
|
||||
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
|
||||
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
|
||||
const allInteractables = [...uiElements];
|
||||
const directIntersects = raycaster.intersectObjects(allInteractables, true);
|
||||
if (directIntersects.length > 0) {
|
||||
const firstIntersected = directIntersects[0].object;
|
||||
const intersectionPoint = directIntersects[0].point;
|
||||
if (firstIntersected.name === "vrPlayPauseButton") {
|
||||
togglePlayPause(); showPanel();
|
||||
} else if (firstIntersected.name === "vrRewindButton") {
|
||||
if (video) { video.currentTime = Math.max(0, video.currentTime - 15); updateSeekBarAppearance(); }
|
||||
showPanel();
|
||||
} else if (firstIntersected.name === "vrForwardButton") {
|
||||
if (video && isFinite(video.duration)) { video.currentTime = Math.min(video.duration, video.currentTime + 15); updateSeekBarAppearance(); }
|
||||
showPanel();
|
||||
} else if (firstIntersected.name === "vrExitButton") {
|
||||
if (xrSession) actualSessionToggle(); // Should trigger exit
|
||||
showPanel(); // Keep panel briefly visible or hide, depending on desired UX
|
||||
} else if (firstIntersected.name === "vrVolumeButton") {
|
||||
handleVrControllerSelect(event, {
|
||||
exitVr: () => {
|
||||
if (xrSession) actualSessionToggle();
|
||||
},
|
||||
forward: () => {
|
||||
if (video && isFinite(video.duration)) {
|
||||
video.currentTime = Math.min(video.duration, video.currentTime + 15);
|
||||
updateSeekBarAppearance();
|
||||
}
|
||||
},
|
||||
hidePanel,
|
||||
isPanelVisible: () => vrPanelVisibility.isVisible,
|
||||
raycaster,
|
||||
rewind: () => {
|
||||
if (video) {
|
||||
video.currentTime = Math.max(0, video.currentTime - 15);
|
||||
updateSeekBarAppearance();
|
||||
}
|
||||
},
|
||||
seek: (progress) => {
|
||||
if (video && isFinite(video.duration)) {
|
||||
video.currentTime = progress * video.duration;
|
||||
updateSeekBarAppearance();
|
||||
}
|
||||
},
|
||||
showPanel,
|
||||
toggleMute: () => {
|
||||
if (video) video.muted = !video.muted;
|
||||
showPanel();
|
||||
} else if (firstIntersected.name === "seekBarHitArea" && video && isFinite(video.duration)) {
|
||||
showPanel();
|
||||
const newTime = getSeekProgressFromIntersection(vrPanel, intersectionPoint) * video.duration;
|
||||
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();
|
||||
}
|
||||
},
|
||||
togglePlayPause,
|
||||
uiElements,
|
||||
vrPanel
|
||||
});
|
||||
}
|
||||
|
||||
async function handleEnterVRButtonClick() {
|
||||
@@ -982,11 +719,7 @@ async function actualSessionToggle() {
|
||||
xrSession = null;
|
||||
|
||||
if (vrControlPanel) {
|
||||
clearTimeout(panelHideTimeout);
|
||||
panelTargetOpacity = 0;
|
||||
panelOpacity = 0;
|
||||
hideVrPanelImmediately(vrPanel);
|
||||
isPanelFading = false;
|
||||
vrPanelVisibility.hideImmediately();
|
||||
}
|
||||
sessionToClose.end().catch(err => {
|
||||
console.error("Error calling .end() on session:", err);
|
||||
@@ -1034,16 +767,13 @@ async function actualSessionToggle() {
|
||||
updateVRPlayPauseButtonIcon();
|
||||
updateVRVolumeButtonIcon();
|
||||
if (vrControlPanel) {
|
||||
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
||||
clearTimeout(panelHideTimeout);
|
||||
hideVrPanelImmediately(vrPanel);
|
||||
vrPanelVisibility.hideImmediately();
|
||||
}
|
||||
|
||||
await renderer.xr.setSession(xrSession);
|
||||
isXrLoopActive = true;
|
||||
renderer.setAnimationLoop(renderXR);
|
||||
frameCounter = 0;
|
||||
lastFadeTimestamp = performance.now();
|
||||
|
||||
} catch (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 (videoTexture) { videoTexture.dispose(); videoTexture = null; }
|
||||
if (vrControlPanel) {
|
||||
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
|
||||
clearTimeout(panelHideTimeout);
|
||||
hideVrPanelImmediately(vrPanel);
|
||||
vrPanelVisibility.hideImmediately();
|
||||
}
|
||||
if (xrSession) {
|
||||
xrSession.removeEventListener('end', onVRSessionEnd);
|
||||
@@ -1104,10 +832,7 @@ function onVRSessionEnd(event) {
|
||||
}
|
||||
hideContentMeshes();
|
||||
if (vrControlPanel) {
|
||||
clearTimeout(panelHideTimeout);
|
||||
isPanelFading = false;
|
||||
panelOpacity = 0; panelTargetOpacity = 0;
|
||||
hideVrPanelImmediately(vrPanel);
|
||||
vrPanelVisibility.hideImmediately();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!isXrLoopActive) {
|
||||
return;
|
||||
@@ -1147,7 +868,7 @@ function renderXR(timestamp, frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPanelFading) {
|
||||
if (vrPanelVisibility.isFading) {
|
||||
animatePanelFade(timestamp);
|
||||
}
|
||||
|
||||
@@ -1168,7 +889,6 @@ function renderXR(timestamp, frame) {
|
||||
if (videoTexture && video && !video.paused && !video.ended) {
|
||||
videoTexture.needsUpdate = true;
|
||||
}
|
||||
handleControllerInteractions();
|
||||
renderer.render(scene, camera);
|
||||
} catch (error) {
|
||||
const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error));
|
||||
|
||||
Reference in New Issue
Block a user