1
0

Further refactor

This commit is contained in:
Aiden
2026-06-10 11:37:02 +10:00
parent 899027e531
commit cb332abd4f
6 changed files with 660 additions and 397 deletions

View File

@@ -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 (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');
}
if (!is2DMode || !video) return;
twoDControls?.updatePlaybackButton(video.paused || video.ended);
}
function update2DMuteButton() {
if (!is2DMode || !muteBtn || !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');
}
}
if (!is2DMode || !video) return;
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;
// 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
if (!renderer) return;
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));