1
0

Seperation

This commit is contained in:
Aiden
2026-06-10 11:26:29 +10:00
parent fd82e1666f
commit 899027e531
5 changed files with 553 additions and 464 deletions

View File

@@ -16,12 +16,20 @@ import {
positionPlaneForPresentation as positionPlaneForPresentationCore,
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 {
createLucideButtonTexture,
createVideoTexture as createVideoTextureCore,
drawRoundedRect
} from './three-utils.js';
import { drawLucideIcon, setLucideIcon } from './icons.js';
createVrControlPanel,
getSeekProgressFromIntersection,
hideVrPanelImmediately,
setVrPanelOpacity,
type VrControlPanel,
updateVrPlayPauseButtonIcon,
updateVrSeekBarAppearance,
updateVrVolumeButtonIcon
} from './vr-control-panel.js';
const _playerBase = new URL('.', import.meta.url).href;
@@ -44,68 +52,11 @@ const CONTROL_PANEL_HIDE_DELAY = 3000; // 3 seconds
let isXrLoopActive = false;
let is2DMode = false;
let vrControlPanel;
let vrPanel: VrControlPanel | undefined;
// 2D Camera Controls
let camera2D;
let cameraRotation = { yaw: 0, pitch: 0 };
let cameraVelocity = { yaw: 0, pitch: 0 };
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;
let lastTouchX = 0;
let lastTouchY = 0;
// 2D Control Constants
const MOUSE_SENSITIVITY = 0.002;
const TOUCH_SENSITIVITY = 0.003;
const MOMENTUM_DAMPING = 0.8; // Reduced from 0.9 for 50% less inertia
const MAX_PITCH = Math.PI * (45 / 180); // ~45 degrees - edge of VR180 content aligns with viewport edge
const MAX_YAW = Math.PI * (45 / 180); // ~45 degrees - edge of VR180 content aligns with viewport edge
// Figma design constants (for layout reference)
const FIGMA_PANEL_WIDTH_PX = 450;
const FIGMA_PANEL_HEIGHT_PX = 132;
const FIGMA_CORNER_RADIUS_PX = 30;
const FIGMA_TITLE_FONT_SIZE_PX = 14;
const FIGMA_TITLE_MARGIN_TOP_PX = 20;
const FIGMA_SEEK_BAR_WIDTH_PX = 386;
const FIGMA_SEEK_BAR_HEIGHT_PX = 5;
const FIGMA_SEEK_BAR_Y_OFFSET_FROM_PANEL_CENTER_PX = (FIGMA_PANEL_HEIGHT_PX / 2) - 54;
const FIGMA_PLAYPAUSE_BUTTON_SIZE_PX = 44;
const FIGMA_PLAYPAUSE_BUTTON_X_PX = 225;
const FIGMA_PLAYPAUSE_BUTTON_Y_PX = 90;
const FIGMA_REWIND_BUTTON_SIZE_PX = 44;
const FIGMA_REWIND_BUTTON_X_PX = 169;
const FIGMA_REWIND_BUTTON_Y_PX = 90;
const FIGMA_FORWARD_BUTTON_SIZE_PX = 44;
const FIGMA_FORWARD_BUTTON_X_PX = 281;
const FIGMA_FORWARD_BUTTON_Y_PX = 90;
const FIGMA_EXIT_BUTTON_SIZE_PX = 44;
const FIGMA_EXIT_BUTTON_X_PX = 42;
const FIGMA_EXIT_BUTTON_Y_PX = 90;
const FIGMA_VOLUME_BUTTON_SIZE_PX = 44;
const FIGMA_VOLUME_BUTTON_X_PX = 408;
const FIGMA_VOLUME_BUTTON_Y_PX = 90;
// World space dimensions derived from Figma constants
const WORLD_PANEL_WIDTH = 1.5;
const SCALE_FACTOR = WORLD_PANEL_WIDTH / FIGMA_PANEL_WIDTH_PX;
const WORLD_PANEL_HEIGHT = FIGMA_PANEL_HEIGHT_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_WIDTH = FIGMA_SEEK_BAR_WIDTH_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_TRACK_HEIGHT = (FIGMA_SEEK_BAR_HEIGHT_PX) * SCALE_FACTOR;
const WORLD_SEEK_BAR_PROGRESS_HEIGHT = (FIGMA_SEEK_BAR_HEIGHT_PX - 1) * SCALE_FACTOR;
const WORLD_SEEK_BAR_Y_OFFSET = FIGMA_SEEK_BAR_Y_OFFSET_FROM_PANEL_CENTER_PX * SCALE_FACTOR;
const WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER = 5;
const WORLD_SEEK_BAR_HIT_AREA_HEIGHT = WORLD_SEEK_BAR_TRACK_HEIGHT * WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER;
const PANEL_TEXTURE_WIDTH = 1024;
const PANEL_TEXTURE_HEIGHT = Math.round(PANEL_TEXTURE_WIDTH * (FIGMA_PANEL_HEIGHT_PX / FIGMA_PANEL_WIDTH_PX));
let fallbackCameraControls: FallbackCameraControls | undefined;
// Panel fade animation variables
let panelOpacity = 0;
@@ -116,17 +67,6 @@ let lastFadeTimestamp = 0;
const FADE_DURATION_MS = 200;
const AUTO_HIDE_DELAY_MS = 10000;
// VR UI Meshes
let seekBarTrackMesh, seekBarProgressMesh, seekBarHitAreaMesh;
let vrPlayPauseButtonMesh, vrPlayPauseButtonCanvas, vrPlayPauseButtonContext, vrPlayPauseButtonTexture;
let vrRewindButtonMesh;
let vrForwardButtonMesh;
let vrExitButtonMesh;
let vrVolumeButtonMesh, vrVolumeButtonCanvas, vrVolumeButtonContext, vrVolumeButtonTexture;
const VR_BUTTON_TEXTURE_SIZE = 128;
injectPlayerStyles(_playerBase);
document.addEventListener('DOMContentLoaded', () => {
@@ -225,6 +165,12 @@ function createVideoTexture() {
return videoTexture;
}
function getVideoTitle() {
return videoElement.getAttribute('title') ||
videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
"Video Title";
}
function init() {
try {
@@ -317,6 +263,11 @@ function init() {
camera2D = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera2D.position.set(0, 1.6, 0.1);
camera2D.rotation.set(0, 0, 0);
fallbackCameraControls = new FallbackCameraControls(camera2D, {
hideControls: hide2DControlPanel,
isEnabled: () => is2DMode,
showControls: show2DControlPanel
});
} catch (e) {
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
renderer = null;
@@ -324,170 +275,10 @@ function init() {
}
try { // Phase 2: VR Control Panel UI
vrControlPanel = new THREE.Group();
vrControlPanel.position.set(0, 0.5, -1.8);
vrControlPanel.rotation.x = 0;
scene.add(vrControlPanel);
vrPanel = createVrControlPanel(scene, getVideoTitle());
vrControlPanel = vrPanel.group;
uiElements.push(...vrPanel.interactables);
const panelCanvas = document.createElement('canvas');
panelCanvas.width = PANEL_TEXTURE_WIDTH;
panelCanvas.height = PANEL_TEXTURE_HEIGHT;
const panelCtx = panelCanvas.getContext('2d');
panelCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
const textureCornerRadius = FIGMA_CORNER_RADIUS_PX * (PANEL_TEXTURE_WIDTH / FIGMA_PANEL_WIDTH_PX);
drawRoundedRect(panelCtx, 0, 0, PANEL_TEXTURE_WIDTH, PANEL_TEXTURE_HEIGHT, textureCornerRadius, true, false);
const videoTitle = videoElement.getAttribute('title') || videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') || "Video Title";
const titleFontSizeTexturePx = Math.round(FIGMA_TITLE_FONT_SIZE_PX * (PANEL_TEXTURE_HEIGHT / FIGMA_PANEL_HEIGHT_PX));
panelCtx.fillStyle = '#ffffff';
panelCtx.font = `500 ${titleFontSizeTexturePx}px Helvetica, Arial, sans-serif`;
panelCtx.textAlign = 'center';
panelCtx.textBaseline = 'top';
const titleMarginTopTexturePx = FIGMA_TITLE_MARGIN_TOP_PX * (PANEL_TEXTURE_HEIGHT / FIGMA_PANEL_HEIGHT_PX);
panelCtx.fillText(videoTitle, PANEL_TEXTURE_WIDTH / 2, titleMarginTopTexturePx);
const panelTexture = new THREE.CanvasTexture(panelCanvas);
panelTexture.minFilter = THREE.LinearFilter;
panelTexture.needsUpdate = true;
const panelMaterial = new THREE.MeshBasicMaterial({
map: panelTexture,
transparent: true,
opacity: 0,
depthWrite: false
});
const panelGeometry = new THREE.PlaneGeometry(WORLD_PANEL_WIDTH, WORLD_PANEL_HEIGHT);
const panelMesh = new THREE.Mesh(panelGeometry, panelMaterial);
panelMesh.name = "vrControlPanelBackground";
panelMesh.renderOrder = 0;
vrControlPanel.add(panelMesh);
uiElements.push(panelMesh);
const seekBarTrackMaterial = new THREE.MeshBasicMaterial({ color: 0x767676, transparent: true, opacity: 0 });
const seekBarTrackGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_TRACK_HEIGHT);
seekBarTrackMesh = new THREE.Mesh(seekBarTrackGeometry, seekBarTrackMaterial);
seekBarTrackMesh.name = "seekBarTrackVisual";
seekBarTrackMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
seekBarTrackMesh.position.z = 0.01;
seekBarTrackMesh.renderOrder = 1;
vrControlPanel.add(seekBarTrackMesh);
const seekBarProgressMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0 });
const seekBarProgressGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_PROGRESS_HEIGHT);
seekBarProgressMesh = new THREE.Mesh(seekBarProgressGeometry, seekBarProgressMaterial);
seekBarProgressMesh.name = "seekBarProgressVisual";
seekBarProgressMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET + (1 * SCALE_FACTOR);
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
seekBarProgressMesh.position.z = 0.015;
seekBarProgressMesh.scale.x = 0.001;
seekBarProgressMesh.renderOrder = 2;
vrControlPanel.add(seekBarProgressMesh);
const seekBarHitAreaGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_TRACK_HEIGHT * WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER);
const seekBarHitAreaMaterial = new THREE.MeshBasicMaterial({ visible: false, transparent: true, opacity: 0 });
seekBarHitAreaMesh = new THREE.Mesh(seekBarHitAreaGeometry, seekBarHitAreaMaterial);
seekBarHitAreaMesh.name = "seekBarHitArea";
seekBarHitAreaMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
seekBarHitAreaMesh.position.z = 0.012;
seekBarHitAreaMesh.renderOrder = 2;
vrControlPanel.add(seekBarHitAreaMesh);
uiElements.push(seekBarHitAreaMesh);
vrPlayPauseButtonCanvas = document.createElement('canvas');
vrPlayPauseButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
vrPlayPauseButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
vrPlayPauseButtonContext = vrPlayPauseButtonCanvas.getContext('2d');
vrPlayPauseButtonTexture = new THREE.CanvasTexture(vrPlayPauseButtonCanvas);
vrPlayPauseButtonTexture.minFilter = THREE.LinearFilter;
const playPauseButtonMaterial = new THREE.MeshBasicMaterial({
map: vrPlayPauseButtonTexture,
transparent: true,
opacity: 0,
depthWrite: false
});
const playPauseButtonWorldSize = FIGMA_PLAYPAUSE_BUTTON_SIZE_PX * SCALE_FACTOR;
const playPauseButtonGeometry = new THREE.PlaneGeometry(playPauseButtonWorldSize, playPauseButtonWorldSize);
vrPlayPauseButtonMesh = new THREE.Mesh(playPauseButtonGeometry, playPauseButtonMaterial);
vrPlayPauseButtonMesh.name = "vrPlayPauseButton";
vrPlayPauseButtonMesh.renderOrder = 3;
const figmaPlayPauseButtonCenterX = FIGMA_PLAYPAUSE_BUTTON_X_PX;
const figmaPlayPauseButtonCenterY = FIGMA_PLAYPAUSE_BUTTON_Y_PX;
const worldPlayPauseButtonOffsetX = (figmaPlayPauseButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldPlayPauseButtonOffsetY = -(figmaPlayPauseButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrPlayPauseButtonMesh.position.set(worldPlayPauseButtonOffsetX, worldPlayPauseButtonOffsetY, 0.02);
vrControlPanel.add(vrPlayPauseButtonMesh);
uiElements.push(vrPlayPauseButtonMesh);
const rewindButtonWorldSize = FIGMA_REWIND_BUTTON_SIZE_PX * SCALE_FACTOR;
const rewindButtonTexture = createLucideButtonTexture('rotate-ccw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, 82, '15');
const rewindButtonMaterial = new THREE.MeshBasicMaterial({ map: rewindButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const rewindButtonGeometry = new THREE.PlaneGeometry(rewindButtonWorldSize, rewindButtonWorldSize);
vrRewindButtonMesh = new THREE.Mesh(rewindButtonGeometry, rewindButtonMaterial);
vrRewindButtonMesh.name = "vrRewindButton";
vrRewindButtonMesh.renderOrder = 3;
const figmaRewindButtonCenterX = FIGMA_REWIND_BUTTON_X_PX;
const figmaRewindButtonCenterY = FIGMA_REWIND_BUTTON_Y_PX;
const worldRewindButtonOffsetX = (figmaRewindButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldRewindButtonOffsetY = -(figmaRewindButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrRewindButtonMesh.position.set(worldRewindButtonOffsetX, worldRewindButtonOffsetY, 0.02);
vrControlPanel.add(vrRewindButtonMesh);
uiElements.push(vrRewindButtonMesh);
const forwardButtonWorldSize = FIGMA_FORWARD_BUTTON_SIZE_PX * SCALE_FACTOR;
const forwardButtonTexture = createLucideButtonTexture('rotate-cw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, 82, '15');
const forwardButtonMaterial = new THREE.MeshBasicMaterial({ map: forwardButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const forwardButtonGeometry = new THREE.PlaneGeometry(forwardButtonWorldSize, forwardButtonWorldSize);
vrForwardButtonMesh = new THREE.Mesh(forwardButtonGeometry, forwardButtonMaterial);
vrForwardButtonMesh.name = "vrForwardButton";
vrForwardButtonMesh.renderOrder = 3;
const figmaForwardButtonCenterX = FIGMA_FORWARD_BUTTON_X_PX;
const figmaForwardButtonCenterY = FIGMA_FORWARD_BUTTON_Y_PX;
const worldForwardButtonOffsetX = (figmaForwardButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldForwardButtonOffsetY = -(figmaForwardButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrForwardButtonMesh.position.set(worldForwardButtonOffsetX, worldForwardButtonOffsetY, 0.02);
vrControlPanel.add(vrForwardButtonMesh);
uiElements.push(vrForwardButtonMesh);
const exitButtonWorldSize = FIGMA_EXIT_BUTTON_SIZE_PX * SCALE_FACTOR;
const exitButtonTexture = createLucideButtonTexture('log-out', '#ffffff', VR_BUTTON_TEXTURE_SIZE, 82);
const exitButtonMaterial = new THREE.MeshBasicMaterial({ map: exitButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const exitButtonGeometry = new THREE.PlaneGeometry(exitButtonWorldSize, exitButtonWorldSize);
vrExitButtonMesh = new THREE.Mesh(exitButtonGeometry, exitButtonMaterial);
vrExitButtonMesh.name = "vrExitButton";
vrExitButtonMesh.renderOrder = 3;
const figmaExitButtonCenterX = FIGMA_EXIT_BUTTON_X_PX;
const figmaExitButtonCenterY = FIGMA_EXIT_BUTTON_Y_PX;
const worldExitButtonOffsetX = (figmaExitButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldExitButtonOffsetY = -(figmaExitButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrExitButtonMesh.position.set(worldExitButtonOffsetX, worldExitButtonOffsetY, 0.02);
vrControlPanel.add(vrExitButtonMesh);
uiElements.push(vrExitButtonMesh);
vrVolumeButtonCanvas = document.createElement('canvas');
vrVolumeButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
vrVolumeButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
vrVolumeButtonContext = vrVolumeButtonCanvas.getContext('2d');
vrVolumeButtonTexture = new THREE.CanvasTexture(vrVolumeButtonCanvas);
vrVolumeButtonTexture.minFilter = THREE.LinearFilter;
const volumeButtonMaterial = new THREE.MeshBasicMaterial({ map: vrVolumeButtonTexture, transparent: true, opacity: 0, depthWrite: false });
const volumeButtonWorldSize = FIGMA_VOLUME_BUTTON_SIZE_PX * SCALE_FACTOR;
const volumeButtonGeometry = new THREE.PlaneGeometry(volumeButtonWorldSize, volumeButtonWorldSize);
vrVolumeButtonMesh = new THREE.Mesh(volumeButtonGeometry, volumeButtonMaterial);
vrVolumeButtonMesh.name = "vrVolumeButton";
vrVolumeButtonMesh.renderOrder = 3;
const figmaVolumeButtonCenterX = FIGMA_VOLUME_BUTTON_X_PX;
const figmaVolumeButtonCenterY = FIGMA_VOLUME_BUTTON_Y_PX;
const worldVolumeButtonOffsetX = (figmaVolumeButtonCenterX - FIGMA_PANEL_WIDTH_PX / 2) * SCALE_FACTOR;
const worldVolumeButtonOffsetY = -(figmaVolumeButtonCenterY - FIGMA_PANEL_HEIGHT_PX / 2) * SCALE_FACTOR;
vrVolumeButtonMesh.position.set(worldVolumeButtonOffsetX, worldVolumeButtonOffsetY, 0.02);
vrControlPanel.add(vrVolumeButtonMesh);
uiElements.push(vrVolumeButtonMesh);
vrControlPanel.visible = false;
panelOpacity = 0;
panelTargetOpacity = 0;
@@ -561,40 +352,24 @@ function init() {
function updateVRPlayPauseButtonIcon() {
if (!vrPlayPauseButtonContext || !vrPlayPauseButtonTexture || !video) {
if (!video) {
return;
}
const ctx = vrPlayPauseButtonContext;
const canvas = vrPlayPauseButtonCanvas;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const iconSize = 82;
const iconOffset = (canvas.width - iconSize) / 2;
drawLucideIcon(ctx, video.paused || video.ended ? 'play' : 'pause', iconOffset, iconOffset, iconSize, '#ffffff', 2);
vrPlayPauseButtonTexture.needsUpdate = true;
updateVrPlayPauseButtonIcon(vrPanel, video.paused || video.ended);
}
function updateVRVolumeButtonIcon() {
if (!vrVolumeButtonContext || !vrVolumeButtonTexture || !video) {
if (!video) {
return;
}
const ctx = vrVolumeButtonContext;
const canvas = vrVolumeButtonCanvas;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const iconSize = 82;
const iconOffset = (canvas.width - iconSize) / 2;
drawLucideIcon(ctx, video.muted || video.volume === 0 ? 'volume-x' : 'volume-2', iconOffset, iconOffset, iconSize, '#ffffff', 2);
vrVolumeButtonTexture.needsUpdate = true;
updateVrVolumeButtonIcon(vrPanel, video.muted || video.volume === 0);
}
function updateSeekBarAppearance() {
if (video && isFinite(video.duration) && video.duration > 0 && seekBarProgressMesh) {
const progress = video.currentTime / video.duration;
seekBarProgressMesh.scale.x = Math.max(0.0001, progress);
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2 + (WORLD_SEEK_BAR_WIDTH * progress) / 2;
} else if (seekBarProgressMesh) {
seekBarProgressMesh.scale.x = 0.0001;
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
}
const progress = video && isFinite(video.duration) && video.duration > 0
? video.currentTime / video.duration
: null;
updateVrSeekBarAppearance(vrPanel, progress);
}
function animatePanelFade(timestamp) {
@@ -623,11 +398,7 @@ function animatePanelFade(timestamp) {
isPanelFading = false;
}
if (opacityChanged) {
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = panelOpacity;
}
});
setVrPanelOpacity(vrPanel, panelOpacity);
}
if (isPanelFading) requestAnimationFrame(animatePanelFade);
}
@@ -710,113 +481,12 @@ function onWindowResize() {
}
}
// 2D Camera Control Functions
function updateCameraRotation() {
if (!camera2D) return;
// Apply momentum
cameraRotation.yaw += cameraVelocity.yaw;
cameraRotation.pitch += cameraVelocity.pitch;
// Constrain pitch (vertical rotation)
cameraRotation.pitch = Math.max(-MAX_PITCH, Math.min(MAX_PITCH, cameraRotation.pitch));
// Constrain yaw (horizontal rotation) to VR180 content boundaries
cameraRotation.yaw = Math.max(-MAX_YAW, Math.min(MAX_YAW, cameraRotation.yaw));
// Apply damping to velocity
cameraVelocity.yaw *= MOMENTUM_DAMPING;
cameraVelocity.pitch *= MOMENTUM_DAMPING;
// Stop very small velocities
if (Math.abs(cameraVelocity.yaw) < 0.001) cameraVelocity.yaw = 0;
if (Math.abs(cameraVelocity.pitch) < 0.001) cameraVelocity.pitch = 0;
// Apply rotation to camera
camera2D.rotation.set(cameraRotation.pitch, cameraRotation.yaw, 0);
}
// Mouse Controls
function onMouseDown(event) {
if (!is2DMode) return;
isDragging = true;
lastMouseX = event.clientX;
lastMouseY = event.clientY;
cameraVelocity.yaw = 0;
cameraVelocity.pitch = 0;
// Hide controls when dragging starts
hide2DControlPanel();
}
function onMouseMove(event) {
if (!is2DMode || !isDragging) return;
const deltaX = event.clientX - lastMouseX;
const deltaY = event.clientY - lastMouseY;
cameraVelocity.yaw = deltaX * MOUSE_SENSITIVITY;
cameraVelocity.pitch = deltaY * MOUSE_SENSITIVITY;
lastMouseX = event.clientX;
lastMouseY = event.clientY;
}
function onMouseUp(event) {
if (!is2DMode) return;
isDragging = false;
// Show controls when dragging ends
show2DControlPanel();
}
// Touch Controls
function onTouchStart(event) {
if (!is2DMode) return;
event.preventDefault();
if (event.touches.length === 1) {
isDragging = true;
lastTouchX = event.touches[0].clientX;
lastTouchY = event.touches[0].clientY;
cameraVelocity.yaw = 0;
cameraVelocity.pitch = 0;
// Hide controls when dragging starts
hide2DControlPanel();
}
}
function onTouchMove(event) {
if (!is2DMode || !isDragging) return;
event.preventDefault();
if (event.touches.length === 1) {
const deltaX = event.touches[0].clientX - lastTouchX;
const deltaY = event.touches[0].clientY - lastTouchY;
cameraVelocity.yaw = deltaX * TOUCH_SENSITIVITY;
cameraVelocity.pitch = deltaY * TOUCH_SENSITIVITY;
lastTouchX = event.touches[0].clientX;
lastTouchY = event.touches[0].clientY;
}
}
function onTouchEnd(event) {
if (!is2DMode) return;
event.preventDefault();
isDragging = false;
// Show controls when dragging ends
show2DControlPanel();
}
// 2D Render Loop
function render2D() {
if (!is2DMode) return;
if (projectionMode === 'vr180') {
updateCameraRotation();
fallbackCameraControls?.updateCameraRotation();
} else if (camera2D) {
camera2D.rotation.set(0, 0, 0);
}
@@ -850,10 +520,7 @@ function init2DControlPanel() {
// Set initial video title
if (videoTitle && video) {
const title = video.getAttribute('title') ||
video.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
"Demo Video";
videoTitle.textContent = title;
videoTitle.textContent = getVideoTitle();
}
// Add event listeners for 2D controls
@@ -926,30 +593,6 @@ function hide2DControlPanel() {
isControlPanelVisible = false;
}
function onCanvasMouseMove() {
if (is2DMode && !isDragging) {
show2DControlPanel();
}
}
function onCanvasTouchStart() {
if (is2DMode) {
show2DControlPanel();
}
}
function on2DMouseMove() {
if (is2DMode) {
show2DControlPanel();
}
}
function on2DTouchStart() {
if (is2DMode && !isDragging) {
show2DControlPanel();
}
}
function update2DControlPanel() {
if (!is2DMode || !video) return;
@@ -1062,20 +705,6 @@ function position2DControlPanel() {
controlPanel.style.zIndex = '1000'; // Ensure it's above the canvas
}
function formatTime(seconds) {
if (!isFinite(seconds)) return '00:00:00';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
}
function hidePlayButton() {
if (playBtn) {
playBtn.classList.add('hidden');
@@ -1140,9 +769,7 @@ function resetToOriginalState() {
hide2DControlPanel();
// Reset camera rotation
cameraRotation = { yaw: 0, pitch: 0 };
cameraVelocity = { yaw: 0, pitch: 0 };
isDragging = false;
fallbackCameraControls?.reset();
positionPlaneForPresentation(false);
// Hide WebGL canvas and show video element
@@ -1212,9 +839,7 @@ function onSelectStartVR(event) {
showPanel();
} else if (firstIntersected.name === "seekBarHitArea" && video && isFinite(video.duration)) {
showPanel();
const localPoint = seekBarTrackMesh.worldToLocal(intersectionPoint.clone());
const normalizedPosition = (localPoint.x + WORLD_SEEK_BAR_WIDTH / 2) / WORLD_SEEK_BAR_WIDTH;
const newTime = Math.max(0, Math.min(1, normalizedPosition)) * video.duration;
const newTime = getSeekProgressFromIntersection(vrPanel, intersectionPoint) * video.duration;
video.currentTime = newTime;
updateSeekBarAppearance();
} else if (firstIntersected.name === "vrControlPanelBackground" || firstIntersected === activeContentMesh) {
@@ -1322,39 +947,13 @@ function start2DMode() {
}
function add2DEventListeners() {
// Canvas-specific mouse movement for showing controls
renderer.domElement.addEventListener('mousemove', onCanvasMouseMove);
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 });
}
fallbackCameraControls?.addEventListeners(renderer.domElement, projectionMode);
}
function remove2DEventListeners() {
if (!renderer || !renderer.domElement) return;
renderer.domElement.removeEventListener('mousemove', onCanvasMouseMove);
// Mouse events
renderer.domElement.removeEventListener('mousedown', onMouseDown);
renderer.domElement.removeEventListener('mousemove', onMouseMove);
renderer.domElement.removeEventListener('mouseup', onMouseUp);
// Touch events
renderer.domElement.removeEventListener('touchstart', onTouchStart);
renderer.domElement.removeEventListener('touchmove', onTouchMove);
renderer.domElement.removeEventListener('touchend', onTouchEnd);
renderer.domElement.removeEventListener('touchstart', onCanvasTouchStart);
fallbackCameraControls?.removeEventListeners(renderer.domElement);
// Fullscreen events
document.removeEventListener('fullscreenchange', onFullscreenChange);
@@ -1386,12 +985,7 @@ async function actualSessionToggle() {
clearTimeout(panelHideTimeout);
panelTargetOpacity = 0;
panelOpacity = 0;
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = 0;
}
});
vrControlPanel.visible = false;
hideVrPanelImmediately(vrPanel);
isPanelFading = false;
}
sessionToClose.end().catch(err => {
@@ -1440,14 +1034,9 @@ async function actualSessionToggle() {
updateVRPlayPauseButtonIcon();
updateVRVolumeButtonIcon();
if (vrControlPanel) {
vrControlPanel.visible = false;
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
clearTimeout(panelHideTimeout);
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = 0;
}
});
hideVrPanelImmediately(vrPanel);
}
await renderer.xr.setSession(xrSession);
@@ -1465,8 +1054,9 @@ async function actualSessionToggle() {
if (sphereMaterial) { sphereMaterial.map = null; sphereMaterial.needsUpdate = true; }
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
if (vrControlPanel) {
vrControlPanel.visible = false; panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
panelOpacity = 0; panelTargetOpacity = 0; isPanelFading = false;
clearTimeout(panelHideTimeout);
hideVrPanelImmediately(vrPanel);
}
if (xrSession) {
xrSession.removeEventListener('end', onVRSessionEnd);
@@ -1517,12 +1107,7 @@ function onVRSessionEnd(event) {
clearTimeout(panelHideTimeout);
isPanelFading = false;
panelOpacity = 0; panelTargetOpacity = 0;
vrControlPanel.children.forEach(child => {
if (child.material && child.material.hasOwnProperty('opacity')) {
child.material.opacity = 0;
}
});
vrControlPanel.visible = false;
hideVrPanelImmediately(vrPanel);
}
if (endedSession && typeof endedSession.removeEventListener === 'function') {