add scrolling marquee on text overflow
Hunter
Tuesday, October 28th, 2025 at 4:16 PM
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}`)
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 {