G2:ScrollTrigger 滚动动画实战——让页面在滚动中讲故事
GSAP 动画实战系列 系列总览 | G1 Tween & Timeline | G2 ScrollTrigger 滚动动画 ← 本文 | G3 文本动画 | G4 SVG 动画 | G5 GSAP + React | G6 AI 项目实战 | G7 社区案例
ScrollTrigger 让 GSAP 动画不再是"自动播放",而是"由用户滚动驱动"。它的核心思想很简单:把时间轴映射到滚动条。
1. 快速开始
npm install gsap
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
2. 基础用法——滚动触发动画
2.1 最简单的 ScrollTrigger
gsap.to('.box', {
x: 500,
scrollTrigger: '.box' // 当 .box 进入视口时触发
})
等价于:
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top bottom', // trigger 的 top 碰到 viewport 的 bottom
end: 'top top', // trigger 的 top 碰到 viewport 的 top
scrub: false,
markers: false, // 开发时设为 true 看触发区域
}
})
2.2 Start / End 详解
start 和 end 是两个参数:第一个值是 trigger 元素的位置,第二个值是 viewport 的位置。
scrollTrigger: {
trigger: '.section',
start: 'top 80%', // trigger 的顶部到达 viewport 的 80% 处 → 开始
end: 'bottom 20%', // trigger 的底部到达 viewport 的 20% 处 → 结束
}
常用组合:
| 场景 | start | end | 说明 |
|---|---|---|---|
| 元素进入视口就触发 | 'top bottom' |
'bottom top' |
整个元素可见区间 |
| 元素顶部到视口顶部 | 'top top' |
'+=500' |
滚动 500px 的距离 |
| 元素居中时触发 | 'center center' |
'+=300' |
居中对齐,滚动 300px |
| 百分比 | 'top 80%' |
'bottom 20%' |
viewport 百分比 |
2.3 Markers——开发必备
scrollTrigger: {
trigger: '.section',
start: 'top center',
end: 'bottom center',
markers: true, // ← 开发时打开,显示触发区域
}
Markers 会显示 start 和 end 两条线,绿色是 start,红色是 end。上线前记得关掉。
2.4 ToggleActions——不用 scrub 时的播放控制
当你不使用 scrub(即滚动不实时驱动动画),可以用 toggleActions 控制触发行为:
scrollTrigger: {
trigger: '.card',
toggleActions: 'play none none reverse',
// 进入 离开 再次进入 再次离开
}
四个值对应四种状态的响应:
play/pause/resume/reverse/restart/reset/complete/none
常用的:'play none none reverse'——进入时播放,离开时反转(再滚回来动画也倒放)。
3. Scrub——滚动实时驱动
gsap.to('.progress-bar', {
width: '100%',
scrollTrigger: {
trigger: '.article',
start: 'top top',
end: 'bottom bottom',
scrub: true, // 滚动条位置直接映射到动画进度
}
})
scrub 还可以设置延迟,让动画比滚动稍微滞后,产生阻尼感:
scrub: 0.5 // 0.5 秒延迟——动画会柔和跟随滚动
scrub: 2 // 2 秒延迟——明显拖尾感
scrub vs 非 scrub 的选择:
- Scrub true:滚动条驱动——适合进度条、视差、图片序列帧
- Scrub false + toggleActions:滚动触发但自动播放——适合单个卡片的进入动画
4. Pin——图钉固定
gsap.to('.hero', {
scale: 1.5,
scrollTrigger: {
trigger: '.hero-section',
start: 'top top',
end: '+=1000',
pin: true, // 钉住 trigger 元素
scrub: true,
}
})
Pin 会让元素在指定区间内固定在屏幕上,同时可以继续做动画。这是实现 Apple 风格滚动叙事的关键。
Pin 的注意事项:
- Pin 的容器需要一个占位元素来防止内容跳跃——GSAP 会自动添加
pin-spacerdiv - 不要在 flex/grid 容器内部 pin——布局可能会崩溃。把要 pin 的元素包一层独立的 div
pinSpacing: false可以不让 GSAP 自动添加 spacer(如果你自己管理布局)
pin: true,
pinSpacing: false, // 不自动加 spacer
5. 三个生产级场景
🎬 以下三个场景的完整代码在 CodePen 演示 和 GSAPify 模板 中都有可运行的实例——推荐边看效果边读代码。
场景一:滚动 Reveal 列表(最常用)
等用户滚动到元素附近时,卡片依次淡入上移:
<div class="cards">
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card">Card 3</div>
<!-- ...更多卡片 -->
</div>
gsap.utils.toArray('.card').forEach((card, i) => {
gsap.from(card, {
y: 60,
opacity: 0,
duration: 0.6,
ease: 'power2.out',
scrollTrigger: {
trigger: card,
start: 'top 85%', // 卡片顶部到 viewport 85% 处触发
toggleActions: 'play none none reverse',
// markers: true,
}
})
})
场景二:Hero 视差缩放
滚动时 Hero 背景缩放变大 + 文字淡出:
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom top',
scrub: true,
}
})
tl.to('.hero-bg', { scale: 1.2 }) // 背景放大
.to('.hero-title', { y: 100, opacity: 0 }, 0) // 标题下移 + 淡出(同时开始)
场景三:水平滚动产品展示
Apple 官网那种水平滚动的产品画廊:
<div class="horizontal-section">
<div class="horizontal-track">
<div class="panel">产品 1</div>
<div class="panel">产品 2</div>
<div class="panel">产品 3</div>
</div>
</div>
.horizontal-section {
height: 100vh;
overflow: hidden;
}
.horizontal-track {
display: flex;
width: 300vw; /* 3 个面板,每个 100vw */
}
.panel {
width: 100vw;
height: 100vh;
flex-shrink: 0;
}
const track = document.querySelector('.horizontal-track')
gsap.to(track, {
x: () => -(track.scrollWidth - window.innerWidth),
ease: 'none',
scrollTrigger: {
trigger: '.horizontal-section',
start: 'top top',
end: () => '+=' + (track.scrollWidth - window.innerWidth),
scrub: true,
pin: true,
}
})
这里的核心技巧:
- Pin 住整个 horizontal-section
- 用 x transform 向左移动 track
- end 值动态计算为 track 的滚动宽度
ease: 'none'确保匀速跟随滚动
6. ScrollTrigger.batch()——批量处理
当你有一堆相同类型的元素,不需要每个单独创建 ScrollTrigger:
ScrollTrigger.batch('.card', {
onEnter: batch => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.1, overwrite: true }),
start: 'top 85%',
// 比逐个创建 ScrollTrigger 性能更好
})
7. matchMedia()——响应式动画
移动端和桌面端可能需要完全不同的动画行为:
ScrollTrigger.matchMedia({
// 桌面端(>= 800px)
'(min-width: 800px)': function() {
gsap.to('.gallery', {
x: -500,
scrollTrigger: {
trigger: '.gallery-wrap',
pin: true,
scrub: true,
}
})
},
// 移动端(< 800px)
'(max-width: 799px)': function() {
// 移动端不用水平滚动,用垂直排列
gsap.utils.toArray('.gallery-item').forEach(item => {
gsap.from(item, {
y: 40,
opacity: 0,
scrollTrigger: {
trigger: item,
start: 'top 85%',
}
})
})
},
// 所有设备都执行的
'all': function() {
// 公共动画
}
})
8. 性能与避坑
常见问题:
-
layout 抖动——使用
pin时,如果 pin 的元素在 flex/grid 内部,布局容易崩。解决方案:用独立的包装 div。 -
ScrollTrigger 刷新——页面动态加载内容后(比如无限滚动),需要调用
ScrollTrigger.refresh()重新计算触发位置。 -
移动端性能——减少移动端的动画数量,优先用
opacity,避免filter: blur()之类的昂贵属性。 -
refreshPriority——当多个 ScrollTrigger 需要按照特定顺序刷新时(比如嵌套 pin),设置refreshPriority: 1(数字越大越先刷新)。
// 嵌套 pin 场景:内层的 refreshPriority 更高
gsap.to(parent, {
scrollTrigger: { trigger: parent, pin: true, refreshPriority: 0 }
})
gsap.to(child, {
scrollTrigger: { trigger: child, pin: true, refreshPriority: 1 }
})
once: true——如果动画只需要播放一次(比如初始加载动画),设置once: true,之后 ScrollTrigger 会自动清理,节省资源。
scrollTrigger: {
trigger: '.hero',
once: true, // 播放一次就清理
}
9. 调试技巧
// 全局显示所有 ScrollTrigger markers
ScrollTrigger.defaults({ markers: true })
// 在所有 ScrollTrigger 创建后,手动刷新
ScrollTrigger.refresh()
// 获取所有 ScrollTrigger 实例
const allST = ScrollTrigger.getAll()
allST.forEach(st => console.log(st.vars.trigger, st.start, st.end))
// 监听刷新
ScrollTrigger.addEventListener('refresh', () => console.log('Scroll triggers refreshed'))
下一篇:G3 文本动画——SplitText、ScrambleText 与 TextPlugin
实操清单
- 安装 gsap 并在项目入口执行
gsap.registerPlugin(ScrollTrigger) - 写一个最简单的 ScrollTrigger:给
.box设置scrollTrigger: '.box',验证元素进入视口时动画触发 - 打开
markers: true,观察 start/end 触发线的位置,调整start和end参数到满意效果后关闭 - 用
toggleActions: 'play none none reverse'实现卡片进入时播放、回滚时反转的效果 - 用
scrub: true实现进度条宽度跟随滚动实时变化,再改为scrub: 0.5感受阻尼差异 - 实现场景一:用
gsap.utils.toArray('.card').forEach(...)给卡片列表逐个绑定滚动 Reveal 淡入动画 - 实现场景二:用
gsap.timeline({ scrollTrigger: { scrub: true } })制作 Hero 背景 scale 放大 + 标题淡出的视差效果 - 实现场景三:配置
.horizontal-track的 CSS(display: flex; width: 300vw),再用pin: true+scrub: true完成水平滚动产品展示 - 用
ScrollTrigger.batch('.card', { onEnter, start })替换场景一中的逐个 forEach,对比性能与代码量 - 用
ScrollTrigger.matchMedia()让桌面端显示水平滚动画廊、移动端降级为垂直淡入列表 - 动态加载内容后调用
ScrollTrigger.refresh(),确认触发位置重新计算正确 - 对只需播放一次的动画加上
once: true,并通过ScrollTrigger.getAll()确认实例已被自动清理