forked from EXT/VR180-Web-Player
Initial build
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user