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
BIN
back-hover.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
forward-hover.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
forward.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
fullscreen-hover.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
fullscreen.png
Normal file
|
After Width: | Height: | Size: 796 B |
23
index.html
@@ -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
|
After Width: | Height: | Size: 4.3 KiB |
BIN
pause-hover.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
play2-hover.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
unmute-hover.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
unmute.png
Normal file
|
After Width: | Height: | Size: 1014 B |
211
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;
|
||||
}
|
||||
}
|
||||
|
||||
206
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();
|
||||
}
|
||||
|
||||