cleanly loop marquee when title overflows
Hunter
Tuesday, March 24th, 2026 at 12:54 AM
337 let marqueePaused = false;
338 let marqueeTimeoutId = null;
339 let marqueeOriginalText = '';
-let marqueeDuration = 0;
340 let marqueeHalfWidth = 0;
-let marqueeTransitionEndHandler = null;
-let marqueeCurrentTransform = 'translateX(0)';
341 +let marqueeRafId = null;
342 +let marqueeStartTime = 0;
343 +let marqueeElapsedBeforePause = 0;
344 +const marqueeSpeed = 50; // pixels per second
345
346 function setupMarquee() {
347 const container = currentSongDisplay;
354 clearTimeout(marqueeTimeoutId);
355 marqueeTimeoutId = null;
356 }
- if (marqueeTransitionEndHandler) {
- textSpan.removeEventListener('transitionend', marqueeTransitionEndHandler);
- marqueeTransitionEndHandler = null;
357 + if (marqueeRafId) {
358 + cancelAnimationFrame(marqueeRafId);
359 + marqueeRafId = null;
360 }
361 marqueeAnimating = false;
362 marqueePaused = false;
363 + marqueeElapsedBeforePause = 0;
364
365 // Reset styles
- textSpan.style.transition = 'none';
366 textSpan.style.transform = 'translateX(0)';
- marqueeCurrentTransform = 'translateX(0)';
367 + textSpan.style.animation = 'none';
368 container.classList.remove('no-overflow');
369
370 // Store original text
397 // Calculate animation parameters
398 const fullWidth = textSpan.scrollWidth;
399 marqueeHalfWidth = fullWidth / 2;
- marqueeDuration = (marqueeHalfWidth / 50) * 1000; // Adjust speed here (pixels per second)
400
401 // Start animation after a brief delay (only if not paused)
- // Don't start animation if already paused
402 if (!marqueePaused) {
403 marqueeTimeoutId = setTimeout(() => {
404 if (marqueePaused) return;
405 + marqueeStartTime = performance.now();
406 + marqueeElapsedBeforePause = 0;
407 + marqueeRafId = requestAnimationFrame(marqueeStep);
408 + }, 1000); // Initial delay before starting scroll
409 + }
410 +}
411
- 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;
412 +function marqueeStep(now) {
413 + if (!marqueeAnimating || marqueePaused) return;
414
- textSpan.style.transition = 'none';
- textSpan.style.transform = 'translateX(0)';
- marqueeCurrentTransform = 'translateX(0)';
415 + const elapsed = marqueeElapsedBeforePause + (now - marqueeStartTime);
416 + const totalDistance = elapsed * marqueeSpeed / 1000;
417 + // Modulo by one segment width for seamless wrapping — no loop boundary
418 + const offset = totalDistance % marqueeHalfWidth;
419
- 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
420 + const textSpan = currentSongDisplay.querySelector('span');
421 + if (textSpan) {
422 + textSpan.style.transform = `translateX(-${offset}px)`;
423 }
424 +
425 + marqueeRafId = requestAnimationFrame(marqueeStep);
426 }
427
428 function pauseMarquee() {
429 marqueePaused = true;
- const textSpan = currentSongDisplay.querySelector('span');
- if (!textSpan || !marqueeAnimating) return;
430 + if (!marqueeAnimating) return;
431
- // Capture current transform position
- const computedStyle = window.getComputedStyle(textSpan);
- const currentTransform = computedStyle.transform;
- marqueeCurrentTransform = currentTransform;
432 + // Accumulate elapsed time before this pause
433 + marqueeElapsedBeforePause += performance.now() - marqueeStartTime;
434
- // Freeze at current position
- textSpan.style.transition = 'none';
- textSpan.style.transform = currentTransform;
435 + if (marqueeRafId) {
436 + cancelAnimationFrame(marqueeRafId);
437 + marqueeRafId = null;
438 + }
439
- // Clear any pending timeout
440 + // Clear any pending timeout (for initial delay)
441 if (marqueeTimeoutId) {
442 clearTimeout(marqueeTimeoutId);
443 marqueeTimeoutId = null;
451 const textSpan = currentSongDisplay.querySelector('span');
452 if (!textSpan) return;
453
- // 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)`;
454 + // Resume from where we left off
455 + marqueeStartTime = performance.now();
456 + marqueeRafId = requestAnimationFrame(marqueeStep);
457 }
458
459 // Add window resize listener to recalculate marquee