diff --git a/docs/idea-page/gallery.js b/docs/idea-page/gallery.js index c39c9c9..481a908 100644 --- a/docs/idea-page/gallery.js +++ b/docs/idea-page/gallery.js @@ -187,8 +187,11 @@ window.addEventListener("resize", () => { if (!REDUCE_MOTION) { carouselTracks.forEach(s => { if (s.paused) return; - s.el.scrollLeft += CAROUSEL_SPEED; - if (s.el.scrollLeft >= s.singleWidth) s.el.scrollLeft -= s.singleWidth; + // Shift by exactly one period so the wrap is phase-continuous and invisible. + // Never clamp the landing position: clamping leaves a residual offset that + // accumulates into a visible jump. + const next = s.el.scrollLeft + CAROUSEL_SPEED; + s.el.scrollLeft = next >= s.singleWidth ? next - s.singleWidth : next; }); } requestAnimationFrame(tickCarousels); diff --git a/docs/idea-page/index.html b/docs/idea-page/index.html index e07c96f..5ba1d7c 100644 --- a/docs/idea-page/index.html +++ b/docs/idea-page/index.html @@ -169,7 +169,10 @@ .cat-name{font-size:16.5px;font-weight:750;letter-spacing:-.2px} .cat-count{font-size:12px;color:var(--dim);font-weight:600;background:var(--line);padding:2px 9px;border-radius:999px} .carousel-wrap{position:relative} -.carousel{display:flex;gap:16px;overflow-x:auto;scroll-behavior:smooth;padding:4px 32px 14px;scrollbar-width:none} +/* No scroll-behavior:smooth here — the marquee writes scrollLeft every frame and CSS + smoothing turns each write into a competing animation. Arrow buttons pass + behavior:"smooth" explicitly to scrollBy(), so they are unaffected. */ +.carousel{display:flex;gap:16px;overflow-x:auto;padding:4px 32px 14px;scrollbar-width:none} .carousel::-webkit-scrollbar{display:none} .cnav{position:absolute;top:38%;transform:translateY(-50%);width:36px;height:36px;border-radius:50%;background:var(--card);border:1px solid var(--line);box-shadow:var(--shadow-md);display:grid;place-items:center;cursor:pointer;z-index:5;color:var(--ink);transition:.2s;opacity:.9} .cnav:hover{transform:translateY(-50%) scale(1.08);opacity:1} diff --git a/docs/reel-page/gallery.js b/docs/reel-page/gallery.js index cf4e478..aea6066 100644 --- a/docs/reel-page/gallery.js +++ b/docs/reel-page/gallery.js @@ -120,13 +120,21 @@ let tracks = []; function setupAutoScroll(){ tracks = []; document.querySelectorAll("#wall-view .wall").forEach((track, idx) => { - const singleWidth = track.scrollWidth; - if (singleWidth <= track.clientWidth + 4) return; // fits, no loop needed + if (track.scrollWidth <= track.clientWidth + 4) return; // fits, no loop needed track.style.scrollBehavior = "auto"; + const originalCount = track.children.length; + if (!originalCount) return; track.insertAdjacentHTML("beforeend", track.innerHTML); // duplicate for a seamless wrap + // True loop period = distance between a tile and its clone. scrollWidth is WRONG here + // because .wall has horizontal padding, which would make every wrap visibly jump. + const kids = track.children; + const period = kids[originalCount] + ? kids[originalCount].offsetLeft - kids[0].offsetLeft + : track.scrollWidth / 2; + if (period <= 0) return; const dir = idx % 2 ? -1 : 1; // alternate row directions - if (dir < 0) track.scrollLeft = singleWidth; - const state = { el: track, singleWidth, paused: false, dir }; + if (dir < 0) track.scrollLeft = period; + const state = { el: track, period, paused: false, dir }; // Pause ONLY while the cursor is over an actual poster tile — not the gaps/padding. track.addEventListener("mousemove", (e) => { state.paused = !!(e.target.closest && e.target.closest(".tile")); }); track.addEventListener("mouseleave", () => { state.paused = false; }); @@ -142,9 +150,11 @@ function setupAutoScroll(){ (function tick(){ if (!REDUCE_MOTION) tracks.forEach(s => { if (s.paused) return; - s.el.scrollLeft += SPEED * s.dir; - if (s.el.scrollLeft >= s.singleWidth) s.el.scrollLeft -= s.singleWidth; - else if (s.el.scrollLeft <= 0) s.el.scrollLeft += s.singleWidth; + const next = s.el.scrollLeft + SPEED * s.dir; + // Wrap by exactly one period, and ONLY on the edge this row is travelling toward. + // Checking both edges makes each wrap land on the other edge's trigger -> per-frame ping-pong. + if (s.dir > 0) s.el.scrollLeft = next >= s.period ? next - s.period : next; + else s.el.scrollLeft = next <= 0 ? next + s.period : next; }); requestAnimationFrame(tick); })(); diff --git a/docs/reel-page/index.html b/docs/reel-page/index.html index d3b65f7..a94e6ae 100644 --- a/docs/reel-page/index.html +++ b/docs/reel-page/index.html @@ -180,7 +180,10 @@ .wall-lbl .wl-ref{font-size:11px;font-weight:700;color:var(--brand-2);vertical-align:super;margin:0 2px 0 1px} .wall-lbl .wl-ref:hover{text-decoration:underline} .wall-lbl .wl-count{font-size:11px;font-weight:700;color:var(--dim);background:var(--line);padding:2px 9px;border-radius:999px} -.wall{display:flex;gap:16px;overflow-x:auto;scroll-behavior:smooth;padding:44px 32px;scrollbar-width:none} +/* No scroll-behavior:smooth here — the marquee writes scrollLeft every frame and CSS + smoothing turns each write into a competing animation. Arrow buttons pass + behavior:"smooth" explicitly to scrollBy(), so they are unaffected. */ +.wall{display:flex;gap:16px;overflow-x:auto;padding:44px 32px;scrollbar-width:none} .wall::-webkit-scrollbar{display:none} .wall-wrap{position:relative} .cnav{position:absolute;top:50%;transform:translateY(-50%);width:38px;height:38px;border-radius:50%;background:var(--card);border:1px solid var(--line);box-shadow:var(--shadow-md);display:grid;place-items:center;cursor:pointer;z-index:30;color:var(--ink);transition:transform .15s,opacity .15s,background .15s;opacity:.9;padding:0}