1
0

Initial build

This commit is contained in:
Aiden
2026-06-10 10:19:03 +10:00
parent 3bd2c135a9
commit 91b612785b
4 changed files with 318 additions and 213 deletions

View File

@@ -1,16 +1,17 @@
#vr-container {
.vrwp {
position: relative;
display: inline-block;
width: 100%;
}
video {
.vrwp-video,
.vrwp canvas {
width: 100%;
height: auto;
aspect-ratio: 16/9;
aspect-ratio: 16 / 9;
}
#playBtn {
.vrwp-play-button {
position: absolute;
top: 50%;
left: 50%;
@@ -25,41 +26,39 @@ video {
z-index: 10;
}
#playBtn:hover {
.vrwp-play-button:hover {
transform: translate(-50%, -50%) scale(1.1);
}
#playBtn:active {
.vrwp-play-button:active {
transform: translate(-50%, -50%) scale(0.95);
}
#playBtn.hidden {
.vrwp-play-button.hidden {
opacity: 0;
pointer-events: none;
}
#playBtn img {
.vrwp-play-button img {
width: 100%;
height: 100%;
}
/* Responsive sizing */
@media (max-width: 600px) {
#playBtn {
.vrwp-play-button {
width: 60px;
height: 60px;
}
}
@media (min-width: 900px) {
#playBtn {
.vrwp-play-button {
width: 100px;
height: 100px;
}
}
/* 2D Video Controls Panel */
#panel {
.vrwp-panel {
position: absolute;
bottom: 10%;
left: 50%;
@@ -69,7 +68,7 @@ video {
border-radius: 30px;
background: rgba(0, 0, 0, 0.70);
color: #fff;
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
z-index: 100;
opacity: 0;
visibility: hidden;
@@ -77,38 +76,38 @@ video {
pointer-events: none;
}
#panel.visible {
.vrwp-panel.visible {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
#status {
.vrwp-status {
margin: 0 12px 12px 12px;
}
#video-title {
.vrwp-video-title {
text-align: center;
margin: 0 0 16px 0;
font-size: 1rem;
font-weight: 500;
}
#current-time,
#total-time {
.vrwp-current-time,
.vrwp-total-time {
margin: 0;
font-size: 0.875rem;
font-variant-numeric: tabular-nums;
}
#progress {
.vrwp-progress {
display: grid;
grid-template-columns: min-content 1fr min-content;
grid-gap: 8px;
align-items: center;
}
#bar {
.vrwp-bar {
width: 100%;
height: 4px;
border-radius: 2px;
@@ -117,7 +116,7 @@ video {
position: relative;
}
#played {
.vrwp-played {
border-radius: 2px;
background: #fff;
height: 4px;
@@ -125,20 +124,20 @@ video {
transition: width 0.1s ease;
}
#controls {
.vrwp-controls {
display: grid;
grid-template-areas: "full lflex nav rflex mute";
grid-template-columns: 44px 1fr 156px 1fr 44px;
height: 44px;
}
#panel button {
.vrwp-panel button {
cursor: pointer;
border: none;
background-color: transparent;
}
#fullscreen {
.vrwp-fullscreen {
grid-area: full;
width: 44px;
height: 44px;
@@ -149,11 +148,11 @@ video {
transition: background-image 0.15s ease-in-out;
}
#fullscreen:hover {
.vrwp-fullscreen:hover {
background-image: url(images/fullscreen-hover.png);
}
#mute {
.vrwp-mute {
grid-area: mute;
width: 44px;
height: 44px;
@@ -164,27 +163,27 @@ video {
transition: background-image 0.15s ease-in-out;
}
#mute:hover {
.vrwp-mute:hover {
background-image: url(images/mute-hover.png);
}
#mute.muted {
.vrwp-mute.muted {
background-image: url(images/mute.png);
}
#mute.muted:hover {
.vrwp-mute.muted:hover {
background-image: url(images/mute-hover.png);
}
#mute.unmuted {
.vrwp-mute.unmuted {
background-image: url(images/unmute.png);
}
#mute.unmuted:hover {
.vrwp-mute.unmuted:hover {
background-image: url(images/unmute-hover.png);
}
#nav {
.vrwp-nav {
grid-area: nav;
display: grid;
grid-template-columns: 44px 44px 44px;
@@ -192,7 +191,7 @@ video {
height: 44px;
}
#back {
.vrwp-back {
width: 44px;
height: 44px;
background-image: url(images/back.png);
@@ -202,11 +201,11 @@ video {
transition: background-image 0.15s ease-in-out;
}
#back:hover {
.vrwp-back:hover {
background-image: url(images/back-hover.png);
}
#play2 {
.vrwp-play-toggle {
width: 44px;
height: 44px;
background-image: url(images/play2.png);
@@ -216,27 +215,27 @@ video {
transition: background-image 0.15s ease-in-out;
}
#play2:hover {
.vrwp-play-toggle:hover {
background-image: url(images/play2-hover.png);
}
#play2.paused {
.vrwp-play-toggle.paused {
background-image: url(images/play2.png);
}
#play2.paused:hover {
.vrwp-play-toggle.paused:hover {
background-image: url(images/play2-hover.png);
}
#play2.playing {
.vrwp-play-toggle.playing {
background-image: url(images/pause.png);
}
#play2.playing:hover {
.vrwp-play-toggle.playing:hover {
background-image: url(images/pause-hover.png);
}
#forward {
.vrwp-forward {
width: 44px;
height: 44px;
background-image: url(images/forward.png);
@@ -246,6 +245,6 @@ video {
transition: background-image 0.15s ease-in-out;
}
#forward:hover {
.vrwp-forward:hover {
background-image: url(images/forward-hover.png);
}

View File

@@ -1,8 +1,16 @@
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
const _playerBase = new URL('.', import.meta.url).href;
const PLAYER_SELECTOR = '[data-vr-web-player]';
const VALID_PROJECTIONS = new Set(['vr180', 'plane']);
const PLANE_WIDTH = 3.2;
const PLANE_HEIGHT = PLANE_WIDTH * (9 / 16);
const PLANE_DISTANCE = 3;
const PLANE_2D_DISTANCE = 1.2;
let playerContainer, projectionMode = 'vr180';
let scene, camera, renderer, video, videoTexture, sphereMaterial;
let vr180Mesh;
let vr180Mesh, planeMesh, activeContentMesh;
let xrSession = null;
let controller1, raycaster, uiElements = [];
const tempMatrix = new THREE.Matrix4();
@@ -106,9 +114,26 @@ const SOUND_MUTED_SVG_PATH = "M6.9082 2.8985C7.71639 2.45747 8.74994 3.03437 8.7
// Dynamic UI Creation Functions
function injectPlayerStyles() {
if (document.querySelector('link[data-vr-web-player-stylesheet]')) {
return;
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = _playerBase + 'vr180-player.css';
link.dataset.vrWebPlayerStylesheet = 'true';
if (document.head) {
document.head.appendChild(link);
} else {
document.addEventListener('DOMContentLoaded', () => document.head.appendChild(link), { once: true });
}
}
function createPlayButton() {
const playButton = document.createElement('button');
playButton.id = 'playBtn';
playButton.type = 'button';
playButton.className = 'vrwp-play-button';
playButton.setAttribute('aria-label', 'Play video');
const playImg = document.createElement('img');
@@ -122,32 +147,32 @@ function createPlayButton() {
function create2DControlPanel() {
const panel = document.createElement('div');
panel.id = 'panel';
panel.className = 'vrwp-panel';
// Status section
const status = document.createElement('div');
status.id = 'status';
status.className = 'vrwp-status';
const videoTitle = document.createElement('p');
videoTitle.id = 'video-title';
videoTitle.className = 'vrwp-video-title';
videoTitle.textContent = 'Title';
const progress = document.createElement('div');
progress.id = 'progress';
progress.className = 'vrwp-progress';
const currentTime = document.createElement('p');
currentTime.id = 'current-time';
currentTime.className = 'vrwp-current-time';
currentTime.textContent = '00:00:00';
const bar = document.createElement('div');
bar.id = 'bar';
bar.className = 'vrwp-bar';
const played = document.createElement('div');
played.id = 'played';
played.className = 'vrwp-played';
bar.appendChild(played);
const totalTime = document.createElement('p');
totalTime.id = 'total-time';
totalTime.className = 'vrwp-total-time';
totalTime.textContent = '00:00:00';
progress.appendChild(currentTime);
@@ -159,29 +184,39 @@ function create2DControlPanel() {
// Controls section
const controls = document.createElement('div');
controls.id = 'controls';
controls.className = 'vrwp-controls';
const fullscreenBtn = document.createElement('button');
fullscreenBtn.id = 'fullscreen';
fullscreenBtn.type = 'button';
fullscreenBtn.className = 'vrwp-fullscreen';
fullscreenBtn.setAttribute('aria-label', 'Toggle fullscreen');
const nav = document.createElement('div');
nav.id = 'nav';
nav.className = 'vrwp-nav';
const backBtn = document.createElement('button');
backBtn.id = 'back';
backBtn.type = 'button';
backBtn.className = 'vrwp-back';
backBtn.setAttribute('aria-label', 'Back 15 seconds');
const play2Btn = document.createElement('button');
play2Btn.id = 'play2';
play2Btn.type = 'button';
play2Btn.className = 'vrwp-play-toggle';
play2Btn.setAttribute('aria-label', 'Play or pause');
const forwardBtn = document.createElement('button');
forwardBtn.id = 'forward';
forwardBtn.type = 'button';
forwardBtn.className = 'vrwp-forward';
forwardBtn.setAttribute('aria-label', 'Forward 15 seconds');
nav.appendChild(backBtn);
nav.appendChild(play2Btn);
nav.appendChild(forwardBtn);
const muteBtn = document.createElement('button');
muteBtn.id = 'mute';
muteBtn.type = 'button';
muteBtn.className = 'vrwp-mute';
muteBtn.setAttribute('aria-label', 'Toggle mute');
controls.appendChild(fullscreenBtn);
controls.appendChild(nav);
@@ -194,28 +229,44 @@ function create2DControlPanel() {
return panel;
}
injectPlayerStyles();
document.addEventListener('DOMContentLoaded', () => {
videoElement = document.getElementById('vr180');
const containers = document.querySelectorAll(PLAYER_SELECTOR);
if (!videoElement) {
console.error("CRITICAL_ERROR_DOM: Essential HTML element (video) not found.");
if (containers.length === 0) {
console.error(`VR_WEB_PLAYER_DOM: Expected exactly one ${PLAYER_SELECTOR} container, found none.`);
return;
}
// Create and insert UI elements dynamically
const container = document.getElementById('vr-container');
if (!container) {
console.error("CRITICAL_ERROR_DOM: VR container not found.");
if (containers.length > 1) {
console.warn(`VR_WEB_PLAYER_DOM: This version supports exactly one ${PLAYER_SELECTOR} container per page. Found ${containers.length}; no player was initialized.`);
return;
}
playerContainer = containers[0];
playerContainer.classList.add('vrwp');
projectionMode = (playerContainer.dataset.projection || 'vr180').trim().toLowerCase();
if (!VALID_PROJECTIONS.has(projectionMode)) {
console.error(`VR_WEB_PLAYER_CONFIG: Unsupported data-projection="${projectionMode}". Use "vr180" or "plane".`);
return;
}
videoElement = playerContainer.querySelector('video');
if (!videoElement) {
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a video element.`);
return;
}
videoElement.classList.add('vrwp-video');
// Create and insert play button
playBtn = createPlayButton();
container.appendChild(playBtn);
playerContainer.appendChild(playBtn);
// Create and insert 2D control panel
const controlPanel = create2DControlPanel();
container.appendChild(controlPanel);
playerContainer.appendChild(controlPanel);
playBtn.disabled = true;
@@ -334,6 +385,86 @@ function createButtonTexture(textOrPathData, textColor = 'white', backgroundColo
return texture;
}
function isLeftEyeCamera(renderingRenderer, activeCamera) {
const xrCamera = renderingRenderer.xr.getCamera();
if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) {
if (activeCamera === xrCamera.cameras[0]) {
return true;
}
if (activeCamera === xrCamera.cameras[1]) {
return false;
}
const viewMatrixX = activeCamera.matrixWorldInverse.elements[12];
const leftCamX = xrCamera.cameras[0].matrixWorldInverse.elements[12];
const rightCamX = xrCamera.cameras[1].matrixWorldInverse.elements[12];
const diffToLeft = Math.abs(viewMatrixX - leftCamX);
const diffToRight = Math.abs(viewMatrixX - rightCamX);
if (diffToLeft < 0.001 || diffToLeft < diffToRight) {
return true;
}
if (diffToRight < 0.001) {
return false;
}
}
return activeCamera.projectionMatrix.elements[8] <= 0;
}
function applySbsTextureWindow(renderingRenderer, activeCamera, material) {
if (!material.map) return;
const isPresentingXR = renderingRenderer.xr.isPresenting;
// Non-XR fallback always shows the left eye so users never see the raw SBS double image.
if (is2DMode && !isPresentingXR) {
material.map.offset.x = 0;
material.map.repeat.x = 0.5;
material.map.offset.y = 0;
material.map.repeat.y = 1;
return;
}
material.map.offset.x = 0;
material.map.repeat.x = 1;
material.map.offset.y = 0;
material.map.repeat.y = 1;
if (!isPresentingXR) {
return;
}
material.map.offset.x = isLeftEyeCamera(renderingRenderer, activeCamera) ? 0 : 0.5;
material.map.repeat.x = 0.5;
}
function hideContentMeshes() {
if (vr180Mesh) vr180Mesh.visible = false;
if (planeMesh) planeMesh.visible = false;
}
function showActiveContentMesh() {
hideContentMeshes();
if (activeContentMesh) {
activeContentMesh.visible = true;
}
}
function positionPlaneForPresentation(isFallback2D = false) {
if (!planeMesh) return;
const zPosition = isFallback2D && camera2D ? camera2D.position.z - PLANE_2D_DISTANCE : -PLANE_DISTANCE;
planeMesh.position.set(0, 1.6, zPosition);
}
function createVideoTexture() {
if (videoTexture) videoTexture.dispose();
videoTexture = new THREE.VideoTexture(video);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.colorSpace = THREE.SRGBColorSpace;
return videoTexture;
}
function init() {
try {
@@ -349,7 +480,7 @@ function init() {
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.getElementById('vr-container').appendChild(renderer.domElement);
playerContainer.appendChild(renderer.domElement);
if (renderer.domElement) {
renderer.domElement.style.display = 'none';
@@ -373,11 +504,8 @@ function init() {
}, false);
gl.canvas.addEventListener('webglcontextrestored', (event) => {
console.log("CONTEXT_EVENT: WebGL Context Restored.");
if (video && sphereMaterial && vr180Mesh && vr180Mesh.visible && renderer.xr.isPresenting && xrSession) {
if (videoTexture) videoTexture.dispose();
videoTexture = new THREE.VideoTexture(video);
videoTexture.minFilter = THREE.LinearFilter; videoTexture.magFilter = THREE.LinearFilter;
videoTexture.colorSpace = THREE.SRGBColorSpace;
if (video && sphereMaterial && activeContentMesh && activeContentMesh.visible && renderer.xr.isPresenting && xrSession) {
videoTexture = createVideoTexture();
sphereMaterial.map = videoTexture;
sphereMaterial.needsUpdate = true;
updateVRPlayPauseButtonIcon();
@@ -403,73 +531,31 @@ function init() {
sphereMaterial = new THREE.MeshBasicMaterial({ map: null });
vr180Mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
vr180Mesh.name = "vr180Mesh";
uiElements.push(vr180Mesh);
vr180Mesh.rotation.y = Math.PI / 2;
scene.add(vr180Mesh);
vr180Mesh.visible = false;
vr180Mesh.onBeforeRender = function (renderer, scene, activeCamera, geometry, material, group) {
if (!material.map) return;
const isPresentingXR = renderer.xr.isPresenting;
// Handle 2D mode - show only left eye view
if (is2DMode && !isPresentingXR) {
material.map.offset.x = 0;
material.map.repeat.x = 0.5;
material.map.offset.y = 0;
material.map.repeat.y = 1;
return;
}
// Default to full texture for non-VR, non-2D mode
material.map.offset.x = 0; material.map.repeat.x = 1;
material.map.offset.y = 0; material.map.repeat.y = 1;
if (!isPresentingXR) {
return;
}
const xrCamera = renderer.xr.getCamera();
let isLeftEye = true; // Default to left eye
if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) {
// Method 1: Direct camera reference comparison
if (activeCamera === xrCamera.cameras[0]) {
isLeftEye = true;
} else if (activeCamera === xrCamera.cameras[1]) {
isLeftEye = false;
} else {
// Method 2: View matrix position (matrixWorldInverse.elements[12] is the X translation)
const viewMatrixX = activeCamera.matrixWorldInverse.elements[12];
const leftCamX = xrCamera.cameras[0].matrixWorldInverse.elements[12];
const rightCamX = xrCamera.cameras[1].matrixWorldInverse.elements[12];
const diffToLeft = Math.abs(viewMatrixX - leftCamX);
const diffToRight = Math.abs(viewMatrixX - rightCamX);
if (diffToLeft < 0.001 || diffToLeft < diffToRight) {
isLeftEye = true;
} else if (diffToRight < 0.001) {
isLeftEye = false;
} else {
// Method 3: Projection matrix asymmetry (elements[8] indicates eye offset)
const projMatrixEl8 = activeCamera.projectionMatrix.elements[8];
isLeftEye = projMatrixEl8 <= 0;
}
}
} else {
// Fallback when xrCamera.cameras is not available
const projMatrixEl8 = activeCamera.projectionMatrix.elements[8];
isLeftEye = projMatrixEl8 <= 0;
}
material.map.offset.x = isLeftEye ? 0 : 0.5;
material.map.repeat.x = 0.5;
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;
uiElements.push(activeContentMesh);
// Initialize 2D camera
camera2D = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera2D.position.set(0, 0, 0);
camera2D.position.set(0, 1.6, 0.1);
camera2D.rotation.set(0, 0, 0);
} catch (e) {
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
@@ -851,7 +937,7 @@ function onWindowResize() {
if (is2DMode) {
// In 2D mode, calculate canvas size based on container dimensions
const container = document.getElementById('vr-container');
const container = playerContainer;
if (container) {
const containerRect = container.getBoundingClientRect();
const containerWidth = containerRect.width;
@@ -1002,7 +1088,11 @@ function onTouchEnd(event) {
function render2D() {
if (!is2DMode) return;
updateCameraRotation();
if (projectionMode === 'vr180') {
updateCameraRotation();
} else if (camera2D) {
camera2D.rotation.set(0, 0, 0);
}
if (renderer && camera2D && scene) {
renderer.render(scene, camera2D);
@@ -1014,17 +1104,17 @@ function render2D() {
// 2D Control Panel Functions
function init2DControlPanel() {
// Get references to 2D control elements
controlPanel = document.getElementById('panel');
videoTitle = document.getElementById('video-title');
currentTimeDisplay = document.getElementById('current-time');
totalTimeDisplay = document.getElementById('total-time');
progressBar = document.getElementById('bar');
playedBar = document.getElementById('played');
fullscreenBtn = document.getElementById('fullscreen');
backBtn = document.getElementById('back');
play2Btn = document.getElementById('play2');
forwardBtn = document.getElementById('forward');
muteBtn = document.getElementById('mute');
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");
@@ -1089,9 +1179,6 @@ function init2DControlPanel() {
show2DControlPanel();
});
}
// Mouse movement listener will be added to canvas in start2DMode
document.addEventListener('touchstart', on2DTouchStart);
}
function show2DControlPanel() {
@@ -1118,6 +1205,12 @@ function onCanvasMouseMove() {
}
}
function onCanvasTouchStart() {
if (is2DMode) {
show2DControlPanel();
}
}
function on2DMouseMove() {
if (is2DMode) {
show2DControlPanel();
@@ -1178,7 +1271,7 @@ function update2DMuteButton() {
function toggle2DFullscreen() {
if (!document.fullscreenElement) {
// Enter fullscreen
const container = document.getElementById('vr-container');
const container = playerContainer;
if (container && container.requestFullscreen) {
container.requestFullscreen().catch(err => {
console.error('Error attempting to enable fullscreen:', err);
@@ -1218,7 +1311,7 @@ function position2DControlPanel() {
// Get the canvas dimensions and position
const canvas = renderer.domElement;
const canvasRect = canvas.getBoundingClientRect();
const containerRect = document.getElementById('vr-container').getBoundingClientRect();
const containerRect = playerContainer.getBoundingClientRect();
// Calculate 10% from the bottom of the canvas
const bottomOffset = canvasRect.height * 0.1;
@@ -1319,6 +1412,7 @@ function resetToOriginalState() {
cameraRotation = { yaw: 0, pitch: 0 };
cameraVelocity = { yaw: 0, pitch: 0 };
isDragging = false;
positionPlaneForPresentation(false);
// Hide WebGL canvas and show video element
if (renderer && renderer.domElement) {
@@ -1392,7 +1486,7 @@ function onSelectStartVR(event) {
const newTime = Math.max(0, Math.min(1, normalizedPosition)) * video.duration;
video.currentTime = newTime;
updateSeekBarAppearance();
} else if (firstIntersected.name === "vrControlPanelBackground" || firstIntersected.name === "vr180Mesh") {
} 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();
@@ -1431,7 +1525,7 @@ function start2DMode() {
is2DMode = true;
// Calculate canvas size based on container dimensions (same logic as onWindowResize)
const container = document.getElementById('vr-container');
const container = playerContainer;
if (container) {
const containerRect = container.getBoundingClientRect();
const containerWidth = containerRect.width;
@@ -1464,17 +1558,14 @@ function start2DMode() {
canvas.style.display = '';
// Create video texture if not exists
if (videoTexture) videoTexture.dispose();
videoTexture = new THREE.VideoTexture(video);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.colorSpace = THREE.SRGBColorSpace;
videoTexture = createVideoTexture();
positionPlaneForPresentation(projectionMode === 'plane');
// Apply texture to sphere material and make mesh visible
if (sphereMaterial && vr180Mesh) {
// Apply texture to the selected projection mesh and make it visible
if (sphereMaterial && activeContentMesh) {
sphereMaterial.map = videoTexture;
sphereMaterial.needsUpdate = true;
vr180Mesh.visible = true;
showActiveContentMesh();
}
// Start video playback
@@ -1500,21 +1591,29 @@ function start2DMode() {
}
function add2DEventListeners() {
// Mouse events
renderer.domElement.addEventListener('mousedown', onMouseDown);
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('mouseup', onMouseUp);
// Canvas-specific mouse movement for showing controls
renderer.domElement.addEventListener('mousemove', onCanvasMouseMove);
// Touch events
renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });
renderer.domElement.addEventListener('touchend', onTouchEnd, { passive: false });
if (projectionMode === 'vr180') {
// Mouse events
renderer.domElement.addEventListener('mousedown', onMouseDown);
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('mouseup', onMouseUp);
// Touch events
renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });
renderer.domElement.addEventListener('touchend', onTouchEnd, { passive: false });
} else {
renderer.domElement.addEventListener('touchstart', onCanvasTouchStart, { passive: true });
}
}
function remove2DEventListeners() {
if (!renderer || !renderer.domElement) return;
renderer.domElement.removeEventListener('mousemove', onCanvasMouseMove);
// Mouse events
renderer.domElement.removeEventListener('mousedown', onMouseDown);
renderer.domElement.removeEventListener('mousemove', onMouseMove);
@@ -1524,6 +1623,7 @@ function remove2DEventListeners() {
renderer.domElement.removeEventListener('touchstart', onTouchStart);
renderer.domElement.removeEventListener('touchmove', onTouchMove);
renderer.domElement.removeEventListener('touchend', onTouchEnd);
renderer.domElement.removeEventListener('touchstart', onCanvasTouchStart);
// Fullscreen events
document.removeEventListener('fullscreenchange', onFullscreenChange);
@@ -1592,16 +1692,15 @@ async function actualSessionToggle() {
}
if (camera) camera.updateProjectionMatrix();
positionPlaneForPresentation(false);
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
if (video) {
videoTexture = new THREE.VideoTexture(video);
videoTexture.minFilter = THREE.LinearFilter; videoTexture.magFilter = THREE.LinearFilter;
videoTexture.colorSpace = THREE.SRGBColorSpace;
if (vr180Mesh && sphereMaterial) {
videoTexture = createVideoTexture();
if (activeContentMesh && sphereMaterial) {
sphereMaterial.map = videoTexture;
sphereMaterial.needsUpdate = true;
vr180Mesh.visible = true;
showActiveContentMesh();
} else { throw new Error("VR mesh components not ready for texture."); }
} else {
throw new Error("Video element not available for creating texture.");
@@ -1631,7 +1730,7 @@ async function actualSessionToggle() {
console.error(sessionStartError, err);
isXrLoopActive = false;
if (vr180Mesh) vr180Mesh.visible = false;
hideContentMeshes();
if (sphereMaterial) { sphereMaterial.map = null; sphereMaterial.needsUpdate = true; }
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
if (vrControlPanel) {
@@ -1682,7 +1781,7 @@ function onVRSessionEnd(event) {
if (videoTexture) {
videoTexture.dispose(); videoTexture = null;
}
if (vr180Mesh) vr180Mesh.visible = false;
hideContentMeshes();
if (vrControlPanel) {
clearTimeout(panelHideTimeout);
isPanelFading = false;