前端开发
使用 GSAP 做滚动叙事、时间线、文字揭示和布局过渡时…
在需要代码驱动 Web 动效(滚动触发、固定 section、文字揭示、布局过渡、平滑滚动同步)时,用 GSAP 插件与统一 ticker 避免不同步、pin 断裂和清理问题。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
使用 GSAP 做滚动叙事、时间线、文字特定
解决思路
- 使用 GSAP 3.12+,此时所有插件(ScrollTrigger、SplitText、Flip、MotionPath、MorphSVG、Draggable、Observer)均可免费用于商业项目。
- 安装并注册插件:`import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { SplitText } from 'gsap/SplitText'; import { Flip } from 'gsap/Flip'; gsap.registerPlugin(ScrollTrigger, SplitText, Flip);`
- 插件必须在使用前注册,否则会静默 no-op;使用 bundler 时在应用入口只注册一次。
- 优先用单个 timeline 而不是多个独立 tween,以获得单一 playhead、相对定位和易于反转:`const tl = gsap.timeline({ defaults: { ease: 'power3.out', duration: 0.6 } }); tl.from('.title', { yPercent: 100, opacity: 0 }).from('.sub', { y: 20, opacity: 0 }, '-=0.3').from('.cta', { scale: 0.9, opacity: 0 }, '<');`
- position 参数用法:`+=0.5` / `-=0.3` 相对 timeline 末尾(间隔/重叠);`<` 表示上一个 tween 开始;`>` 表示上一个 tween 结束;`<0.2` 表示上一个 tween 开始后 0.2s;`myLabel` 表示通过 `tl.addLabel('myLabel')` 添加的命名标签。
- 入场用 `.from()`(从给定值动画到当前 CSS),退场用 `.to()`,两端都要明确时用 `.fromTo()`(对重复运行最稳健)。
- 即时设置用 `gsap.set(el, { autoAlpha: 0 })`(不补间;autoAlpha = opacity + visibility),高频更新用 `const xTo = gsap.quickTo(el, 'x', { duration: 0.4, ease: 'power3' }); window.addEventListener('pointermove', (e) => xTo(e.clientX));`
- 优先使用 `autoAlpha` 而不是原始 `opacity`,因为它在 0 时还会切换 `visibility:hidden`,让元素退出 hit-testing。
- ScrollTrigger 基础写法:`gsap.to('.panel', { xPercent: -100, ease: 'none', scrollTrigger: { trigger: '.wrap', start: 'top top', end: '+=2000', pin: true, scrub: 1, markers: true } });` 生产环境移除 markers。
- `start`/`end` 接受 `'triggerPos viewportPos'`(如 `'top center'`)或 `'+=px'`。
- `scrub: true` 将动画进度精确锁定到滚动;`scrub: <number>` 增加平滑滞后。
- 非 scrubbed 触发器用 `toggleActions: 'play pause resume reverse'` 控制 onEnter/onLeave/onEnterBack/onLeaveBack。
- scrubbed tween 使用 `ease: 'none'`,让运动线性跟随滚动。
- 将 Lenis(或 Locomotive)平滑滚动与 ScrollTrigger 配对时,必须让二者运行在同一循环;否则 ScrollTrigger 读取过期滚动位置导致不同步。
- 用 GSAP ticker 驱动 Lenis,并在每次 Lenis 滚动时更新 ScrollTrigger:`const lenis = new Lenis({ duration: 1.2, smoothWheel: true }); lenis.on('scroll', ScrollTrigger.update); gsap.ticker.add((t) => lenis.raf(t * 1000)); gsap.ticker.lagSmoothing(0);`
- 关键单位:`gsap.ticker` 传入秒,`lenis.raf()` 需要毫秒,因此乘以 1000。
- 不要另外运行独立的 `requestAnimationFrame(raf)` 循环给 Lenis,否则会双重驱动。
- 修复抖动/卡顿:移除与 ticker 竞争的遗留 `requestAnimationFrame(raf)` 循环,并设置 `gsap.ticker.lagSmoothing(0)`。
- 修复 markers 漂移:确保已通过 `lenis.on('scroll', ScrollTrigger.update)` 订阅 ScrollTrigger 更新。
- 修复 pin 断裂:使用 Lenis 时不要设置 `scrollerProxy`(它滚动 document);使用 Locomotive 时必须接好 `scrollerProxy` + `pinType`,并在每个 trigger 上设置 `scroller:`。
- 修复滚动卡住/方向翻转:检查是否缺少 `* 1000` 单位转换,或两个循环顺序不对。
- SPA 卸载清理:执行 `gsap.ticker.remove(update)` + `lenis.destroy()`,否则每次导航都会堆叠循环。
- 用 `prefers-reduced-motion` 做门控:跳过 Lenis,使用原生滚动。
- 完整 Lenis + Locomotive 接线、锚点链接路由、`data-lenis-prevent`、React(`useGSAP`/`useEffect`)及症状→原因表见 `references/scrolltrigger-lenis.md`。
- SplitText 文本揭示:`const split = SplitText.create('.headline', { type: 'lines, words', linesClass: 'line' }); gsap.from(split.lines, { yPercent: 100, opacity: 0, stagger: 0.08, duration: 0.7, ease: 'power4.out' });`
- SplitText 行遮罩揭示:在 line wrapper 上设置 `overflow: hidden`,使 `yPercent: 100` 能干净隐藏;GSAP 3.13+ 加 `autoSplit: true` 以便字体加载/尺寸变化时重新拆分。
- 重新拆分前或卸载时调用 `split.revert()`,恢复原始 DOM,避免重复节点。
- 始终在 Web 字体加载完成后拆分(`document.fonts.ready.then(...)`),防止错误换行。
- Flip 布局过渡:先 `const state = Flip.getState('.item');` 捕获 BEFORE;再切换/修改 DOM/CSS(`container.classList.toggle('grid');`);最后 `Flip.from(state, { duration: 0.6, ease: 'power2.inOut', stagger: 0.05, absolute: true });` 动画化差异。
- Flip 中 `absolute: true` 在移动期间将元素脱离文档流,防止 reflow 抖动。
- 缓动选择:`power2/3.out` 用于入场(快后稳);`power2.inOut` 用于屏幕内两个状态间移动;`back.out(1.7)` 用于过冲/弹出(数字为过冲量);`elastic.out(1, 0.3)` 用于弹跳但少用;`none` 用于 scrubbed 滚动 tween;`steps(n)` 用于逐帧/阶梯运动;自定义 cubic-bezier 等价写法:`CustomEase.create('x', 'M0,0 C0.2,0 0,1 1,1')`(CustomEase 免费)。
- 性能:只动画 `transform`(`x/y/xPercent/scale/rotation`)和 `opacity`,它们由 GPU 合成,跳过 layout/paint;避免动画 `top/left/width/height`。
- 对重型/固定元素设置 `will-change: transform`,之后移除。
- 大量相似滚动揭示用 `ScrollTrigger.batch()`,而不是每个元素一个 trigger。
- 视口或内容变化后调用 `ScrollTrigger.refresh()`。
- SPA/框架清理必须做:用 `gsap.context()`(或 `@gsap/react` 的 `useGSAP`)包裹所有 gsap + ScrollTrigger 代码,并在 `return () => ctx.revert();` 中清理,杀死 tween、trigger 并还原内联样式。
- 交付独立 HTML 时,目标是可直接在浏览器打开的单个 HTML 文件:无构建步骤、无框架、无渲染管线;按工作重量匹配交付物,单个文件适合 web motion,不要动辄上 bundler。
- 输出契约:一个 `.html` 文件,GSAP + 插件通过 CDN 引入,标记和动画放在一个内联 `<script>` 中;所有运动放在一个 master timeline(`const tl = gsap.timeline()`),只有一个可 seek 的 playhead;包含 seek harness。
- 加入 seek harness:`const t = new URLSearchParams(location.search).get('t'); if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); } window.__ready = true; console.log('duration', tl.duration());` 无 `?t` 时正常播放,`?t=N` 冻结到 N 秒。
- 使用打包 helper:`scripts/seek-shot.sh anim.html 0 1.5 3` 冻结 `?t=N` harness 并截图每个时刻;`scripts/contact-sheet.sh sheet.png frame-*.png` 平铺成一张总览图;详见 `scripts/README.md`。
- 验证循环:在开始、中间、结束三个时刻打开文件(`anim.html?t=0`、`?t=<dur/2>`、`?t=<dur>`;从 console 读取 `tl.duration()` 得到结束时间)。
- 截图每个冻结帧。
- 检查 fidelity(是否符合 brief)和 artifacts(文字裁切、元素出画布、字体加载前 FOUC、接缝卡顿);输出应看起来有意且完整。
- 任意 headless 截图工具都可用,例如 Playwright:`npx playwright screenshot --wait-for-timeout=500 'file://$PWD/anim.html?t=1.2' frame-mid.png`。
- 完成前检查:能在浏览器独立打开,无 console 错误,无缺失 CDN。
- 完成前检查:所有动画在一个 master timeline;`?t=N` 正确冻结。
- 完成前检查:已在开始/中间/结束截图;符合 brief 且无 artifacts。
- 完成前检查:遵守 `prefers-reduced-motion`(简化或跳过 timeline)。
- 完成前检查:缓动是有意选择的;空间运动上没有意外的 `linear`。
- 快速 API 映射:序列重叠 `tl.from(...).from(..., '-=0.3')`;滚动固定 `scrollTrigger:{ pin:true, end:'+=N' }`;滚动 scrub `scrollTrigger:{ scrub:1, ease:'none' }`;文字行揭示 `SplitText.create(el,{type:'lines'})`;布局变化动画 `Flip.getState` → 修改 → `Flip.from`;快速指针跟随 `gsap.quickTo(el,'x',{...})`;杀死全部 `ScrollTrigger.getAll().forEach(t=>t.kill())`。
- 参考文件:`references/scrolltrigger-cookbook.md` 包含 pin、scrub、水平滚动、snap、`ScrollTrigger.batch()` 揭示、视差、嵌套 trigger 和 SPA 清理完整代码;`references/scrolltrigger-lenis.md` 包含完整平滑滚动同步;`examples/hero-timeline.js` 是可运行 hero 入场 timeline;`examples/standalone-template.html` 是独立交付模板,带 `?t=N` seek harness 和 reduced-motion 处理。
适用边界 · 注意事项
- 当只需简单 hover/focus 或一次性 CSS transition 能完成时,不适用,避免引入 GSAP 与插件。
- 当项目无法加载第三方库/CDN 或要求零 JS 动画时,不适用。
- 当必须使用已有动画框架且不能混用额外 ticker/ScrollTrigger 时,不适用,避免循环冲突。