1
0

Implement proper mute/unmute button functionality for 2D controls

- Add CSS classes (.muted/.unmuted) for mute button state management
- Fix button logic to show action that will be taken (standard UI pattern):
  - When audio playing (unmuted): shows mute icon (click to mute)
  - When audio muted: shows unmute icon (click to unmute)
- Add proper hover states for both mute and unmute icons
- Initialize button state correctly on video load
- Smooth 0.15s transitions between all states
- Follows standard media player UI conventions
This commit is contained in:
Michael Verdi
2025-07-31 22:01:20 -05:00
parent da4ca420f6
commit 83489ea969
2 changed files with 84 additions and 7 deletions

View File

@@ -162,6 +162,22 @@
background-image: url(mute-hover.png); background-image: url(mute-hover.png);
} }
#mute.muted {
background-image: url(mute.png);
}
#mute.muted:hover {
background-image: url(mute-hover.png);
}
#mute.unmuted {
background-image: url(unmute.png);
}
#mute.unmuted:hover {
background-image: url(unmute-hover.png);
}
#nav { #nav {
grid-area: nav; grid-area: nav;
display: grid; display: grid;

View File

@@ -13,7 +13,7 @@ let controlPanel, videoTitle, currentTimeDisplay, totalTimeDisplay, progressBar,
let fullscreenBtn, backBtn, play2Btn, forwardBtn, muteBtn; let fullscreenBtn, backBtn, play2Btn, forwardBtn, muteBtn;
let controlPanelTimeout; let controlPanelTimeout;
let isControlPanelVisible = false; let isControlPanelVisible = false;
const CONTROL_PANEL_HIDE_DELAY = 5000; // 5 seconds const CONTROL_PANEL_HIDE_DELAY = 3000; // 3 seconds
let isXrLoopActive = false; let isXrLoopActive = false;
let is2DMode = false; let is2DMode = false;
@@ -555,6 +555,7 @@ function init() {
updateVRPlayPauseButtonIcon(); updateVRPlayPauseButtonIcon();
updateVRVolumeButtonIcon(); updateVRVolumeButtonIcon();
update2DControlPanel(); update2DControlPanel();
update2DMuteButton();
}; };
video.oncanplaythrough = () => { video.oncanplaythrough = () => {
if (playBtn && video.readyState >= video.HAVE_FUTURE_DATA) { if (playBtn && video.readyState >= video.HAVE_FUTURE_DATA) {
@@ -795,6 +796,9 @@ function onMouseDown(event) {
lastMouseY = event.clientY; lastMouseY = event.clientY;
cameraVelocity.yaw = 0; cameraVelocity.yaw = 0;
cameraVelocity.pitch = 0; cameraVelocity.pitch = 0;
// Hide controls when dragging starts
hide2DControlPanel();
} }
function onMouseMove(event) { function onMouseMove(event) {
@@ -813,6 +817,9 @@ function onMouseMove(event) {
function onMouseUp(event) { function onMouseUp(event) {
if (!is2DMode) return; if (!is2DMode) return;
isDragging = false; isDragging = false;
// Show controls when dragging ends
show2DControlPanel();
} }
// Touch Controls // Touch Controls
@@ -825,6 +832,9 @@ function onTouchStart(event) {
lastTouchY = event.touches[0].clientY; lastTouchY = event.touches[0].clientY;
cameraVelocity.yaw = 0; cameraVelocity.yaw = 0;
cameraVelocity.pitch = 0; cameraVelocity.pitch = 0;
// Hide controls when dragging starts
hide2DControlPanel();
} }
} }
@@ -848,6 +858,9 @@ function onTouchEnd(event) {
if (!is2DMode) return; if (!is2DMode) return;
event.preventDefault(); event.preventDefault();
isDragging = false; isDragging = false;
// Show controls when dragging ends
show2DControlPanel();
} }
// 2D Render Loop // 2D Render Loop
@@ -942,8 +955,7 @@ function init2DControlPanel() {
}); });
} }
// Add mouse movement listener for 2D mode // Mouse movement listener will be added to canvas in start2DMode
document.addEventListener('mousemove', on2DMouseMove);
document.addEventListener('touchstart', on2DTouchStart); document.addEventListener('touchstart', on2DTouchStart);
} }
@@ -965,6 +977,12 @@ function hide2DControlPanel() {
isControlPanelVisible = false; isControlPanelVisible = false;
} }
function onCanvasMouseMove() {
if (is2DMode && !isDragging) {
show2DControlPanel();
}
}
function on2DMouseMove() { function on2DMouseMove() {
if (is2DMode) { if (is2DMode) {
show2DControlPanel(); show2DControlPanel();
@@ -1011,8 +1029,15 @@ function update2DPlayPauseButton() {
function update2DMuteButton() { function update2DMuteButton() {
if (!is2DMode || !muteBtn || !video) return; if (!is2DMode || !muteBtn || !video) return;
// The CSS will handle the visual state based on the muted property if (video.muted) {
// We could add classes here if needed for different mute states // Video is muted, show unmute icon (user can click to unmute)
muteBtn.classList.remove('muted');
muteBtn.classList.add('unmuted');
} else {
// Video is unmuted, show mute icon (user can click to mute)
muteBtn.classList.remove('unmuted');
muteBtn.classList.add('muted');
}
} }
function toggle2DFullscreen() { function toggle2DFullscreen() {
@@ -1034,6 +1059,24 @@ function toggle2DFullscreen() {
} }
} }
function handle2DVideoEnd() {
if (!is2DMode || !video) return;
// Keep video at last frame (don't reset currentTime)
// Video is already paused by onVideoEnded()
// Show control panel and keep it visible (no auto-hide timeout)
if (controlPanel) {
clearTimeout(controlPanelTimeout);
controlPanel.classList.add('visible');
isControlPanelVisible = true;
// Don't set timeout - panel stays visible until user interacts
}
// Update play button to show replay state
update2DPlayPauseButton();
}
function position2DControlPanel() { function position2DControlPanel() {
if (!is2DMode || !controlPanel || !renderer) return; if (!is2DMode || !controlPanel || !renderer) return;
@@ -1089,10 +1132,20 @@ function enableNativeControls() {
function togglePlayPause() { function togglePlayPause() {
if (!video || !video.currentSrc) return; if (!video || !video.currentSrc) return;
if (video.paused || video.ended) { if (video.paused || video.ended) {
// If video has ended in 2D mode, restart from beginning
if (video.ended && is2DMode) {
video.currentTime = 0;
}
if (video.readyState >= video.HAVE_ENOUGH_DATA || video.currentSrc) { if (video.readyState >= video.HAVE_ENOUGH_DATA || video.currentSrc) {
const playPromise = video.play(); const playPromise = video.play();
if (playPromise !== undefined) { if (playPromise !== undefined) {
playPromise.catch(err => console.error("Error during video.play():", err)); playPromise.then(() => {
// Resume normal control panel auto-hide behavior after restart
if (is2DMode && video.ended === false) {
show2DControlPanel();
}
}).catch(err => console.error("Error during video.play():", err));
} else { } else {
console.error("video.play() did not return a promise."); console.error("video.play() did not return a promise.");
} }
@@ -1147,7 +1200,9 @@ function resetToOriginalState() {
function onVideoEnded() { function onVideoEnded() {
if (video && !video.paused) video.pause(); if (video && !video.paused) video.pause();
if (xrSession && renderer && renderer.xr.isPresenting) { if (xrSession && renderer && renderer.xr.isPresenting) {
// VR mode - exit VR and reset to original state
actualSessionToggle().catch(err => { actualSessionToggle().catch(err => {
console.error("Error during automatic VR exit on video end:", err); console.error("Error during automatic VR exit on video end:", err);
// Fallback cleanup if actualSessionToggle fails or doesn't fully clean up // Fallback cleanup if actualSessionToggle fails or doesn't fully clean up
@@ -1160,8 +1215,11 @@ function onVideoEnded() {
onVRSessionEnd({session: null}); // Call with null session if already gone onVRSessionEnd({session: null}); // Call with null session if already gone
} }
}); });
} else if (is2DMode) {
// 2D mode - stay on last frame with controls visible
handle2DVideoEnd();
} else { } else {
// If not in VR, still reset to original state when video ends // Regular mode - reset to original state
resetToOriginalState(); resetToOriginalState();
} }
} }
@@ -1298,6 +1356,9 @@ function add2DEventListeners() {
renderer.domElement.addEventListener('mousemove', onMouseMove); renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('mouseup', onMouseUp); renderer.domElement.addEventListener('mouseup', onMouseUp);
// Canvas-specific mouse movement for showing controls
renderer.domElement.addEventListener('mousemove', onCanvasMouseMove);
// Touch events // Touch events
renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false }); renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false }); renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });