forked from Keelrleek/SoulStream
fix: artist page fade line no longer lags fast scroll (compositor-driven)
The content fade mask on .scrollbody lives in the scrolling element's local box, so its cut stop (--flow-cut) must ride scroll to stay at a fixed screen line. Driving that from heroScroll's rAF lagged the native compositor scroll by a frame on fast flicks, so the cut slid up and rows briefly showed above where they should clip (user-reported). Move --flow-cut to a CSS scroll-driven animation (@property + @keyframes + animation-timeline: scroll(nearest block), @supports-gated), so it's a pure function of scroll position evaluated on the compositor and can't lag. The keyframe delta and animation-range share one oversized constant (100000px) so the value is base + scrollTop for ANY scroll distance — necessary because the artist page changes its own content height as it scrolls, making the total range non-constant. apply() keeps the per-frame path only as the unsupported-engine fallback. Verified live: screen cut constant at 217px across scrollTop 300..2500, --flow-cut == base+scrollTop exactly.
This commit is contained in:
1 parent
86e0d86604
commit
67d98dfe79
1 file changed
+64
-2
@@ -750,6 +750,12 @@
|
||||
}
|
||||
if (!page || !band || !scroller) return;
|
||||
|
||||
// Compositor-driven fade line where supported (Chromium 115+). When
|
||||
// it is, apply() no longer touches --flow-cut per frame — the CSS
|
||||
// scroll timeline owns it — so a fast scroll can't outrun the mask.
|
||||
const scrollDriven =
|
||||
typeof CSS !== "undefined" && CSS.supports("animation-timeline", "scroll()");
|
||||
|
||||
// Infinity until measured, and again if there is somehow no room to
|
||||
// ride up in: --p then stays 0 and the page is simply the page it is
|
||||
// today, which is the right way for this to fail.
|
||||
@@ -896,6 +902,16 @@
|
||||
}
|
||||
stripCut =
|
||||
BAND_TOP + strip + Math.round(rowBottom) + 2 - (body?.offsetTop ?? 0);
|
||||
|
||||
// Base for the compositor-driven fade line (CSS @keyframes
|
||||
// flow-cut-ride). Only base is needed — the keyframe delta and
|
||||
// animation-range are a fixed oversized constant, so the value
|
||||
// rides scroll as base + scrollTop with no dependence on the
|
||||
// scroller's (here scroll-varying) total range. stripCut is
|
||||
// scroll-invariant by construction, so this is stable.
|
||||
if (scrollDriven) {
|
||||
page.style.setProperty("--flow-cut-base", `${stripCut.toFixed(1)}px`);
|
||||
}
|
||||
}
|
||||
|
||||
function apply() {
|
||||
@@ -904,8 +920,12 @@
|
||||
// The fade line rides EVERY frame — including past the park,
|
||||
// where --p is pinned at 1 but the rows keep scrolling under the
|
||||
// banner. It must not sit behind the p-guard below, or the mask
|
||||
// freezes at the park and rows climb over the banner.
|
||||
page.style.setProperty("--flow-cut", `${(stripCut + scrollTop).toFixed(1)}px`);
|
||||
// freezes at the park and rows climb over the banner. Skipped when
|
||||
// the CSS scroll timeline owns --flow-cut (see scrollDriven) — that
|
||||
// path can't lag the native scroll the way this rAF could.
|
||||
if (!scrollDriven) {
|
||||
page.style.setProperty("--flow-cut", `${(stripCut + scrollTop).toFixed(1)}px`);
|
||||
}
|
||||
const p = Math.min(1, Math.max(0, scrollTop / travel));
|
||||
if (p === previous) return;
|
||||
previous = p;
|
||||
@@ -3687,6 +3707,48 @@
|
||||
);
|
||||
}
|
||||
|
||||
/* --flow-cut on the COMPOSITOR. The mask lives in .scrollbody's local box,
|
||||
which scrolls, so the cut stop must ride scroll to stay at a fixed
|
||||
screen line (= stripCut + scrollTop). Driving that from a rAF lagged the
|
||||
native scroll by a frame on fast flicks, so rows briefly poked ABOVE the
|
||||
cut before the mask caught up (user-reported). A scroll-driven animation
|
||||
makes --flow-cut a pure function of scroll position evaluated on the
|
||||
compositor — it can never lag.
|
||||
|
||||
The animation range and the keyframe delta are the SAME oversized
|
||||
constant (RIDE_PX, far beyond any real page's scroll), so at scroll s
|
||||
the interpolated value is base + (s/RIDE_PX)*RIDE_PX = base + s exactly,
|
||||
with NO dependence on the scroller's total range — which is essential
|
||||
here because the artist page changes its own content height as it
|
||||
scrolls (the tab-grid space reserve), so the total range is not even a
|
||||
constant. measure() writes only base (= stripCut, scroll-invariant).
|
||||
Where scroll-driven animations are unsupported, apply() keeps setting
|
||||
--flow-cut per frame (the @supports gate leaves that path untouched). */
|
||||
@property --flow-cut {
|
||||
syntax: "<length>";
|
||||
inherits: true;
|
||||
initial-value: -99999px;
|
||||
}
|
||||
@keyframes flow-cut-ride {
|
||||
from {
|
||||
--flow-cut: var(--flow-cut-base, -99999px);
|
||||
}
|
||||
to {
|
||||
/* base + RIDE_PX; RIDE_PX must equal the animation-range end. */
|
||||
--flow-cut: calc(var(--flow-cut-base, -99999px) + 100000px);
|
||||
}
|
||||
}
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.artist-page[data-scroll="1"] .scrollbody {
|
||||
animation-name: flow-cut-ride;
|
||||
animation-duration: auto;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: both;
|
||||
animation-timeline: scroll(nearest block);
|
||||
animation-range: 0px 100000px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nothing but the controls themselves may take clicks: the block spans the
|
||||
pane and floats over the track list once it has parked. */
|
||||
.idpin {
|
||||
|
||||
Reference in new issue
Block a user