commit a11e4dad03469269b0b77da8cd3d274f0e52a42a
parent 99b1adcfd0891656c4ccc6e32084b571e1f26a05
Author: Hunter
Date: Tue, 24 Mar 2026 00:54:44 -0400
cleanly loop marquee when title overflows
Diffstat:
| M | resources/script.js | | | 97 | ++++++++++++++++++++++++++++++------------------------------------------------- |
1 file changed, 37 insertions(+), 60 deletions(-)
diff --git a/resources/script.js b/resources/script.js
@@ -337,10 +337,11 @@ let marqueeAnimating = false;
let marqueePaused = false;
let marqueeTimeoutId = null;
let marqueeOriginalText = '';
-let marqueeDuration = 0;
let marqueeHalfWidth = 0;
-let marqueeTransitionEndHandler = null;
-let marqueeCurrentTransform = 'translateX(0)';
+let marqueeRafId = null;
+let marqueeStartTime = 0;
+let marqueeElapsedBeforePause = 0;
+const marqueeSpeed = 50; // pixels per second
function setupMarquee() {
const container = currentSongDisplay;
@@ -353,17 +354,17 @@ function setupMarquee() {
clearTimeout(marqueeTimeoutId);
marqueeTimeoutId = null;
}
- if (marqueeTransitionEndHandler) {
- textSpan.removeEventListener('transitionend', marqueeTransitionEndHandler);
- marqueeTransitionEndHandler = null;
+ if (marqueeRafId) {
+ cancelAnimationFrame(marqueeRafId);
+ marqueeRafId = null;
}
marqueeAnimating = false;
marqueePaused = false;
+ marqueeElapsedBeforePause = 0;
// Reset styles
- textSpan.style.transition = 'none';
textSpan.style.transform = 'translateX(0)';
- marqueeCurrentTransform = 'translateX(0)';
+ textSpan.style.animation = 'none';
container.classList.remove('no-overflow');
// Store original text
@@ -396,54 +397,47 @@ function setupMarquee() {
// 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;
+ marqueeStartTime = performance.now();
+ marqueeElapsedBeforePause = 0;
+ marqueeRafId = requestAnimationFrame(marqueeStep);
+ }, 1000); // Initial delay before starting scroll
+ }
+}
- 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;
+function marqueeStep(now) {
+ if (!marqueeAnimating || marqueePaused) return;
- textSpan.style.transition = 'none';
- textSpan.style.transform = 'translateX(0)';
- marqueeCurrentTransform = 'translateX(0)';
+ const elapsed = marqueeElapsedBeforePause + (now - marqueeStartTime);
+ const totalDistance = elapsed * marqueeSpeed / 1000;
+ // Modulo by one segment width for seamless wrapping — no loop boundary
+ const offset = totalDistance % marqueeHalfWidth;
- 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
+ const textSpan = currentSongDisplay.querySelector('span');
+ if (textSpan) {
+ textSpan.style.transform = `translateX(-${offset}px)`;
}
+
+ marqueeRafId = requestAnimationFrame(marqueeStep);
}
function pauseMarquee() {
marqueePaused = true;
- const textSpan = currentSongDisplay.querySelector('span');
- if (!textSpan || !marqueeAnimating) return;
+ if (!marqueeAnimating) return;
- // Capture current transform position
- const computedStyle = window.getComputedStyle(textSpan);
- const currentTransform = computedStyle.transform;
- marqueeCurrentTransform = currentTransform;
+ // Accumulate elapsed time before this pause
+ marqueeElapsedBeforePause += performance.now() - marqueeStartTime;
- // Freeze at current position
- textSpan.style.transition = 'none';
- textSpan.style.transform = currentTransform;
+ if (marqueeRafId) {
+ cancelAnimationFrame(marqueeRafId);
+ marqueeRafId = null;
+ }
- // Clear any pending timeout
+ // Clear any pending timeout (for initial delay)
if (marqueeTimeoutId) {
clearTimeout(marqueeTimeoutId);
marqueeTimeoutId = null;
@@ -457,26 +451,9 @@ function resumeMarquee() {
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)`;
+ // Resume from where we left off
+ marqueeStartTime = performance.now();
+ marqueeRafId = requestAnimationFrame(marqueeStep);
}
// Add window resize listener to recalculate marquee