commit 8bff327fc15be281e21682b1a0704ace203fabda
parent 05cb63fecd2c487909e4535e30aa849406b49a71
Author: Hunter
Date:   Tue, 28 Oct 2025 16:16:05 -0400

add scrolling marquee on text overflow

Diffstat:
Mscript.js | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mstyles.css | 8+++++++-
2 files changed, 198 insertions(+), 33 deletions(-)

diff --git a/script.js b/script.js @@ -31,7 +31,7 @@ fetch('tracks.json') songs = data; if (songs.length > 0) { playerReady = true; - updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} - ${songs[0].title}`); + updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} – ${songs[0].title}`); renderPlaylist(); // Pre-cache resources first, then songs preloadResources().then(() => { @@ -50,9 +50,21 @@ fetch('tracks.json') audio.addEventListener('play', () => { startProgressBar(); const song = songs[currentSongIndex]; - updateCurrentSongDisplay(song.looping ? - `Looping: ${song.artist} - ${song.title}` : - `Now playing: ${song.artist} - ${song.title}`); + const songText = `${song.artist} – ${song.title}`; + + // Always update display to ensure we remove "Ready to play:" prefix + const currentText = currentSongDisplay.querySelector('span')?.textContent || ''; + + // Check if it's a different song (not just play/pause of same song) + const isNewSong = !currentText.includes(songText); + const hasReadyToPlay = currentText.includes('Ready to play:'); + + if (isNewSong || hasReadyToPlay) { + updateCurrentSongDisplay(songText); + } else { + // Same song, just resume the marquee + resumeMarquee(); + } // Update media session metadata if ('mediaSession' in navigator) { @@ -65,11 +77,7 @@ audio.addEventListener('play', () => { audio.addEventListener('pause', () => { stopProgressBar(); - // Only show "Paused" if the user actually paused (not during track transitions or seeking) - if (!audio.ended && !isSeeking) { - const song = songs[currentSongIndex]; - updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`); - } + pauseMarquee(); }); audio.addEventListener('ended', () => { @@ -144,16 +152,6 @@ function toggleLooping(index) { // Toggle looping songs[index].looping = !(songs[index].looping || false); renderPlaylist(); - - const song = songs[index]; - // Update the display text based on current state - if (isPlaying) { - updateCurrentSongDisplay(song.looping ? - `Looping: ${song.artist} - ${song.title}` : - `Now playing: ${song.artist} - ${song.title}`); - } else { - updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`); - } } else { playSong(index); } @@ -188,8 +186,178 @@ function playSong(index) { function updateCurrentSongDisplay(text) { currentSongDisplay.innerHTML = `<span>${text}</span>`; + // Initialize marquee effect after a brief delay to ensure DOM is updated + setTimeout(() => setupMarquee(), 50); } +// Marquee state +let marqueeAnimating = false; +let marqueePaused = false; +let marqueeTimeoutId = null; +let marqueeOriginalText = ''; +let marqueeDuration = 0; +let marqueeHalfWidth = 0; +let marqueeTransitionEndHandler = null; +let marqueeCurrentTransform = 'translateX(0)'; + +function setupMarquee() { + const container = currentSongDisplay; + const textSpan = container.querySelector('span'); + + if (!textSpan) return; + + // Clear any existing animation + if (marqueeTimeoutId) { + clearTimeout(marqueeTimeoutId); + marqueeTimeoutId = null; + } + if (marqueeTransitionEndHandler) { + textSpan.removeEventListener('transitionend', marqueeTransitionEndHandler); + marqueeTransitionEndHandler = null; + } + marqueeAnimating = false; + marqueePaused = false; + + // Reset styles + textSpan.style.transition = 'none'; + textSpan.style.transform = 'translateX(0)'; + marqueeCurrentTransform = 'translateX(0)'; + container.classList.remove('no-overflow'); + + // Store original text + marqueeOriginalText = textSpan.textContent; + + // Force reflow + void textSpan.offsetWidth; + + // Check if text overflows + const containerWidth = container.offsetWidth - 30; // Account for padding + const textWidth = textSpan.scrollWidth; + const overflows = textWidth > containerWidth; + + if (!overflows) { + // No overflow - show ellipsis behavior + container.classList.add('no-overflow'); + container.classList.remove('marquee-active'); + return; + } + + // Text overflows - setup marquee + marqueeAnimating = true; + container.classList.add('marquee-active'); + + // Add spacing and duplicate text for seamless loop + // Using non-breaking spaces (\u00A0) so they don't collapse in HTML + const spacing = '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0'; // 12 non-breaking spaces + textSpan.textContent = marqueeOriginalText + spacing + marqueeOriginalText + spacing; + + // Calculate animation parameters + const fullWidth = textSpan.scrollWidth; + marqueeHalfWidth = fullWidth / 2; + marqueeDuration = (marqueeHalfWidth / 50) * 1000; // Adjust speed here (pixels per second) + + // Start animation after a brief delay (only if not paused) + // Don't start animation if already paused + if (!marqueePaused) { + marqueeTimeoutId = setTimeout(() => { + if (marqueePaused) return; + + textSpan.style.transition = `transform ${marqueeDuration}ms linear`; + textSpan.style.transform = `translateX(-${marqueeHalfWidth}px)`; + marqueeCurrentTransform = `translateX(-${marqueeHalfWidth}px)`; + + // Reset and loop + marqueeTransitionEndHandler = () => { + if (!marqueeAnimating || marqueePaused) return; + + textSpan.style.transition = 'none'; + textSpan.style.transform = 'translateX(0)'; + marqueeCurrentTransform = 'translateX(0)'; + + setTimeout(() => { + if (!marqueeAnimating || marqueePaused) return; + textSpan.style.transition = `transform ${marqueeDuration}ms linear`; + textSpan.style.transform = `translateX(-${marqueeHalfWidth}px)`; + marqueeCurrentTransform = `translateX(-${marqueeHalfWidth}px)`; + }, 50); + }; + + textSpan.addEventListener('transitionend', marqueeTransitionEndHandler); + }, 1000); // Initial delay before starting scroll + } +} + +function pauseMarquee() { + marqueePaused = true; + const textSpan = currentSongDisplay.querySelector('span'); + if (!textSpan || !marqueeAnimating) return; + + // Capture current transform position + const computedStyle = window.getComputedStyle(textSpan); + const currentTransform = computedStyle.transform; + marqueeCurrentTransform = currentTransform; + + // Freeze at current position + textSpan.style.transition = 'none'; + textSpan.style.transform = currentTransform; + + // Clear any pending timeout + if (marqueeTimeoutId) { + clearTimeout(marqueeTimeoutId); + marqueeTimeoutId = null; + } +} + +function resumeMarquee() { + if (!marqueeAnimating) return; + marqueePaused = false; + + const textSpan = currentSongDisplay.querySelector('span'); + if (!textSpan) return; + + // Apply the stored transform position first + textSpan.style.transition = 'none'; + textSpan.style.transform = marqueeCurrentTransform; + + // Force reflow to apply the transform + void textSpan.offsetWidth; + + // Get current position from the stored transform + const matrix = new DOMMatrix(marqueeCurrentTransform); + const currentX = matrix.m41; + + // Calculate remaining distance and time + const distanceTraveled = Math.abs(currentX); + const percentComplete = distanceTraveled / marqueeHalfWidth; + const timeRemaining = marqueeDuration * (1 - percentComplete); + + // Resume animation from current position + textSpan.style.transition = `transform ${timeRemaining}ms linear`; + textSpan.style.transform = `translateX(-${marqueeHalfWidth}px)`; + marqueeCurrentTransform = `translateX(-${marqueeHalfWidth}px)`; +} + +// Add window resize listener to recalculate marquee +window.addEventListener('resize', () => { + if (marqueeOriginalText) { + // Store the paused state before recalculating + const wasPaused = marqueePaused; + + // Restore original text before recalculating + const textSpan = currentSongDisplay.querySelector('span'); + if (textSpan) { + textSpan.textContent = marqueeOriginalText; + } + + setupMarquee(); + + // Restore paused state after recalculation + if (wasPaused) { + marqueePaused = true; + } + } +}); + function togglePlayPause() { if (!playerReady) return; if (isPlaying) { @@ -336,12 +504,10 @@ function attemptSeekWithRetry(seekTime, targetPercentage) { // Allow small tolerance for floating point comparison if (Math.abs(audio.currentTime - targetSeekTime) > 0.5) { // Browser clamped to buffered range - need to wait for more data - // Now pause and show seeking status + // Now pause during seeking if (wasPlaying) { audio.pause(); } - const song = songs[currentSongIndex]; - updateCurrentSongDisplay(`Seeking: ${song.artist} - ${song.title}`); continueSeekingToTarget(wasPlaying); } else { // Successfully reached target immediately (was already buffered) @@ -355,11 +521,6 @@ function attemptSeekWithRetry(seekTime, targetPercentage) { } function continueSeekingToTarget(wasPlaying) { - const song = songs[currentSongIndex]; - - // Keep showing seeking status - updateCurrentSongDisplay(`Seeking: ${song.artist} - ${song.title}`); - // Handler for when more data loads function retrySeek() { if (!isSeeking || targetSeekTime === null) { @@ -384,8 +545,6 @@ function continueSeekingToTarget(wasPlaying) { if (wasPlaying) { audio.play(); - } else { - updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`); } } } @@ -549,7 +708,7 @@ function preloadNextSong() { return; } - console.log(`Preloading: ${song.artist} - ${song.title}`); + console.log(`Preloading: ${song.artist} – ${song.title}`); // Use fetch to force full download of the entire file fetch(`tracks/${filename}`) @@ -606,7 +765,7 @@ function preloadNextSong() { blob: blob }; - console.log(`✓ Fully preloaded: ${song.artist} - ${song.title}`); + console.log(`✓ Fully preloaded: ${song.artist} – ${song.title}`); // Move to next song currentPreloadIndex++; @@ -657,7 +816,7 @@ function processPriorityPreload() { return; } - console.log(`🔥 Priority preloading: ${song.artist} - ${song.title}`); + console.log(`🔥 Priority preloading: ${song.artist} – ${song.title}`); // Use fetch to force full download of the entire file fetch(`tracks/${filename}`) diff --git a/styles.css b/styles.css @@ -178,9 +178,15 @@ body { } .current-song span { + display: inline-block; white-space: nowrap; + will-change: transform; +} + +.current-song.no-overflow span { overflow: hidden; - text-overflow: ellipsis; + text-overflow: hidden; + max-width: 100%; } #audioPlayer {