Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions docs/idea-page/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,28 @@ function renderGrid(data){
const REDUCE_MOTION = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const CAROUSEL_SPEED = 0.45; // px per frame
let carouselTracks = [];
const carouselStateByTrack = new WeakMap();
function wrapCarouselPhase(value, period){
return ((value % period) + period) % period;
}
// Idempotent per track: a track already set up keeps its duplicated content and its
// measured period, so this can be re-run on resize without duplicating twice.
function setupCarouselAutoScroll(){
carouselTracks = [];
document.querySelectorAll(".carousel").forEach(track => {
const wrap = track.closest(".carousel-wrap");
if (track.dataset.looping === "1"){
carouselTracks.push({ el: track, singleWidth: +track.dataset.period, paused: false });
const n = track.children.length / 2;
const kids = track.children;
const measuredWidth = kids[n] ? kids[n].offsetLeft - kids[0].offsetLeft : 0;
const singleWidth = measuredWidth > 0 ? measuredWidth : +track.dataset.period;
const state = carouselStateByTrack.get(track);
if (state){
state.singleWidth = singleWidth;
state.position = wrapCarouselPhase(track.scrollLeft, singleWidth);
track.dataset.period = String(singleWidth);
carouselTracks.push(state);
}
return;
}
if (track.scrollWidth <= track.clientWidth + 4){
Expand All @@ -167,10 +181,13 @@ function setupCarouselAutoScroll(){
const singleWidth = kids[n] ? kids[n].offsetLeft - kids[0].offsetLeft : track.scrollWidth;
track.dataset.looping = "1";
track.dataset.period = String(singleWidth);
const state = { el: track, singleWidth, paused: false };
const state = { el: track, singleWidth, position: 0, paused: false };
carouselStateByTrack.set(track, state);
track.addEventListener("mouseenter", () => { state.paused = true; });
track.addEventListener("mouseleave", () => { state.paused = false; });
track.addEventListener("pointerdown", () => { state.paused = true; });
track.addEventListener("pointerup", () => { state.paused = false; });
track.addEventListener("pointercancel", () => { state.paused = false; });
carouselTracks.push(state);
});
}
Expand All @@ -186,9 +203,18 @@ window.addEventListener("resize", () => {
(function tickCarousels(){
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;
// Keep the floating-point phase in JS. Chromium rounds sub-pixel scrollLeft
// writes, so deriving every frame from the DOM would discard the speed.
if (s.paused){
s.position = wrapCarouselPhase(s.el.scrollLeft, s.singleWidth);
return;
}
// 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.position + CAROUSEL_SPEED;
s.position = next >= s.singleWidth ? next - s.singleWidth : next;
s.el.scrollLeft = s.position;
});
}
requestAnimationFrame(tickCarousels);
Expand Down Expand Up @@ -495,7 +521,15 @@ document.addEventListener("click", (ev) => {
const nav = ev.target.closest(".cnav");
if (nav) {
const track = document.getElementById(nav.dataset.target);
if (track) track.scrollBy({left: nav.classList.contains("prev") ? -320 : 320, behavior:"smooth"});
if (track){
const state = carouselStateByTrack.get(track);
if (state){
state.paused = true;
clearTimeout(state._resume);
state._resume = setTimeout(() => { state.paused = false; }, 900);
}
track.scrollBy({left: nav.classList.contains("prev") ? -320 : 320, behavior:"smooth"});
}
return;
}
const node = ev.target.closest(".node");
Expand Down
5 changes: 4 additions & 1 deletion docs/idea-page/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,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}
Expand Down
52 changes: 44 additions & 8 deletions docs/reel-page/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,28 @@ function renderAll(){
const REDUCE_MOTION = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = 0.4; // px/frame
let tracks = [];
function wrapPhase(value, period){
return ((value % period) + period) % period;
}
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 };
const position = dir < 0 ? period : 0;
track.scrollLeft = position;
const state = { el: track, originalCount, period, position, 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; });
Expand All @@ -139,12 +151,36 @@ function setupAutoScroll(){
tracks.push(state);
});
}
let wallResizeTimer = null;
window.addEventListener("resize", () => {
clearTimeout(wallResizeTimer);
wallResizeTimer = setTimeout(() => {
tracks.forEach(s => {
const kids = s.el.children;
const period = kids[s.originalCount]
? kids[s.originalCount].offsetLeft - kids[0].offsetLeft
: s.period;
if (period <= 0) return;
s.period = period;
s.position = wrapPhase(s.el.scrollLeft, period);
});
}, 150);
});
(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;
// Keep the floating-point phase in JS. Chromium rounds sub-pixel scrollLeft
// writes, so deriving every frame from the DOM would discard SPEED entirely.
if (s.paused){
s.position = wrapPhase(s.el.scrollLeft, s.period);
return;
}
const next = s.position + 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.
s.position = s.dir > 0
? (next >= s.period ? next - s.period : next)
: (next <= 0 ? next + s.period : next);
s.el.scrollLeft = s.position;
});
requestAnimationFrame(tick);
})();
Expand Down
5 changes: 4 additions & 1 deletion docs/reel-page/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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: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: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}
Expand Down