首页
/ OpenMontage 中的 GSAP ScrollTrigger 实战:滚动驱动动画、Pinning、Scrub 与容器动画的官方技能全解

OpenMontage 中的 GSAP ScrollTrigger 实战:滚动驱动动画、Pinning、Scrub 与容器动画的官方技能全解

2026-09-07 11:19:52作者:庞队千Virginia

本篇技术指南围绕 OpenMontage 仓库内置的 gsap-scrolltrigger 技能文档(.agents/skills/gsap-scrolltrigger/SKILL.md)展开,系统讲解 GSAP 官方 ScrollTrigger 插件的注册、触发机制、核心配置、批处理、滚动代理、Scrub、Pinning 与横向伪滚动等关键技术点。作为本文读者的你,将在理解该技能文件在 OpenMontage 技能体系中的定位之后,掌握一套可直接复用的滚动驱动 Web 动效实现方案与避坑清单。

一、技能定位:ScrollTrigger 在 OpenMontage 中的角色

OpenMontage 是一个 Agent 驱动的视频制作系统,仓库内以 .agents/skills/ 方式维护了数百个"技能(Skill)"文件,供接入的 AI 编程助手在写代码时自动检索与遵循。GSAP(GreenSock Animation Platform)相关能力被拆分为八个可加载的 Layer-3 技能,分别为 gsap-coregsap-timelinegsap-reactgsap-pluginsgsap-performancegsap-utilsgsap-frameworks 与本文主角 gsap-scrolltrigger,它们统一由 .agents/skills/gsap/README.md 这一导航文件编排(该目录本身不含 SKILL.md,不会被直接加载)。

gsap-scrolltrigger 技能文件的 frontmatter(SKILL.md)声明了它的触发语义:

name: gsap-scrolltrigger
description: Official GSAP skill for ScrollTrigger  scroll-linked animations, pinning, scrub, triggers. Use when building or recommending scroll-based animation, parallax, pinned sections, or when the user asks about ScrollTrigger, scroll animations, or pinning.
license: MIT

该技能来自 greensock 官方技能仓库,MIT 许可。需要特别强调的是,按 .agents/skills/gsap/README.md 的说明,OpenMontage 当前的视频成片主要由 Remotion 的 useCurrentFrame() + interpolate() + spring() 或 HyperFrames 运行时驱动;而 ScrollTrigger 属于**滚动驱动(scroll-driven)**能力,其适用边界是 "web preview only, not video render"——即用于交互式网页预览中的滚动动效,而不是离线视频帧渲染。把握这一边界,是正确使用本技能的第一前提。

本技能与其它 GSAP 技能的协作关系(原文即列明):

下文所有配置与代码均以技能文件正文为骨架,并结合仓库说明展开注释。

二、注册插件:一次注册,全局生效

ScrollTrigger 是 GSAP 的官方插件,脚本加载完成后必须先注册,且全局只需注册一次

gsap.registerPlugin(ScrollTrigger);

注册必须在任何使用 scrollTrigger 配置或 ScrollTrigger.* 静态方法的代码之前执行。该文件还给出了编写代码时的顶层约束(SKILL.md):无论有多少个触发器,都不要重复注册。

三、基本触发器:把 tween 绑定到滚动位置

最基础的用法是把一个 tween 或 timeline 与滚动位置绑定。以下来自技能文档的示例中,.box 从视口顶部进入视口中心时开始向右移动 500px,离开视口中心时反向移动回来:

gsap.to(".box", {
  x: 500,
  duration: 1,
  scrollTrigger: {
    trigger: ".box",
    start: "top center",   // when top of trigger hits center of viewport
    end: "bottom center",  // when the bottom of the trigger hits the center of the viewport
    toggleActions: "play reverse play reverse" // onEnter play, onLeave reverse, onEnterBack play, onLeaveBack reverse
  }
});

start / end 的取值语法

start / end 描述"触发元素(trigger)位置 vs 视口/滚动容器位置",格式为 "triggerPosition viewportPosition"

  • 字符串组合示例:"top top""center center""bottom 80%"
  • 纯数字像素值:如 500,表示滚动容器(默认视口)从顶部算起总共滚过 500px 时触发;
  • 相对值:"+=300"(超出 start 再滚 300px)、"+=100%"(超出 start 一个滚动容器高度)、"max"(最大滚动位置);
  • clamp() 包裹(v3.12+):将位置限制在页面边界内,如 start: "clamp(top bottom)"end: "clamp(bottom top)"
  • 函数:返回字符串或数字,接收当前 ScrollTrigger 实例,可用于动态计算。

