From 0f0acb642f086f157629dd40977a80b541b0e9da Mon Sep 17 00:00:00 2001 From: Joe Lothan Date: Thu, 21 May 2026 00:56:50 -0400 Subject: [PATCH] fixed firefox marquee rollover flicker --- PLAN.md | 2 ++ frontend/site.js | 45 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/PLAN.md b/PLAN.md index 4d4fa28..a4c8b01 100644 --- a/PLAN.md +++ b/PLAN.md @@ -665,6 +665,8 @@ Before public launch: - **Fix: `filter: blur(0px)` on Firefox.** A no-op CSS filter forces Firefox through a different compositing path that enables sub-pixel rendering during transform animations. Applied via `.browser-firefox .tab-row { filter: blur(0px); }` using the existing browser detection class on ``. - **`image-rendering: pixelated` conflicts with sub-pixel transforms.** Once the filter hack enables sub-pixel rendering, `pixelated` (nearest-neighbor sampling) causes icons to flicker at sub-pixel positions. Fix: `.browser-firefox .tab-icon { image-rendering: auto; }` — bilinear filtering interpolates smoothly. Icons are very slightly softer on Firefox but don't flicker. Chrome/Safari keep `pixelated`. - **Integer-pixel rAF animation is not a good alternative.** Attempted `requestAnimationFrame` with `Math.round()`/`Math.floor()` to control pixel snapping — eliminates flicker but introduces visible stepping at slow speeds. Text jumps are more distracting than the original jitter. The `filter` hack is strictly better. +- **Web Animations API flickers at loop boundary on Firefox.** Even with pixel-precise endpoints (not percentage), Firefox's compositor flickers when an `iterations: Infinity` animation restarts. Fix: use `requestAnimationFrame` with continuous sub-pixel positions and modular wrapping (`if (pos >= halfWidth) pos -= halfWidth`) — no iteration boundary means no flicker. Combined with `filter: blur(0px)` for sub-pixel smoothness. Chrome/Safari use the Web Animations API without issue. +- **Marquee speed should be px/sec, not fixed duration.** Original code used 90-150s fixed duration, but wider screens have wider rows, so the same duration = faster movement. Fix: target 15-25 px/sec and calculate duration from measured row width after DOM insertion. ## Phase 10: Parallelization (if needed) diff --git a/frontend/site.js b/frontend/site.js index b8d636b..fadab88 100644 --- a/frontend/site.js +++ b/frontend/site.js @@ -23,6 +23,7 @@ function detectBrowser() { } const browserName = detectBrowser(); document.body.classList.add(`browser-${browserName}`); +const isFirefox = browserName === "firefox"; // How many tabs fit in one row? function tabsPerRow() { @@ -109,14 +110,12 @@ function createRow(entries, rowIndex) { const row = document.createElement("div"); row.className = "tab-row"; - const pxPerSec = 20 + (rng() * 15); // 20-35 px/sec, consistent across screen sizes + const pxPerSec = 15 + (rng() * 10); // 15-25 px/sec, consistent across screen sizes const goLeft = rng() > 0.5; const stagger = rng(); - row.classList.add(goLeft ? "scroll-left" : "scroll-right"); - - // Store config — duration calculated after DOM insertion (needs measured width) - row._animConfig = { pxPerSec, stagger }; + // Store config — animation starts after DOM insertion (needs measured width) + row._animConfig = { pxPerSec, goLeft, stagger }; // Add tabs twice so the marquee loops seamlessly (translate -50% = one full set) for (let copy = 0; copy < 2; copy++) { @@ -128,13 +127,39 @@ function createRow(entries, rowIndex) { return row; } -// Set animation speed based on actual row width (must be called after DOM insertion) +// Start animation (must be called after DOM insertion) function startRowAnimation(row) { - const { pxPerSec, stagger } = row._animConfig; + const { pxPerSec, goLeft, stagger } = row._animConfig; const halfWidth = row.scrollWidth / 2; - const duration = halfWidth / pxPerSec; - row.style.setProperty("--speed", `${duration}s`); - row.style.animationDelay = `${-stagger * duration}s`; + + if (isFirefox) { + // Firefox: rAF with continuous sub-pixel positions — no loop boundary, + // no compositor iteration restart flicker. filter: blur(0px) in CSS + // prevents Firefox's device-pixel snapping. + let pos = stagger * halfWidth; + let lastTime = null; + + function tick(now) { + if (lastTime === null) { lastTime = now; requestAnimationFrame(tick); return; } + pos += ((now - lastTime) / 1000) * pxPerSec; + lastTime = now; + if (pos >= halfWidth) pos -= halfWidth; + row.style.transform = `translateX(${goLeft ? -pos : -(halfWidth - pos)}px)`; + requestAnimationFrame(tick); + } + + requestAnimationFrame(tick); + } else { + // Chrome/Safari: Web Animations API on the compositor + const duration = halfWidth / pxPerSec * 1000; + const from = goLeft ? "translateX(0px)" : `translateX(-${halfWidth}px)`; + const to = goLeft ? `translateX(-${halfWidth}px)` : "translateX(0px)"; + + row.animate( + [{ transform: from }, { transform: to }], + { duration, iterations: Infinity, easing: "linear", iterationStart: stagger } + ); + } }