1
0

Implement 2D video controls for non-VR devices

- Add complete 2D control panel with HTML structure and CSS styling
- Implement JavaScript functionality for all control buttons:
  - Fullscreen toggle for immersive 16:9 video experience
  - Play/pause with dynamic icon switching
  - Back/forward 15-second skip controls
  - Mute/unmute toggle
  - Click-to-seek progress bar with real-time updates
- Add auto-hide behavior (5-second timeout) with mouse/touch activation
- Integrate with existing 2D mode - shows only when VR not supported
- Include all button PNG assets (normal and hover states)
- Responsive design for mobile devices
- Professional styling matching design specifications
This commit is contained in:
Michael Verdi
2025-07-31 18:26:49 -05:00
parent 14e90f7428
commit dbaefeb337
17 changed files with 439 additions and 1 deletions

BIN
back-hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
forward-hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
forward.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
fullscreen-hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
fullscreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

View File

@@ -31,8 +31,29 @@
<source src="sbs-video.mp4" type="video/mp4">
</video>
<button id="playBtn" aria-label="Play video">
<img src="play.png" alt="Play">
<img src="play.svg" alt="Play">
</button>
<div id="panel">
<div id="status">
<p id="video-title">Title</p>
<div id="progress">
<p id="current-time">00:00:00</p>
<div id="bar">
<div id="played"></div>
</div>
<p id="total-time">00:00:00</p>
</div>
</div>
<div id="controls">
<button id="fullscreen"></button>
<div id="nav">
<button id="back"></button>
<button id="play2"></button>
<button id="forward"></button>
</div>
<button id="mute"></button>
</div>
</div>
</div>
</main>
<script type="module" src="vr180-player.js"></script>

BIN
mute-hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
mute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
pause-hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

BIN
play2-hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
play2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
unmute-hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
unmute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

View File

@@ -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;
}
}

View File

@@ -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();
}