animation strip
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
// with a short CSS transition instead: an IntersectionObserver on the same
|
||||
// sentinel toggles `.compact`. No per-frame scroll handler anywhere.
|
||||
let sentinelEl = $state<HTMLDivElement>();
|
||||
let stripEl = $state<HTMLDivElement>();
|
||||
let stripScrollEl = $state<HTMLDivElement>();
|
||||
let daysWrapEl = $state<HTMLDivElement>();
|
||||
let needsSnapFallback = $state(false);
|
||||
@@ -71,21 +72,40 @@
|
||||
onMount(() => {
|
||||
const scrubSupported =
|
||||
CSS.supports('animation-timeline: view()') && CSS.supports('timeline-scope: none');
|
||||
if (scrubSupported || !sentinelEl) return;
|
||||
needsSnapFallback = true;
|
||||
// `stuck` fires the moment the strip pins (turns the bar opaque before any
|
||||
// content can slide under it); `compact` snaps the cells once more than
|
||||
// half of the sentinel band has scrolled past. The transitions smooth both.
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const e = entries[entries.length - 1];
|
||||
stuck = e.intersectionRatio < 0.97;
|
||||
compact = e.intersectionRatio < 0.5;
|
||||
},
|
||||
{ threshold: [0.25, 0.5, 0.75, 0.97] }
|
||||
);
|
||||
io.observe(sentinelEl);
|
||||
return () => io.disconnect();
|
||||
if (scrubSupported || !sentinelEl || !stripEl) return;
|
||||
|
||||
// Without scroll-driven animations (Firefox, older Safari) the collapse used
|
||||
// to snap between the two states at a threshold, which reads as a jump next
|
||||
// to the smooth scrub everywhere else. Drive `--strip-p` from the scroll
|
||||
// position instead, so the cells shrink with the scroll exactly as they do
|
||||
// natively. One rAF-coalesced write per frame, no layout thrash.
|
||||
const sentinel = sentinelEl;
|
||||
const strip = stripEl;
|
||||
const scroller = sentinel.closest('main');
|
||||
|
||||
let frame = 0;
|
||||
const update = () => {
|
||||
frame = 0;
|
||||
const band = sentinel.getBoundingClientRect();
|
||||
const top = scroller ? scroller.getBoundingClientRect().top : 0;
|
||||
const height = band.height || 1;
|
||||
const progress = Math.min(1, Math.max(0, (top - band.top) / height));
|
||||
strip.style.setProperty('--strip-p', String(progress));
|
||||
strip.style.setProperty('--stuck', progress > 0.02 ? '1' : '0');
|
||||
};
|
||||
const schedule = () => {
|
||||
if (!frame) frame = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
update();
|
||||
const target: EventTarget = scroller ?? window;
|
||||
target.addEventListener('scroll', schedule, { passive: true });
|
||||
window.addEventListener('resize', schedule);
|
||||
return () => {
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
target.removeEventListener('scroll', schedule);
|
||||
window.removeEventListener('resize', schedule);
|
||||
};
|
||||
});
|
||||
|
||||
// Start scrolled so the "Past" button sits just off the left edge (revealed by
|
||||
@@ -129,6 +149,7 @@
|
||||
<div bind:this={sentinelEl} class="sentinel" aria-hidden="true"></div>
|
||||
|
||||
<div
|
||||
bind:this={stripEl}
|
||||
class="daystrip sticky -top-3 z-30 -mx-3 lg:-top-6 lg:-mx-8 mt-1"
|
||||
class:js-snap={needsSnapFallback}
|
||||
class:compact
|
||||
|
||||
Reference in New Issue
Block a user