add scrolling marquee on text overflow

Hunter
Tuesday, October 28th, 2025 at 4:16 PM

script.js+191-32
styles.css+7-1

script.js
 31  		songs = data;
 32  		if (songs.length > 0) {
 33  			playerReady = true;
    -			updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} - ${songs[0].title}`);
 34 +			updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} – ${songs[0].title}`);
 35  			renderPlaylist();
 36  			// Pre-cache resources first, then songs
 37  			preloadResources().then(() => {

 50  audio.addEventListener('play', () => {
 51  	startProgressBar();
 52  	const song = songs[currentSongIndex];
    -	updateCurrentSongDisplay(song.looping ?
    -		`Looping: ${song.artist} - ${song.title}` :
    -		`Now playing: ${song.artist} - ${song.title}`);
 53 +	const songText = `${song.artist} – ${song.title}`;
 54 +
 55 +	// Always update display to ensure we remove "Ready to play:" prefix
 56 +	const currentText = currentSongDisplay.querySelector('span')?.textContent || '';
 57 +
 58 +	// Check if it's a different song (not just play/pause of same song)
 59 +	const isNewSong = !currentText.includes(songText);
 60 +	const hasReadyToPlay = currentText.includes('Ready to play:');
 61 +
 62 +	if (isNewSong || hasReadyToPlay) {
 63 +		updateCurrentSongDisplay(songText);
 64 +	} else {
 65 +		// Same song, just resume the marquee
 66 +		resumeMarquee();
 67 +	}
 68  
 69  	// Update media session metadata
 70  	if ('mediaSession' in navigator) {

 77  
 78  audio.addEventListener('pause', () => {
 79  	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}`);
    -	}
 80 +	pauseMarquee();
 81  });
 82  
 83  audio.addEventListener('ended', () => {

152  		// Toggle looping
153  		songs[index].looping = !(songs[index].looping || false);
154  		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}`);
    -		}
155  	} else {
156  		playSong(index);
157  	}

186  
187  function updateCurrentSongDisplay(text) {
188  	currentSongDisplay.innerHTML = `<span>${text}</span>`;
189 +	// Initialize marquee effect after a brief delay to ensure DOM is updated
190 +	setTimeout(() => setupMarquee(), 50);
191  }
192  
193 +// Marquee state
194 +let marqueeAnimating = false;
195 +let marqueePaused = false;
196 +let marqueeTimeoutId = null;
197 +let marqueeOriginalText = '';
198 +let marqueeDuration = 0;
199 +let marqueeHalfWidth = 0;
200 +let marqueeTransitionEndHandler = null;
201 +let marqueeCurrentTransform = 'translateX(0)';
202 +
203 +function setupMarquee() {
204 +	const container = currentSongDisplay;
205 +	const textSpan = container.querySelector('span');
206 +
207 +	if (!textSpan) return;
208 +
209 +	// Clear any existing animation
210 +	if (marqueeTimeoutId) {
211 +		clearTimeout(marqueeTimeoutId);
212 +		marqueeTimeoutId = null;
213 +	}
214 +	if (marqueeTransitionEndHandler) {
215 +		textSpan.removeEventListener('transitionend', marqueeTransitionEndHandler);
216 +		marqueeTransitionEndHandler = null;
217 +	}
218 +	marqueeAnimating = false;
219 +	marqueePaused = false;
220 +
221 +	// Reset styles
222 +	textSpan.style.transition = 'none';
223 +	textSpan.style.transform = 'translateX(0)';
224 +	marqueeCurrentTransform = 'translateX(0)';
225 +	container.classList.remove('no-overflow');
226 +
227 +	// Store original text
228 +	marqueeOriginalText = textSpan.textContent;
229 +
230 +	// Force reflow
231 +	void textSpan.offsetWidth;
232 +
233 +	// Check if text overflows
234 +	const containerWidth = container.offsetWidth - 30; // Account for padding
235 +	const textWidth = textSpan.scrollWidth;
236 +	const overflows = textWidth > containerWidth;
237 +
238 +	if (!overflows) {
239 +		// No overflow - show ellipsis behavior
240 +		container.classList.add('no-overflow');
241 +		container.classList.remove('marquee-active');
242 +		return;
243 +	}
244 +
245 +	// Text overflows - setup marquee
246 +	marqueeAnimating = true;
247 +	container.classList.add('marquee-active');
248 +
249 +	// Add spacing and duplicate text for seamless loop
250 +	// Using non-breaking spaces (\u00A0) so they don't collapse in HTML
251 +	const spacing = '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0'; // 12 non-breaking spaces
252 +	textSpan.textContent = marqueeOriginalText + spacing + marqueeOriginalText + spacing;
253 +
254 +	// Calculate animation parameters
255 +	const fullWidth = textSpan.scrollWidth;
256 +	marqueeHalfWidth = fullWidth / 2;
257 +	marqueeDuration = (marqueeHalfWidth / 50) * 1000; // Adjust speed here (pixels per second)
258 +
259 +	// Start animation after a brief delay (only if not paused)
260 +	// Don't start animation if already paused
261 +	if (!marqueePaused) {
262 +		marqueeTimeoutId = setTimeout(() => {
263 +			if (marqueePaused) return;
264 +
265 +			textSpan.style.transition = `transform ${marqueeDuration}ms linear`;
266 +			textSpan.style.transform = `translateX(-${marqueeHalfWidth}px)`;
267 +			marqueeCurrentTransform = `translateX(-${marqueeHalfWidth}px)`;
268 +
269 +			// Reset and loop
270 +			marqueeTransitionEndHandler = () => {
271 +				if (!marqueeAnimating || marqueePaused) return;
272 +
273 +				textSpan.style.transition = 'none';
274 +				textSpan.style.transform = 'translateX(0)';
275 +				marqueeCurrentTransform = 'translateX(0)';
276 +
277 +				setTimeout(() => {
278 +					if (!marqueeAnimating || marqueePaused) return;
279 +					textSpan.style.transition = `transform ${marqueeDuration}ms linear`;
280 +					textSpan.style.transform = `translateX(-${marqueeHalfWidth}px)`;
281 +					marqueeCurrentTransform = `translateX(-${marqueeHalfWidth}px)`;
282 +				}, 50);
283 +			};
284 +
285 +			textSpan.addEventListener('transitionend', marqueeTransitionEndHandler);
286 +		}, 1000); // Initial delay before starting scroll
287 +	}
288 +}
289 +
290 +function pauseMarquee() {
291 +	marqueePaused = true;
292 +	const textSpan = currentSongDisplay.querySelector('span');
293 +	if (!textSpan || !marqueeAnimating) return;
294 +
295 +	// Capture current transform position
296 +	const computedStyle = window.getComputedStyle(textSpan);
297 +	const currentTransform = computedStyle.transform;
298 +	marqueeCurrentTransform = currentTransform;
299 +
300 +	// Freeze at current position
301 +	textSpan.style.transition = 'none';
302 +	textSpan.style.transform = currentTransform;
303 +
304 +	// Clear any pending timeout
305 +	if (marqueeTimeoutId) {
306 +		clearTimeout(marqueeTimeoutId);
307 +		marqueeTimeoutId = null;
308 +	}
309 +}
310 +
311 +function resumeMarquee() {
312 +	if (!marqueeAnimating) return;
313 +	marqueePaused = false;
314 +
315 +	const textSpan = currentSongDisplay.querySelector('span');
316 +	if (!textSpan) return;
317 +
318 +	// Apply the stored transform position first
319 +	textSpan.style.transition = 'none';
320 +	textSpan.style.transform = marqueeCurrentTransform;
321 +
322 +	// Force reflow to apply the transform
323 +	void textSpan.offsetWidth;
324 +
325 +	// Get current position from the stored transform
326 +	const matrix = new DOMMatrix(marqueeCurrentTransform);
327 +	const currentX = matrix.m41;
328 +
329 +	// Calculate remaining distance and time
330 +	const distanceTraveled = Math.abs(currentX);
331 +	const percentComplete = distanceTraveled / marqueeHalfWidth;
332 +	const timeRemaining = marqueeDuration * (1 - percentComplete);
333 +
334 +	// Resume animation from current position
335 +	textSpan.style.transition = `transform ${timeRemaining}ms linear`;
336 +	textSpan.style.transform = `translateX(-${marqueeHalfWidth}px)`;
337 +	marqueeCurrentTransform = `translateX(-${marqueeHalfWidth}px)`;
338 +}
339 +
340 +// Add window resize listener to recalculate marquee
341 +window.addEventListener('resize', () => {
342 +	if (marqueeOriginalText) {
343 +		// Store the paused state before recalculating
344 +		const wasPaused = marqueePaused;
345 +
346 +		// Restore original text before recalculating
347 +		const textSpan = currentSongDisplay.querySelector('span');
348 +		if (textSpan) {
349 +			textSpan.textContent = marqueeOriginalText;
350 +		}
351 +
352 +		setupMarquee();
353 +
354 +		// Restore paused state after recalculation
355 +		if (wasPaused) {
356 +			marqueePaused = true;
357 +		}
358 +	}
359 +});
360 +
361  function togglePlayPause() {
362  	if (!playerReady) return;
363  	if (isPlaying) {

504  		// Allow small tolerance for floating point comparison
505  		if (Math.abs(audio.currentTime - targetSeekTime) > 0.5) {
506  			// Browser clamped to buffered range - need to wait for more data
    -			// Now pause and show seeking status
507 +			// Now pause during seeking
508  			if (wasPlaying) {
509  				audio.pause();
510  			}
    -			const song = songs[currentSongIndex];
    -			updateCurrentSongDisplay(`Seeking: ${song.artist} - ${song.title}`);
511  			continueSeekingToTarget(wasPlaying);
512  		} else {
513  			// Successfully reached target immediately (was already buffered)

521  }
522  
523  function continueSeekingToTarget(wasPlaying) {
    -	const song = songs[currentSongIndex];
    -
    -	// Keep showing seeking status
    -	updateCurrentSongDisplay(`Seeking: ${song.artist} - ${song.title}`);
    -
524  	// Handler for when more data loads
525  	function retrySeek() {
526  		if (!isSeeking || targetSeekTime === null) {

545  
546  				if (wasPlaying) {
547  					audio.play();
    -				} else {
    -					updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`);
548  				}
549  			}
550  		}

708  		return;
709  	}
710  
    -	console.log(`Preloading: ${song.artist} - ${song.title}`);
711 +	console.log(`Preloading: ${song.artist} – ${song.title}`);
712  
713  	// Use fetch to force full download of the entire file
714  	fetch(`tracks/${filename}`)

765  				blob: blob
766  			};
767  
    -			console.log(`✓ Fully preloaded: ${song.artist} - ${song.title}`);
768 +			console.log(`✓ Fully preloaded: ${song.artist} – ${song.title}`);
769  
770  			// Move to next song
771  			currentPreloadIndex++;

816  		return;
817  	}
818  
    -	console.log(`🔥 Priority preloading: ${song.artist} - ${song.title}`);
819 +	console.log(`🔥 Priority preloading: ${song.artist} – ${song.title}`);
820  
821  	// Use fetch to force full download of the entire file
822  	fetch(`tracks/${filename}`)
styles.css
178  }
179  
180  .current-song span {
181 +	display: inline-block;
182  	white-space: nowrap;
183 +	will-change: transform;
184 +}
185 +
186 +.current-song.no-overflow span {
187  	overflow: hidden;
    -	text-overflow: ellipsis;
188 +	text-overflow: hidden;
189 +	max-width: 100%;
190  }
191  
192  #audioPlayer {