当布局发生变化(如动态内容、字体加载、图片加载导致尺寸/位置变化)后,需要调用 ScrollTrigger.refresh() 让这些值重新计算。

四、核心配置项全表

技能文档为 scrollTrigger 配置对象给出下表(SKILL.md),这是整份技能最可复用的速查表,建议写作时逐项对照:

Property Type Description
trigger String | Element Element whose position defines where the ScrollTrigger starts. Required (or use shorthand).
start String | Number | Function When the trigger becomes active. Default "top bottom" (or "top top" if pin: true).
end String | Number | Function When the trigger ends. Default "bottom top". Use endTrigger if end is based on a different element.
endTrigger String | Element Element used for end when different from trigger.
scrub Boolean | Number Link animation progress to scroll. true = direct; number = seconds for playhead to "catch up".
toggleActions String Four actions in order: onEnter, onLeave, onEnterBack, onLeaveBack. Each: "play", "pause", "resume", "reset", "restart", "complete", "reverse", "none". Default "play none none none".
pin Boolean | String | Element Pin an element while active. true = pin the trigger. Don't animate the pinned element itself; animate children.
pinSpacing Boolean | String Default true (adds spacer so layout doesn't collapse). false or "margin".
horizontal Boolean true for horizontal scrolling.
scroller String | Element Scroll container (default: viewport). Use selector or element for a scrollable div.
markers Boolean | Object true for dev markers; or { startColor, endColor, fontSize, ... }. Remove in production.
once Boolean If true, kills the ScrollTrigger after end is reached once (animation keeps running).
id String Unique id for ScrollTrigger.getById(id).
refreshPriority Number Lower = refreshed first. Use when creating ScrollTriggers in non–top-to-bottom order: set so triggers refresh in page order (first on page = lower number).
toggleClass String | Object Add/remove class when active. String = on trigger; or { targets: ".x", className: "active" }.
snap Number | Array | Function | "labels" | Object Snap to progress values. Number = increments (e.g. 0.25); array = specific values; "labels" = timeline labels; object: { snapTo: 0.25, duration: 0.3, delay: 0.1, ease: "power1.inOut" }.
containerAnimation Tween | Timeline For "fake" horizontal scroll: the timeline/tween that moves content horizontally. ScrollTrigger ties vertical scroll to this animation's progress. See Horizontal scroll (containerAnimation) below. Pinning and snapping are not available on containerAnimation-based ScrollTriggers.
onEnter, onLeave, onEnterBack, onLeaveBack Function Callbacks when crossing start/end; receive the ScrollTrigger instance (progress, direction, isActive, getVelocity()).
onUpdate, onToggle, onRefresh, onScrubComplete Function onUpdate fires when progress changes; onToggle when active flips; onRefresh after recalc; onScrubComplete when numeric scrub finishes.

补充两个快捷语义:

  1. 简写形式scrollTrigger: ".selector" 等价于只设置了 trigger 一项。
  2. refreshPriority 的取值方向:数字越小越先 refresh,页面顶部第一个区块应赋最小值,用于保证按页面出现顺序刷新(详见"刷新与清理"一节)。

独立的 ScrollTrigger(不绑定 tween)

当需要自定义行为而不是驱动某个动画时,可用 ScrollTrigger.create() 直接创建独立触发器,并在回调里基于 self.progress 更新 UI:

ScrollTrigger.create({
  trigger: "#id",
  start: "top top",
  end: "bottom 50%+=100px",
  onUpdate: (self) => console.log(self.progress.toFixed(3), self.direction)
});

注意这里的 end: "bottom 50%+=100px" 混用了视口位置 50% 与相对位移 +=100px,ScrollTrigger 支持这种组合表达式。

五、ScrollTrigger.batch():批量触发与回调打包

ScrollTrigger.batch(triggers, vars) 会为每个目标元素创建一个 ScrollTrigger,并把一小段时间间隔内触发的同类型回调(onEnter、onLeave 等)打包成一批统一分发,返回 ScrollTrigger 实例数组。非常适合"每个进入视口的元素都执行一次带 stagger 的入场动画",也是 IntersectionObserver 的轻量替代方案。

  • triggers:选择器文本(如 ".box")或元素数组;
  • vars:标准 ScrollTrigger 配置(start、end、once、各回调等)。不要trigger(目标本身就是触发器),也不要传与动画绑定相关的选项:animationinvalidateOnRefreshonSnapCompleteonScrubCompletescrubsnaptoggleActions

回调签名差异(关键细节):批量回调接收两个参数,而普通 ScrollTrigger 回调只接收一个实例——

  1. targets —— 该时间片内触发回调的触发元素数组;
  2. scrollTriggers —— 对应的 ScrollTrigger 实例数组,可用于读取 progress、direction 或调用 kill()

vars 中与打包粒度相关的两个选项:

  • interval(Number):收集一批的最大时间(秒),默认约为一个 requestAnimationFrame。当某类回调首次触发时计时开始;间隔耗尽或达到 batchMax 即投递本批。
  • batchMax(Number | Function):每批最多元素数,满则先触发回调并开启下一批。传入函数可按响应式布局动态返回数值——该函数会在 refresh(resize、标签页重新聚焦等)时重新执行。

基础批量示例(SKILL.md):

ScrollTrigger.batch(".box", {
  onEnter: (elements, triggers) => {
    gsap.to(elements, { opacity: 1, y: 0, stagger: 0.15 });
  },
  onLeave: (elements, triggers) => {
    gsap.to(elements, { opacity: 0, y: 100 });
  },
  start: "top 80%",
  end: "bottom 20%"
});

intervalbatchMax 做更细粒度控制(SKILL.md):

ScrollTrigger.batch(".card", {
  interval: 0.1,
  batchMax: 4,
  onEnter: (batch) => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.1, overwrite: true }),
  onLeaveBack: (batch) => gsap.set(batch, { opacity: 0, y: 50, overwrite: true })
});

