diff --git a/back-hover.png b/back-hover.png new file mode 100644 index 0000000..b1bc879 Binary files /dev/null and b/back-hover.png differ diff --git a/back.png b/back.png new file mode 100644 index 0000000..f52a814 Binary files /dev/null and b/back.png differ diff --git a/forward-hover.png b/forward-hover.png new file mode 100644 index 0000000..f63a4c4 Binary files /dev/null and b/forward-hover.png differ diff --git a/forward.png b/forward.png new file mode 100644 index 0000000..682682e Binary files /dev/null and b/forward.png differ diff --git a/fullscreen-hover.png b/fullscreen-hover.png new file mode 100644 index 0000000..582407b Binary files /dev/null and b/fullscreen-hover.png differ diff --git a/fullscreen.png b/fullscreen.png new file mode 100644 index 0000000..a4ce126 Binary files /dev/null and b/fullscreen.png differ diff --git a/index.html b/index.html index 8e60042..de00596 100644 --- a/index.html +++ b/index.html @@ -31,8 +31,29 @@ +
+
+

Title

+
+

00:00:00

+
+
+
+

00:00:00

+
+
+
+ + + +
+
diff --git a/mute-hover.png b/mute-hover.png new file mode 100644 index 0000000..480d6dd Binary files /dev/null and b/mute-hover.png differ diff --git a/mute.png b/mute.png new file mode 100644 index 0000000..6bd5db0 Binary files /dev/null and b/mute.png differ diff --git a/pause-hover.png b/pause-hover.png new file mode 100644 index 0000000..1197a2f Binary files /dev/null and b/pause-hover.png differ diff --git a/pause.png b/pause.png new file mode 100644 index 0000000..5c98f69 Binary files /dev/null and b/pause.png differ diff --git a/play2-hover.png b/play2-hover.png new file mode 100644 index 0000000..15fa59c Binary files /dev/null and b/play2-hover.png differ diff --git a/play2.png b/play2.png new file mode 100644 index 0000000..1ad8a8f Binary files /dev/null and b/play2.png differ diff --git a/unmute-hover.png b/unmute-hover.png new file mode 100644 index 0000000..eeb6c6c Binary files /dev/null and b/unmute-hover.png differ diff --git a/unmute.png b/unmute.png new file mode 100644 index 0000000..9852cb6 Binary files /dev/null and b/unmute.png differ diff --git a/vr180-player.css b/vr180-player.css index 518c7c5..39dffe4 100644 --- a/vr180-player.css +++ b/vr180-player.css @@ -51,3 +51,214 @@ height: 100px; } } + +/* 2D Video Controls Panel */ +#panel { + position: absolute; + bottom: 10%; + left: 50%; + transform: translateX(-50%); + max-width: 1000px; + padding: 20px; + 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"; + z-index: 100; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; + pointer-events: none; +} + +#panel.visible { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +#panel img { + width: 100%; + height: auto; +} + +#status { + margin: 0 12px 12px 12px; +} + +#video-title { + text-align: center; + margin: 0 0 16px 0; + font-size: 1rem; + font-weight: 500; +} + +#current-time, +#total-time { + margin: 0; + font-size: 0.875rem; + font-variant-numeric: tabular-nums; +} + +#progress { + display: grid; + grid-template-columns: min-content 1fr min-content; + grid-gap: 8px; + align-items: center; +} + +#bar { + width: 100%; + height: 4px; + border-radius: 2px; + background: #666; + cursor: pointer; + position: relative; +} + +#played { + border-radius: 2px; + background: #fff; + height: 4px; + width: 0%; + transition: width 0.1s ease; +} + +#controls { + display: grid; + grid-template-areas: "full lflex nav rflex mute"; + grid-template-columns: 44px 1fr 156px 1fr 44px; + height: 44px; +} + +#panel button { + all: unset; + cursor: pointer; + border-radius: 4px; + transition: opacity 0.15s ease-in-out; +} + +#panel button:hover { + opacity: 0.8; +} + +#fullscreen { + grid-area: full; + width: 44px; + height: 44px; + background-image: url(fullscreen.png); + background-size: 44px 44px; + background-repeat: no-repeat; + background-position: center; + transition: background-image 0.15s ease-in-out; +} + +#fullscreen:hover { + background-image: url(fullscreen-hover.png); +} + +#mute { + grid-area: mute; + width: 44px; + height: 44px; + background-image: url(mute.png); + background-size: 44px 44px; + background-repeat: no-repeat; + background-position: center; + transition: background-image 0.15s ease-in-out; +} + +#mute:hover { + background-image: url(mute-hover.png); +} + +#nav { + grid-area: nav; + display: grid; + grid-template-columns: 44px 44px 44px; + grid-gap: 12px; + height: 44px; +} + +#back { + width: 44px; + height: 44px; + background-image: url(back.png); + background-size: 44px 44px; + background-repeat: no-repeat; + background-position: center; + transition: background-image 0.15s ease-in-out; +} + +#back:hover { + background-image: url(back-hover.png); +} + +#play2 { + width: 44px; + height: 44px; + background-image: url(play2.png); + background-size: 44px 44px; + background-repeat: no-repeat; + background-position: center; + transition: background-image 0.15s ease-in-out; +} + +#play2:hover { + background-image: url(play2-hover.png); +} + +#play2.paused { + background-image: url(play2.png); +} + +#play2.paused:hover { + background-image: url(play2-hover.png); +} + +#play2.playing { + background-image: url(pause2.png); +} + +#play2.playing:hover { + background-image: url(pause2-hover.png); +} + +#forward { + width: 44px; + height: 44px; + background-image: url(forward.png); + background-size: 44px 44px; + background-repeat: no-repeat; + background-position: center; + transition: background-image 0.15s ease-in-out; +} + +#forward:hover { + background-image: url(forward-hover.png); +} + +/* Responsive adjustments for 2D controls */ +@media (max-width: 600px) { + #panel { + max-width: 90%; + padding: 16px; + } + + #controls { + grid-template-columns: 36px 1fr 132px 1fr 36px; + height: 36px; + } + + #fullscreen, #mute, #back, #play2, #forward { + width: 36px; + height: 36px; + background-size: 36px 36px; + } + + #nav { + grid-template-columns: 36px 36px 36px; + grid-gap: 8px; + height: 36px; + } +} diff --git a/vr180-player.js b/vr180-player.js index 329fb27..7628bb9 100644 --- a/vr180-player.js +++ b/vr180-player.js @@ -8,6 +8,13 @@ const tempMatrix = new THREE.Matrix4(); 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 = 5000; // 5 seconds + let isXrLoopActive = false; let is2DMode = false; let vrControlPanel; @@ -547,6 +554,7 @@ function init() { updateSeekBarAppearance(); updateVRPlayPauseButtonIcon(); updateVRVolumeButtonIcon(); + update2DControlPanel(); }; video.oncanplaythrough = () => { if (playBtn && video.readyState >= video.HAVE_FUTURE_DATA) { @@ -557,13 +565,16 @@ function init() { video.ontimeupdate = () => { if (isFinite(video.duration)) { updateSeekBarAppearance(); + update2DControlPanel(); } }; video.onplaying = () => { updateVRPlayPauseButtonIcon(); + update2DPlayPauseButton(); }; video.onpause = () => { updateVRPlayPauseButtonIcon(); + update2DPlayPauseButton(); }; video.onerror = (e) => { const videoError = video.error; @@ -573,7 +584,11 @@ function init() { }; video.addEventListener('ended', onVideoEnded); video.addEventListener('volumechange', updateVRVolumeButtonIcon); + video.addEventListener('volumechange', update2DMuteButton); } + + // Initialize 2D control panel + init2DControlPanel(); } catch (e) { console.error("INIT_ERROR (Phase 3 - Event Listeners):", e); } @@ -845,6 +860,191 @@ function render2D() { requestAnimationFrame(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'); + + if (!controlPanel) { + console.error("2D Control panel not found"); + return; + } + + // 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; + } + + // 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); + } + 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(); + }); + } + + // Add mouse movement listener for 2D mode + document.addEventListener('mousemove', on2DMouseMove); + document.addEventListener('touchstart', on2DTouchStart); +} + +function show2DControlPanel() { + if (!is2DMode || !controlPanel) return; + + clearTimeout(controlPanelTimeout); + controlPanel.classList.add('visible'); + isControlPanelVisible = true; + + controlPanelTimeout = setTimeout(hide2DControlPanel, CONTROL_PANEL_HIDE_DELAY); +} + +function hide2DControlPanel() { + if (!controlPanel) return; + + clearTimeout(controlPanelTimeout); + controlPanel.classList.remove('visible'); + isControlPanelVisible = false; +} + +function on2DMouseMove() { + if (is2DMode) { + show2DControlPanel(); + } +} + +function on2DTouchStart() { + if (is2DMode) { + show2DControlPanel(); + } +} + +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}%`; + } +} + +function update2DPlayPauseButton() { + if (!is2DMode || !play2Btn || !video) return; + + if (video.paused || video.ended) { + play2Btn.classList.remove('playing'); + play2Btn.classList.add('paused'); + } else { + play2Btn.classList.remove('paused'); + play2Btn.classList.add('playing'); + } +} + +function update2DMuteButton() { + if (!is2DMode || !muteBtn || !video) return; + + // The CSS will handle the visual state based on the muted property + // We could add classes here if needed for different mute states +} + +function toggle2DFullscreen() { + if (!document.fullscreenElement) { + // Enter fullscreen + const container = document.getElementById('vr-container'); + 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); + }); + } + } +} + +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'); @@ -895,6 +1095,9 @@ function resetToOriginalState() { is2DMode = false; remove2DEventListeners(); + // Hide 2D control panel + hide2DControlPanel(); + // Reset camera rotation cameraRotation = { yaw: 0, pitch: 0 }; cameraVelocity = { yaw: 0, pitch: 0 }; @@ -1050,6 +1253,9 @@ function start2DMode() { // Add event listeners for 2D controls add2DEventListeners(); + // Show 2D control panel + show2DControlPanel(); + // Start 2D render loop render2D(); }