进入视口触发:IntersectionObserver
「滚动驱动」有两种语义,容易混。高级交互里的 scroll-linked 是把播放头绑死在滚动量上:进度等于 scrollTop 归一化,滚动条本身就是播放头,来回拖会让动画来回 scrub。
本页是另一种——元素进入视口时触发一次入场动画。滚动只负责「越过阈值」这个一次性事件,触发后动画按自己的时间轴播完,与后续滚动无关。一个是 scrub(连续映射),一个是 trigger(离散触发)。
1 · 跨阈值触发
2 · trigger 与 scrub 的分野
差别有两处。其一,scrub 的进度是滚动量的连续函数,回滚动画就倒退,动画长度等于滚动区间;trigger 只取跨阈值这一离散事件,动画长度是它自己的 duration,与滚动多少无关。
其二,scrub 永远可逆,trigger 在 once 模式下不回退——这正是入场动画想要的:出现一次就留在那里。实现上,观察到 isIntersecting 且比例达标即播放入场,并对该元素 unobserve,此后无论怎样滚动都不再回调。repeat 模式则保留观察:进入时播放、离开时把元素重置回隐藏初态。
注 · 手动监听 scroll 需要每帧读 getBoundingClientRect 判断是否进入视口,既频繁又易触发重排。IntersectionObserver 把「相交比例跨过阈值」交给浏览器异步算,只在跨阈值时回调一次,天然适合 trigger 类场景。root
设为滚动容器即可在容器内而非整页判定相交。
警示 · threshold 传的是一个数组而非单值:本页给的是 [0, threshold, 1]。只传目标阈值的话,元素从「完全不可见」跳到「超过阈值」的那一帧可能不产生回调——观察器只在跨过列表中的某个值时才通知。补上 0 与 1 两端,进入与离开都能稳定拿到事件,repeat
模式的重置才不会漏。
3 · 错峰
多张卡片同时越过阈值时,同一批 entries 会一起送达。按元素索引排序后叠加 delay,就得到依次浮现的节奏——与交错是同一件事,只是这一「组」由观察器的一次回调界定,而非预先给定的元素列表。
// trigger 语义: 跨阈值触发一次, 与后续滚动无关 (对比 scroll-linked scrub)
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting && e.intersectionRatio >= threshold) {
new Animation({ // 入场: fade + slide-up
keyframes: [0, 1],
duration: 520,
ease: 'easeOut',
onUpdate: (p) => {
el.style.opacity = p;
el.style.transform = `translateY(${(1 - p) * 28}px)`;
},
});
if (once) io.unobserve(e.target); // once: 出现过就不再回退
} else if (repeat) {
reset(e.target); // repeat: 离开视口重置, 再进入重播
}
}
}, { root: scroller, threshold }); // root = 滚动容器, 容器内判定相交
与之对照的 scroll-linked scrub 见高级交互;入场动画本身用的多关键帧 fade 加 slide-up 见关键帧。