Re-anchor a moving reel on where it is, not what it was playing

A viewport change re-anchors the reel on the video that is playing,
resolved by id rather than by a scroll position the change has already
invalidated. That is what keeps a rotation on the same video.

Mid-swipe it names the wrong one. setActive runs from onScroll's next
frame, so a resize landing before that frame still reads the video being
swiped *away from* -- and realignToActive writes its position straight to
scrollTop, hauling the reel back to it. A phone slides its chrome on every
scroll, so the resize lands mid-swipe on nearly every swipe: the reel went
where the thumb put it, far enough to show the top of the next video at
the bottom of the screen, and was then written back a beat later.

The input guard in onScroll could not catch this. There is no scroll event
to guard -- the write happens inside the resize.

So while the reader is demonstrably driving, take the reel's own position
as the authority and re-anchor on the step it is nearest at the new
height. It still lands on a snap point, so nothing is left straddling two
screens, but the swipe stands. Only a step's worth of disagreement counts
as theirs: a scroller just re-parented into or out of the
picture-in-picture window reads 0, and following that would land them on
the first video instead of their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
Simon
2026-10-27 22:26:39 +00:00
parent 7033288bc4
commit c60f5e01dc
2 changed files with 59 additions and 4 deletions

View File

@@ -1413,16 +1413,40 @@ App.feed = App.feed || {};
// video playing/focused rather than snapping to a neighbour.
const realignToActive = function() {
if (stepCount() === 0) return;
const h = slideHeight();
const scroller = getScroller();
// Anchoring by id is what keeps a rotation on the same video, because
// the scroll position it would otherwise read has been invalidated by
// the very change being answered. But mid-swipe that id names the video
// being *left*: setActive runs from onScroll's next frame, so a resize
// landing before it still reads the old one -- and writing its position
// hauls the reel back to it. A phone slides its chrome on every scroll,
// so that happened on nearly every swipe.
//
// While the reader is demonstrably driving, the reel's own position is
// the better authority: re-anchor on the step it is nearest at the new
// height. That still puts it on a snap point -- no slide left straddling
// two screens -- without undoing the swipe.
let index = state.feedActiveIndex;
if (state.feedActiveVideoId != null) {
const found = (state.loadedVideos || [])
.findIndex((v) => String(v.id) === String(state.feedActiveVideoId));
if (found >= 0) index = Math.floor(found / paneCount());
}
const driving = Date.now() - lastInputAt < INPUT_GRACE_MS;
if (driving && scroller && h) {
// A swipe moves by a step, so only a step's worth of disagreement
// is the reader's doing. Anything further apart is the position
// being meaningless rather than ahead -- a scroller that has just
// been re-parented into (or out of) the picture-in-picture window
// reads 0, and following that would land the reel on the first
// video instead of theirs.
const atScroll = clampIndex(Math.round(scroller.scrollTop / h));
if (atScroll >= 0 && Math.abs(atScroll - index) <= 1) index = atScroll;
}
index = clampIndex(index);
if (index < 0) return;
state.feedActiveIndex = index;
const h = slideHeight();
const start = windowBounds(index).start;
// Only write what actually differs. This runs on every frame of a
// turn, and a spacer height or a scrollTop written back unchanged
@@ -1431,7 +1455,6 @@ App.feed = App.feed || {};
const spacer = getTopSpacer();
const spacerHeight = `${start * h}px`;
if (spacer && spacer.style.height !== spacerHeight) spacer.style.height = spacerHeight;
const scroller = getScroller();
const top = index * h;
if (scroller && Math.abs(scroller.scrollTop - top) > 0.5) scroller.scrollTop = top;
const activeSlide = slidesByIndex.get(index);