第二例把回调参数命名为 batch(即第一参数 targets 的数组),用 overwrite: true 防止同一元素上动画相互覆盖。卡片墙、瀑布流列表等场景可将此模式作为通用范式。

六、ScrollTrigger.scrollerProxy():接入第三方平滑滚动库

当页面使用第三方平滑滚动库(而非 GSAP 自带的 ScrollSmoother)时,ScrollTrigger 默认读取的是元素原生 scrollTop/scrollLeft,无法感知库内部的"假滚动"。ScrollTrigger.scrollerProxy(scroller, vars) 用于覆写 ScrollTrigger 对一个指定滚动容器的读写方式:

  • scroller:选择器或元素(如 "body"".container");
  • vars:包含 scrollTop 和/或 scrollLeft 函数。每个函数同时充当 getter 与 setter:被传入参数调用时为 setter;被无参数调用时返回当前值(getter)。scrollTopscrollLeft 至少其一必填。

可选的 vars 项:

  • getBoundingClientRect:返回 { top, left, width, height } 的函数(视口通常为 { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }),当滚动容器真实 rect 非默认值时使用;
  • scrollWidth / scrollHeight:同样遵循"有参=setter、无参=getter"的读写函数,用于库暴露了不同尺寸口径的情形;
  • fixedMarkers(Boolean):为 true 时把 markers 视为 position: fixed。当滚动容器被 transform(例如平滑滚动库平移了容器)导致 markers 错位时使用;
  • pinType"fixed""transform",控制该容器的 pinning 实现方式。主线程滚动场景若出现 pin 抖动用 "fixed";若 pin 不跟随则用 "transform"

关键一步:第三方滚动库更新位置时,必须让 ScrollTrigger 收到通知——把 ScrollTrigger.update 注册为监听器(如 smoothScroller.addListener(ScrollTrigger.update)),否则其内部计算全部过期。

技能文档给出的完整代理示例(SKILL.md):

// Example: proxy body scroll to a third-party scroll instance
ScrollTrigger.scrollerProxy(document.body, {
  scrollTop(value) {
    if (arguments.length) scrollbar.scrollTop = value;
    return scrollbar.scrollTop;
  },
  getBoundingClientRect() {
    return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
  }
});
scrollbar.addListener(ScrollTrigger.update);

if (arguments.length) 正是用"是否有参数"来区分这次调用是 setter 还是 getter。

七、Scrub:让动画进度跟随滚动

Scrub 把动画进度与滚动位置强关联,制造"滚动驱动"的顺滑观感:

gsap.to(".box", {
  x: 500,
  scrollTrigger: {
    trigger: ".box",
    start: "top center",
    end: "bottom center",
    scrub: true        // or number (smoothness delay in seconds), so 0.5 means it'd take 0.5 seconds to "catch up" to the current scroll position.
  }
});
  • scrub: true:进度与滚动 1:1 同步;
  • scrub: 1:以秒为单位的"追赶快慢",滚动停止后播放头用约 1 秒才跟上当前位置,产生惯性缓冲感。

