fixed firefox marquee rollover flicker

This commit is contained in:
Joe Lothan 2026-05-21 00:56:50 -04:00
parent fe3d5f7039
commit 0f0acb642f
2 changed files with 37 additions and 10 deletions

View file

@ -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 `<body>`.
- **`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)

View file

@ -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 }
);
}
}