G2:ScrollTrigger 滚动动画实战——让页面在滚动中讲故事

GSAP 动画实战系列 系列总览 | G1 Tween & Timeline | G2 ScrollTrigger 滚动动画 ← 本文 | G3 文本动画 | G4 SVG 动画 | G5 GSAP + React | G6 AI 项目实战 | G7 社区案例


# ScrollTrigger 滚动动画 ## 基础用法 - trigger / start / end - toggleActions / markers - scrub - pin ## 高级模式 - batch 批量处理 - matchMedia 响应式 - refreshPriority 控制 - containerAnimation ## 生产级场景 - 视差滚动 Hero - 水平滚动产品展示 - 淡入列表 reveal

ScrollTrigger 让 GSAP 动画不再是"自动播放",而是"由用户滚动驱动"。它的核心思想很简单:把时间轴映射到滚动条

1. 快速开始

npm install gsap
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

gsap.registerPlugin(ScrollTrigger)

2. 基础用法——滚动触发动画

📐 所有 ScrollTrigger 演示合集 → CodePen | 官方文档

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 详解

startend 是两个参数:第一个值是 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 会显示 startend 两条线,绿色是 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 的注意事项

  1. Pin 的容器需要一个占位元素来防止内容跳跃——GSAP 会自动添加 pin-spacer div
  2. 不要在 flex/grid 容器内部 pin——布局可能会崩溃。把要 pin 的元素包一层独立的 div
  3. 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. 性能与避坑

常见问题

  1. layout 抖动——使用 pin 时,如果 pin 的元素在 flex/grid 内部,布局容易崩。解决方案:用独立的包装 div。

  2. ScrollTrigger 刷新——页面动态加载内容后(比如无限滚动),需要调用 ScrollTrigger.refresh() 重新计算触发位置。

  3. 移动端性能——减少移动端的动画数量,优先用 opacity,避免 filter: blur() 之类的昂贵属性。

  4. 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 }
})
  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 触发线的位置,调整 startend 参数到满意效果后关闭
  • 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() 确认实例已被自动清理