八、Pinning:滚动区间内钉住元素

当滚动处于 start~end 区间时,把触发器元素钉在视口上(默认 position: fixed):

scrollTrigger: {
  trigger: ".section",
  start: "top top",
  end: "+=1000",   // pin for 1000px scroll
  pin: true,
  scrub: 1
}
  • pin: true 即钉住 trigger 自身;也可用字符串/元素指定其它目标;
  • 不要直接动画被钉住元素本身,应动画其子元素(否则 transform 与 fixed 定位相互干扰);
  • pinSpacing 默认 true:ScrollTrigger 会插入 spacer 撑开布局,避免元素变 position: fixed 后布局塌陷;只有当你自己处理了布局占位时才设置 pinSpacing: false(或使用 "margin" 模式)。

pin: true 时,start 的默认值从 "top bottom" 变为 "top top"(见核心配置表),因为钉住的起点通常是元素顶部抵达视口顶部。

九、Markers:开发期可视化触发区间

开发时开启 markers 可直观看到 start/end 线:

scrollTrigger: {
  trigger: ".box",
  start: "top center",
  end: "bottom center",
  markers: true
}

进阶形式为对象 { startColor, endColor, fontSize, ... } 以自定义颜色与字号。上线前务必移除或设为 false(见"Do Not"清单)。

十、Timeline + ScrollTrigger:用滚动驱动整条时间线

scrollTrigger 直接挂在 timeline 自身上,则该时间线内所有子动画作为一个整体被滚动区间驱动(可选 scrub 平滑):

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: ".container",
    start: "top top",
    end: "+=2000",
    scrub: 1,
    pin: true
  }
});
tl.to(".a", { x: 100 }).to(".b", { y: 50 }).to(".c", { opacity: 0 });

时间线的整体进度经由 trigger 的 start/end 区间绑定到滚动。结构红线:ScrollTrigger 只能挂在 timeline 或顶层 tween 上,绝不能挂在 timeline 内部的子 tween 上(gsap.timeline().to(".a", { scrollTrigger: {...} }) 是错误写法),也不要把带 ScrollTrigger 的动画嵌套进父级 timeline。

十一、横向伪滚动(containerAnimation)

经典"讲故事式"长页面模式:垂直滚动驱动一个 section 被 pin 住,section 内部的内容沿水平方向平移(即"假的横向滚动")。实现四步(SKILL.md):

  1. Pin 这个全屏面板(trigger = 面板);
  2. 写一个 tween 动画内部容器/包裹层的 xxPercent(例如 x: () => (targets.length - 1) * -window.innerWidth 或负 xPercent 使其左移),该 tween 必须使用 ease: "none"
  3. 给该 tween 挂 ScrollTrigger:pin: truescrub: true
  4. 需要依据横向位移触发其它元素动画时,给那些 ScrollTrigger 设置 containerAnimation 指向该 tween。
const scrollingEl = document.querySelector(".horizontal-el");
// Panel = pinned viewport-sized section. .horizontal-wrap = inner content that moves left.
const scrollTween = gsap.to(scrollingEl, {
  xPercent: () => Max.max(0, window.innerWidth - scrollingEl.offsetWidth),
  ease: "none", // ease: "none" is required
  scrollTrigger: {
    trigger: scrollingEl,
    pin: scrollingEl.parentNode, // wrapper so that we're not animating the pinned element
    start: "top top",
    end: "+=1000"
  }
});

// other tweens that trigger based on horizontal movement should reference the containerAnimation:
gsap.to(".nested-el-1", {
  y: 100,
  scrollTrigger: {
    containerAnimation: scrollTween, // IMPORTANT
    trigger: ".nested-wrapper-1",
    start: "left center", // based on horizontal movement
    toggleActions: "play none none reset"
  }
});

Caveats(本模式高危区)

  • 使用 containerAnimation 的 ScrollTrigger 不支持 pinning 与 snap
  • 容器动画必须 ease: "none",否则滚动位置与横向位移无法保持 1:1——这是最常见的错误;
  • 避免直接横向动画 trigger 元素本身,应动画其子元素;
  • trigger 若被移动,需相应补偿其 start/end。

