Skip to content
Open
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
44 changes: 37 additions & 7 deletions docs/idea-page/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ function setupCarouselAutoScroll(){
document.querySelectorAll(".carousel").forEach(track => {
const wrap = track.closest(".carousel-wrap");
if (track.dataset.looping === "1"){
// Already duplicated: re-measure the period (tile widths / padding can change on
// resize) without duplicating again, and resync the phase to the current position.
const n = track.children.length / 2;
const kids = track.children;
const measuredWidth = kids[n] ? kids[n].offsetLeft - kids[0].offsetLeft : 0;
Expand All @@ -157,6 +159,7 @@ function setupCarouselAutoScroll(){
if (state){
state.singleWidth = singleWidth;
state.position = wrapCarouselPhase(track.scrollLeft, singleWidth);
state._lastWritten = track.scrollLeft;
track.dataset.period = String(singleWidth);
carouselTracks.push(state);
}
Expand All @@ -181,13 +184,38 @@ 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, position: 0, paused: false };
const state = { el: track, singleWidth, position: 0, paused: false, _lastWritten: 0 };
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; });
// Manual scrolls (arrow buttons, touch, wheel) must resync the phase accumulator,
// otherwise the next tick would snap back to the stale phase. Our own writes are
// detected by comparing against the last value we wrote.
track.addEventListener("scroll", () => {
const st = carouselStateByTrack.get(track);
if (st && Math.abs(track.scrollLeft - st._lastWritten) > 0.5) st.position = track.scrollLeft;
});
// Look the state up by element instead of closing over `state`: setupCarouselAutoScroll
// is re-run on resize and replaces the state objects, so a stale closure would keep
// mutating an object the tick loop no longer reads (hover-pause silently breaks).
track.addEventListener("mouseenter", () => {
const st = carouselStateByTrack.get(track);
if (st) st.paused = true;
});
track.addEventListener("mouseleave", () => {
const st = carouselStateByTrack.get(track);
if (st) st.paused = false;
});
track.addEventListener("pointerdown", () => {
const st = carouselStateByTrack.get(track);
if (st) st.paused = true;
});
track.addEventListener("pointerup", () => {
const st = carouselStateByTrack.get(track);
if (st) st.paused = false;
});
track.addEventListener("pointercancel", () => {
const st = carouselStateByTrack.get(track);
if (st) st.paused = false;
});
carouselTracks.push(state);
});
}
Expand All @@ -207,14 +235,16 @@ window.addEventListener("resize", () => {
// writes, so deriving every frame from the DOM would discard the speed.
if (s.paused){
s.position = wrapCarouselPhase(s.el.scrollLeft, s.singleWidth);
s._lastWritten = s.el.scrollLeft;
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;
s._lastWritten = Math.round(s.position);
s.el.scrollLeft = s._lastWritten;
});
}
requestAnimationFrame(tickCarousels);
Expand Down
60 changes: 54 additions & 6 deletions docs/reel-page/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ function wrapPhase(value, period){
function setupAutoScroll(){
tracks = [];
document.querySelectorAll("#wall-view .wall").forEach((track, idx) => {
if (track.dataset.looping === "1"){
// Already duplicated + measured: re-measure the period (tile widths / padding can
// change on resize) without duplicating the content a second time, and re-attach
// with the current position so the phase stays in sync with where the row is.
const kids = track.children;
const n = track.children.length / 2;
const period = kids[n] ? kids[n].offsetLeft - kids[0].offsetLeft : track.scrollWidth / 2;
if (period > 0) track.dataset.period = String(period);
tracks.push({ el: track, period: +track.dataset.period, paused: false, dir: +track.dataset.dir, position: track.scrollLeft, _lastWritten: track.scrollLeft });
return;
}
if (track.scrollWidth <= track.clientWidth + 4) return; // fits, no loop needed
track.style.scrollBehavior = "auto";
const originalCount = track.children.length;
Expand All @@ -138,15 +149,40 @@ function setupAutoScroll(){
const dir = idx % 2 ? -1 : 1; // alternate row directions
const position = dir < 0 ? period : 0;
track.scrollLeft = position;
const state = { el: track, originalCount, period, position, paused: false, dir };
const state = { el: track, originalCount, period, position, paused: false, dir, _lastWritten: position };
track.dataset.looping = "1";
track.dataset.period = String(period);
track.dataset.dir = String(dir);
// Manual scrolls (arrow buttons, touch, wheel) must resync the phase accumulator,
// otherwise the next tick would snap back to the stale phase. Our own writes are
// detected by comparing against the last value we wrote.
track.addEventListener("scroll", () => {
const st = tracks.find(s => s.el === track);
if (st && Math.abs(track.scrollLeft - st._lastWritten) > 0.5) st.position = track.scrollLeft;
});
// 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; });
// Look the state up by element instead of closing over `state`: setupAutoScroll is
// re-run on resize and replaces the state objects, so a stale closure would keep
// mutating an object the tick loop no longer reads (hover-pause silently breaks).
track.addEventListener("mousemove", (e) => {
const st = tracks.find(s => s.el === track);
if (st) st.paused = !!(e.target.closest && e.target.closest(".tile"));
});
track.addEventListener("mouseleave", () => {
const st = tracks.find(s => s.el === track);
if (st) st.paused = false;
});
// Pause the marquee while the cursor is over a left/right arrow so its scroll is clean.
const wrap = track.closest(".wall-wrap");
if (wrap) wrap.querySelectorAll(".cnav").forEach(nav => {
nav.addEventListener("mouseenter", () => { state.paused = true; });
nav.addEventListener("mouseleave", () => { state.paused = false; });
nav.addEventListener("mouseenter", () => {
const st = tracks.find(s => s.el === track);
if (st) st.paused = true;
});
nav.addEventListener("mouseleave", () => {
const st = tracks.find(s => s.el === track);
if (st) st.paused = false;
});
});
tracks.push(state);
});
Expand All @@ -163,6 +199,7 @@ window.addEventListener("resize", () => {
if (period <= 0) return;
s.period = period;
s.position = wrapPhase(s.el.scrollLeft, period);
s._lastWritten = s.el.scrollLeft;
});
}, 150);
});
Expand All @@ -172,6 +209,7 @@ window.addEventListener("resize", () => {
// writes, so deriving every frame from the DOM would discard SPEED entirely.
if (s.paused){
s.position = wrapPhase(s.el.scrollLeft, s.period);
s._lastWritten = s.el.scrollLeft;
return;
}
const next = s.position + SPEED * s.dir;
Expand All @@ -180,11 +218,21 @@ window.addEventListener("resize", () => {
s.position = s.dir > 0
? (next >= s.period ? next - s.period : next)
: (next <= 0 ? next + s.period : next);
s.el.scrollLeft = s.position;
s._lastWritten = Math.round(s.position);
s.el.scrollLeft = s._lastWritten;
});
requestAnimationFrame(tick);
})();

// Re-measure on resize: the loop period depends on tile widths, and a row that fit at
// one width can overflow at a narrower one. setupAutoScroll is idempotent, so
// already-looping rows keep their duplicated content and only get re-synced.
let marqueeResizeTimer = null;
window.addEventListener("resize", () => {
clearTimeout(marqueeResizeTimer);
marqueeResizeTimer = setTimeout(setupAutoScroll, 150);
});

// ---------- lightbox: show the reel directly ----------
let openSlug = null;
function openReel(slug){
Expand Down