注意:原文示例中用于计算位移的 Max.max 存在笔误,实际应写作 Math.max(0, window.innerWidth - scrollingEl.offsetWidth),写作时请以 Math.max 为准,含义为"横向可移动的最大距离不会小于 0"。

十二、刷新与清理

  • ScrollTrigger.refresh():在 DOM/布局变化(新增内容、图片、字体加载、动态数据)后重新计算所有触发器位置。视口 resize 时自动触发 refresh(防抖 200ms),动态内容变化则不会自动刷新,必须手动调用。
  • 刷新顺序:refresh 按创建顺序执行(或按 refreshPriority 从小到大)。页面中的 ScrollTrigger 应按从上到下(scroll 0 → max)的顺序创建;异步/动态创建无法保证顺序时,必须为每个实例显式设置 refreshPriority(页面越靠前数字越小),否则 pin spacer 的插入时机可能破坏布局。
  • 销毁清理:SPA 切页或删除元素前要 kill 旧实例,防止它们继续作用于已失效的元素:
ScrollTrigger.getAll().forEach(t => t.kill());
// or kill by the id assigned to the ScrollTrigger in its config object like {id: "my-id", ...}
ScrollTrigger.getById("my-id")?.kill();
  • React 场景:优先使用 useGSAP() hook(来自 @gsap/react NPM 包)让清理自动完成;或手动在 useEffect 返回的 cleanup / gsap.context() 中回收全部动画与 ScrollTrigger(详见 gsap-react)。

十三、官方最佳实践清单

技能文档将最佳实践归纳如下(SKILL.md),可作为每次实现前的自检清单:

  • gsap.registerPlugin(ScrollTrigger) 一次,置于任何 ScrollTrigger 使用之前;
  • ✅ 影响触发器位置的 DOM/布局变化(新内容、图片、字体)之后调用 ScrollTrigger.refresh();视口 resize 已自动处理(防抖 200ms);
  • ✅ React 中使用 useGSAP() hook 确保 ScrollTrigger 与 GSAP 动画随组件卸载被 revert/清理,或在 useEffect/useLayoutEffect cleanup 中用手动 gsap.context() 处理;
  • ✅ 滚动关联进度用 scrub,离散播放/倒放用 toggleActions,同一触发器不要两者并用(若同时存在,scrub 优先);
  • ✅ 基于 containerAnimation 的伪横向滚动,横向 tween/timeline 必须用 ease: "none",保证滚动与横向位置同步;
  • ✅ 按页面出现顺序(顶部→底部,scroll 0 → max)创建 ScrollTrigger;无法保证顺序时给每个实例设置 refreshPriority(页面首个区块数字最小),使 refresh 按页面顺序执行。

十四、常见误区(Do Not 清单)

  • ❌ 把 ScrollTrigger 放在 timeline 的子 tween 上——只能放在 timeline顶层 tween 上。错误:gsap.timeline().to(".a", { scrollTrigger: {...} });正确:gsap.timeline({ scrollTrigger: {...} }).to(".a", { x: 100 })
  • ❌ 忘记在 DOM/布局变化(新内容、图片、字体)后调用 ScrollTrigger.refresh();resize 自动处理,动态内容不会;
  • ❌ 把带 ScrollTrigger 的动画嵌套进父级 timeline —— ScrollTrigger 只能存在于顶层动画;
  • ❌ 使用前忘记 gsap.registerPlugin(ScrollTrigger)
  • ❌ 同一 ScrollTrigger 上同时使用 scrubtoggleActions;只能二选一,两者共存时 scrub 胜出;
  • ❌ 用 containerAnimation 伪横向滚动时给横向动画设置除 "none" 以外的 ease,会破坏滚动→位移的 1:1 映射;
  • ❌ 随机/异步创建 ScrollTrigger 却不设置 refreshPriority;refresh 按创建顺序(或 refreshPriority)执行,错误顺序会影响布局(如 pin spacing)。请自上而下创建或显式分配优先级使其按页面顺序刷新;
  • ❌ 生产环境遗留 markers: true
  • ❌ 布局变化后忘记 refresh()(影响触发器位置的新内容/图片/字体;视口 resize 自动处理)。

十五、进一步阅读

ScrollTrigger 的完整上游文档由 GSAP 官方维护(含全部静态方法、示例与版本差异说明),本技能文件基于其稳定 API 提炼了最常用、最易错的部分,实现复杂需求时可结合官方文档与技能清单交叉核对。

登录后查看全文
热门项目推荐
相关项目推荐