gsap-web

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GSAP for the Web

GSAP 用于Web开发

GSAP (GreenSock Animation Platform) is the workhorse for code-driven web motion: sequenced timelines, scroll-driven storytelling, text reveals, and layout transitions. As of GSAP 3.12+, every plugin (ScrollTrigger, SplitText, Flip, MotionPath, MorphSVG, Draggable, Observer) is 100% free, including for commercial use.
GSAP(GreenSock动画平台)是代码驱动Web动效的主力工具:支持序列时间线、滚动驱动叙事、文本渐显以及布局过渡。从GSAP 3.12版本开始,所有插件(ScrollTrigger、SplitText、Flip、MotionPath、MorphSVG、Draggable、Observer)均完全免费,包括商业用途。

When to use

适用场景

  • Scroll-driven storytelling: pinned sections, parallax, progress scrubbing, horizontal scroll
  • Sequenced hero animations and complex multi-element timelines with precise overlap control
  • Text reveals split into chars/words/lines (SplitText)
  • Layout transitions where an element changes position/size/parent (Flip)
  • Any motion needing fine timing control, easing precision, or imperative orchestration
  • 滚动驱动叙事:固定区块、视差效果、进度绑定滚动、横向滚动
  • 首屏序列动画及复杂多元素时间线,支持精确的重叠控制
  • 文本拆分逐字/逐行/逐段渐显(SplitText)
  • 元素位置/尺寸/父容器变化时的布局过渡(Flip)
  • 任何需要精细时间控制、缓动精度或命令式编排的动效

Install and register

安装与注册

js
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { Flip } from "gsap/Flip";

gsap.registerPlugin(ScrollTrigger, SplitText, Flip);
Plugins MUST be registered before use or they silently no-op. With a bundler, register once at app entry.
js
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { Flip } from "gsap/Flip";

gsap.registerPlugin(ScrollTrigger, SplitText, Flip);
插件必须在使用前注册,否则会静默失效。使用打包工具时,只需在应用入口处注册一次。

Core techniques

核心技巧

Timelines first

优先使用时间线

Prefer one timeline over many independent tweens — it gives a single playhead, relative positioning, and easy reversal.
js
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")  // start 0.3s before prev ends
  .from(".cta",   { scale: 0.9, opacity: 0 }, "<"); // align to prev tween START
Position parameter cheat sheet:
  • "+=0.5"
    /
    "-=0.3"
    — relative to the end of the timeline (gap / overlap)
  • "<"
    — start of the previous tween;
    ">"
    — end of the previous tween
  • "<0.2"
    — 0.2s after the previous tween's start
  • "myLabel"
    — at a named label added via
    tl.addLabel("myLabel")
Use
.from()
for entrances (animates FROM the given values TO current CSS),
.to()
for exits,
.fromTo()
when both ends must be explicit (most robust against re-runs).
优先使用单个时间线而非多个独立补间动画——它提供统一的播放头、相对定位和便捷的反转功能。
js
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")  // 在前一个动画结束前0.3秒开始
  .from(".cta",   { scale: 0.9, opacity: 0 }, "<"); // 与前一个动画的开始时间对齐
位置参数速查:
  • "+=0.5"
    /
    "-=0.3"
    —— 相对于时间线的结束位置(间隔 / 重叠)
  • "<"
    —— 前一个补间的开始;
    ">"
    —— 前一个补间的结束
  • "<0.2"
    —— 前一个补间开始后0.2秒
  • "myLabel"
    —— 在通过
    tl.addLabel("myLabel")
    添加的命名标签位置
使用
.from()
实现入场动画(从给定值动画到当前CSS状态),
.to()
实现退场动画,
.fromTo()
用于需要明确两端状态的场景(在重复运行时最稳定)。

gsap.to vs set vs quickTo

gsap.to vs set vs quickTo

js
gsap.set(el, { autoAlpha: 0 });           // instant, no tween (autoAlpha = opacity + visibility)
const xTo = gsap.quickTo(el, "x", { duration: 0.4, ease: "power3" });
window.addEventListener("pointermove", (e) => xTo(e.clientX)); // fast repeated updates
autoAlpha
is preferred over raw
opacity
because it also toggles
visibility:hidden
at 0, removing the element from hit-testing.
js
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
,将元素从命中测试中移除。

ScrollTrigger basics

ScrollTrigger 基础

js
gsap.to(".panel", {
  xPercent: -100,
  ease: "none",
  scrollTrigger: {
    trigger: ".wrap",
    start: "top top",      // when trigger top hits viewport top
    end: "+=2000",         // 2000px of scroll distance
    pin: true,             // freeze .wrap while the tween plays
    scrub: 1,              // tie progress to scrollbar (1 = 1s catch-up smoothing)
    markers: true,         // dev-only visual markers — remove for prod
  },
});
Key semantics:
  • start
    /
    end
    take
    "triggerPos viewportPos"
    (e.g.
    "top center"
    ) or
    "+=px"
    .
  • scrub: true
    locks animation progress to scroll exactly;
    scrub: <number>
    adds smoothing lag.
  • toggleActions: "play pause resume reverse"
    controls onEnter/onLeave/onEnterBack/onLeaveBack for non-scrubbed triggers.
  • Use
    ease: "none"
    for scrubbed tweens so motion tracks scroll linearly.
js
gsap.to(".panel", {
  xPercent: -100,
  ease: "none",
  scrollTrigger: {
    trigger: ".wrap",
    start: "top top",      // 当触发器顶部碰到视口顶部时
    end: "+=2000",         // 2000px的滚动距离
    pin: true,             // 在补间动画播放时冻结.wrap元素
    scrub: 1,              // 将动画进度与滚动条绑定(1 = 1秒的追赶平滑效果)
    markers: true,         // 开发专用可视化标记——生产环境需移除
  },
});
关键语义:
  • start
    /
    end
    接受
    "triggerPos viewportPos"
    (例如
    "top center"
    )或
    "+=px"
    格式。
  • scrub: true
    将动画进度完全锁定到滚动位置;
    scrub: <number>
    添加平滑延迟。
  • toggleActions: "play pause resume reverse"
    控制非滚动绑定触发器的进入/离开/反向进入/反向离开行为。
  • 滚动绑定的补间动画使用
    ease: "none"
    ,使动效与滚动线性同步。

Smooth scroll (Lenis) + ScrollTrigger

平滑滚动(Lenis)+ ScrollTrigger

Pairing Lenis (or Locomotive) smooth scrolling with ScrollTrigger desyncs unless both run on one loop. Lenis smooths scroll on its own rAF while ScrollTrigger reads scroll on GSAP's ticker; when they tick independently ScrollTrigger samples a stale position. The fix: drive Lenis from GSAP's ticker and update ScrollTrigger on every Lenis scroll.
js
import Lenis from "lenis";

const lenis = new Lenis({ duration: 1.2, smoothWheel: true });

lenis.on("scroll", ScrollTrigger.update);          // 1. update ScrollTrigger on every Lenis scroll
gsap.ticker.add((t) => lenis.raf(t * 1000));       // 2. one loop: ticker drives Lenis (seconds → ms)
gsap.ticker.lagSmoothing(0);                        // 3. stop GSAP catch-up jitter on heavy frames
Critical:
gsap.ticker
passes time in seconds,
lenis.raf()
wants milliseconds — multiply by 1000. Do NOT also run a standalone
requestAnimationFrame(raf)
loop for Lenis; that double-drives it.
Common bugs:
  • Jitter/stutter — a leftover
    requestAnimationFrame(raf)
    loop competing with the ticker, or missing
    lagSmoothing(0)
    . Remove the rogue loop; set lag smoothing to 0.
  • Markers drift from their triggers —
    ScrollTrigger.update
    is not subscribed to
    lenis.on("scroll", …)
    .
  • Pins break — under Lenis do NOT set a
    scrollerProxy
    (it scrolls the document). Under Locomotive you MUST wire
    scrollerProxy
    +
    pinType
    and set
    scroller:
    on every trigger.
  • Scroll stuck / flipped direction — missing the
    * 1000
    unit conversion, or two loops out of order.
Cleanup on SPA unmount:
gsap.ticker.remove(update)
+
lenis.destroy()
, else loops stack per navigation. Gate behind
prefers-reduced-motion
(skip Lenis, run native scroll). Full Lenis + Locomotive wiring, anchor-link routing,
data-lenis-prevent
, React (
useGSAP
/
useEffect
), and a symptom→cause table are in
references/scrolltrigger-lenis.md
.
将Lenis(或Locomotive)平滑滚动与ScrollTrigger结合时,若两者不在同一循环中运行会导致不同步。Lenis在自身的rAF中处理平滑滚动,而ScrollTrigger在GSAP的ticker中读取滚动位置;当两者独立运行时,ScrollTrigger会读取到过时的位置。解决方法:通过GSAP的ticker驱动Lenis,并在每次Lenis滚动时更新ScrollTrigger。
js
import Lenis from "lenis";

const lenis = new Lenis({ duration: 1.2, smoothWheel: true });

lenis.on("scroll", ScrollTrigger.update);          // 1. 每次Lenis滚动时更新ScrollTrigger
gsap.ticker.add((t) => lenis.raf(t * 1000));       // 2. 统一循环:ticker驱动Lenis(秒 → 毫秒)
gsap.ticker.lagSmoothing(0);                        // 3. 在高负载帧时停止GSAP的追赶抖动
关键注意事项:
gsap.ticker
传递的时间单位是
lenis.raf()
需要毫秒——需乘以1000。不要同时为Lenis运行独立的
requestAnimationFrame(raf)
循环,否则会导致双重驱动。
常见问题:
  • 抖动/卡顿 —— 存在与ticker冲突的残留
    requestAnimationFrame(raf)
    循环,或未设置
    lagSmoothing(0)
    。移除多余循环;将延迟平滑设置为0。
  • 标记偏移 —— 未将
    ScrollTrigger.update
    订阅到
    lenis.on("scroll", …)
  • 固定功能失效 —— 在Lenis下不要设置
    scrollerProxy
    (它滚动整个文档)。在Locomotive下必须配置
    scrollerProxy
    +
    pinType
    ,并为每个触发器设置
    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 (text reveal)

SplitText(文本渐显)

js
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" });
Gotchas:
  • Wrap line-mask reveals: set
    overflow: hidden
    on the line wrapper so
    yPercent: 100
    hides cleanly. Add
    autoSplit: true
    (GSAP 3.13+) to re-split on font load / resize.
  • Call
    split.revert()
    before re-splitting or on unmount to restore original DOM and avoid duplicated nodes.
  • Always split AFTER web fonts load (
    document.fonts.ready.then(...)
    ) to prevent wrong line breaks.
js
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" });
注意事项:
  • 逐行遮罩渐显:为行容器设置
    overflow: hidden
    ,使
    yPercent: 100
    能完全隐藏文本。添加
    autoSplit: true
    (GSAP 3.13+)可在字体加载/窗口 resize 时重新拆分文本。
  • 在重新拆分或卸载前调用
    split.revert()
    ,恢复原始DOM结构,避免节点重复。
  • 务必在网页字体加载完成后再拆分文本(
    document.fonts.ready.then(...)
    ),防止换行错误。

Flip (layout transitions)

Flip(布局过渡)

Flip records state, lets the DOM change instantly, then animates the visual difference (FLIP technique). Ideal for grid<->list, expanding cards, and reparenting.
js
const state = Flip.getState(".item");   // 1. capture BEFORE
container.classList.toggle("grid");      // 2. mutate DOM/CSS (instant)
Flip.from(state, {                        // 3. animate the delta
  duration: 0.6, ease: "power2.inOut", stagger: 0.05,
  absolute: true,                         // take items out of flow during move (prevents reflow jitter)
});
Flip会记录元素状态,允许DOM即时变化,然后动画展示视觉差异(FLIP技术)。非常适合网格<->列表切换、卡片展开以及元素重父化场景。
js
const state = Flip.getState(".item");   // 1. 捕获变化前的状态
container.classList.toggle("grid");      // 2. 即时修改DOM/CSS
Flip.from(state, {                        // 3. 动画展示差异
  duration: 0.6, ease: "power2.inOut", stagger: 0.05,
  absolute: true,                         // 移动过程中将元素移出文档流(防止重排抖动)
});

Easing quick guide

缓动速查

  • power2/3.out
    — entrances (fast then settle)
  • power2.inOut
    — moves between two on-screen states
  • back.out(1.7)
    — overshoot/pop (number = overshoot amount)
  • elastic.out(1, 0.3)
    — bouncy, use sparingly
  • none
    — scrubbed scroll tweens
  • steps(n)
    — sprite/stepped motion
  • Custom cubic-bezier equivalent:
    CustomEase.create("x", "M0,0 C0.2,0 0,1 1,1")
    (CustomEase is free)
  • power2/3.out
    —— 入场动画(快速启动后减速)
  • power2.inOut
    —— 屏幕内状态切换动画
  • back.out(1.7)
    —— 过冲/弹出效果(数值表示过冲量)
  • elastic.out(1, 0.3)
    —— 弹性效果,谨慎使用
  • none
    —— 滚动绑定的补间动画
  • steps(n)
    —— 帧动画/步进动效
  • 自定义贝塞尔曲线等价方案:
    CustomEase.create("x", "M0,0 C0.2,0 0,1 1,1")
    (CustomEase免费)

Performance and cleanup

性能与清理

  • Animate
    transform
    (
    x/y/xPercent/scale/rotation
    ) and
    opacity
    only — they are GPU-composited and skip layout/paint. Avoid animating
    top/left/width/height
    .
  • Set
    will-change: transform
    on heavy/pinned elements; remove after.
  • Batch many similar scroll reveals with
    ScrollTrigger.batch()
    instead of one trigger per element.
  • After a viewport/content change, call
    ScrollTrigger.refresh()
    .
SPA / framework cleanup is mandatory — orphaned triggers cause memory leaks and ghost pinning. Use
gsap.context()
(or
useGSAP
from
@gsap/react
):
js
useEffect(() => {
  const ctx = gsap.context(() => {
    // all gsap + ScrollTrigger code here
  }, rootRef);
  return () => ctx.revert(); // kills tweens, triggers, and reverts inline styles
}, []);
  • 仅动画
    transform
    x/y/xPercent/scale/rotation
    )和
    opacity
    ——它们由GPU合成,跳过布局/绘制阶段。避免动画
    top/left/width/height
  • 对复杂/固定元素设置
    will-change: transform
    ;动画结束后移除该属性。
  • 使用
    ScrollTrigger.batch()
    批量处理多个相似的滚动渐显效果,而非为每个元素单独创建触发器。
  • 视口/内容变化后,调用
    ScrollTrigger.refresh()
SPA/框架中的清理操作至关重要——孤立的触发器会导致内存泄漏和幽灵固定问题。使用
gsap.context()
(或
@gsap/react
中的
useGSAP
):
js
useEffect(() => {
  const ctx = gsap.context(() => {
    // 所有gsap + ScrollTrigger代码写在此处
  }, rootRef);
  return () => ctx.revert(); // 销毁补间、触发器并恢复内联样式
}, []);

Deliver & verify (standalone HTML)

交付与验证(独立HTML)

Packaged helper (
scripts/
):
scripts/seek-shot.sh anim.html 0 1.5 3
freezes the
?t=N
harness and screenshots each moment;
scripts/contact-sheet.sh sheet.png frame-*.png
tiles them for one-glance review. See
scripts/README.md
.
For a self-contained animation (hero, reveal, loop, micro-scene) the deliverable is one HTML file that opens directly in a browser — no build step, no framework, no render pipeline. Match the deliverable to the weight of the work: a single file is the right tier for web motion; don't reach for a bundler when one file does the job.
Output contract:
  • One
    .html
    file: GSAP + plugins from CDN, your markup, and the animation in one inline
    <script>
    .
  • All motion on one master timeline (
    const tl = gsap.timeline()
    ) — a single playhead you can seek.
  • Include the seek harness below so any moment can be frozen for inspection.
Seek harness — freeze an exact moment for screenshots. The web parallel of a video player's frame-pin:
?t=N
seeks the master timeline to
N
seconds and pauses, so a screenshot lands on a still, deterministic frame.
html
<script>
  // ... build your master timeline as `tl` ...
  const t = new URLSearchParams(location.search).get("t");
  if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); }  // frozen at t seconds
  // no ?t → plays normally
  window.__ready = true;            // ready signal for headless wait
  console.log("duration", tl.duration());
</script>
Verify loop — render → freeze → screenshot → check:
  1. Open the file at three moments across the timeline — start, mid, end:
    …/anim.html?t=0
    ,
    ?t=<dur/2>
    ,
    ?t=<dur>
    . Read
    tl.duration()
    from the console for the end time.
  2. Screenshot each frozen frame.
  3. Check both fidelity (does it match the brief?) and artifacts (clipped text, elements off-canvas, FOUC before fonts load, jank at seams). Output should look intentional and finished.
Any headless screenshot tool works — your agent's browser tool, or Playwright:
bash
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=1.2" frame-mid.png
Before you finish:
  1. Opens standalone in a browser — no console errors, no missing CDN.
  2. All animation on one master timeline;
    ?t=N
    freezes correctly.
  3. Screenshotted at start / mid / end — matches the brief, no artifacts.
  4. prefers-reduced-motion
    honored (timeline simplified or skipped).
  5. Easing is intentional — no accidental
    linear
    on spatial motion.
A complete runnable template with the harness wired in is in
examples/standalone-template.html
.
打包工具
scripts/
):
scripts/seek-shot.sh anim.html 0 1.5 3
会冻结
?t=N
控制的动画并截取每个时刻的截图;
scripts/contact-sheet.sh sheet.png frame-*.png
将截图拼接成一张预览图。详见
scripts/README.md
对于独立动画(首屏、渐显、循环、微型场景),交付物应为可直接在浏览器中打开的单个HTML文件——无需构建步骤、无需框架、无需渲染流水线。交付物的复杂度应与工作量匹配:单个文件是Web动效的合适交付形式;能通过单个文件完成的工作,无需使用打包工具。
输出规范:
  • 单个
    .html
    文件:包含CDN引入的GSAP+插件、你的标记代码以及内联
    <script>
    中的动画逻辑。
  • 所有动效基于单个主时间线
    const tl = gsap.timeline()
    )——可通过单个播放头控制进度。
  • 包含以下时间线定位工具,以便冻结任意时刻进行检查。
时间线定位工具——冻结精确时刻用于截图。 类似视频播放器的帧锁定功能:
?t=N
会将主时间线定位到第N秒并暂停,确保截图是静态的、确定的帧。
html
<script>
  // ... 在此构建主时间线`tl` ...
  const t = new URLSearchParams(location.search).get("t");
  if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); }  // 冻结在第t秒
  // 无?t参数时正常播放
  window.__ready = true;            // 无头工具就绪信号
  console.log("duration", tl.duration());
</script>
验证流程——渲染→冻结→截图→检查:
  1. 在时间线的三个时刻打开文件:开始、中间、结束:
    …/anim.html?t=0
    ,
    ?t=<dur/2>
    ,
    ?t=<dur>
    。从控制台读取
    tl.duration()
    获取结束时间。
  2. 截取每个冻结帧的截图。
  3. 检查还原度(是否符合需求?)和瑕疵(文本截断、元素超出画布、字体加载前的FOUC、接缝处卡顿)。输出结果应看起来是精心设计且完整的。
任何无头截图工具均可使用——你的Agent浏览器工具,或Playwright:
bash
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=1.2" frame-mid.png
交付前检查:
  1. 可在浏览器中独立打开——无控制台错误、无CDN缺失。
  2. 所有动效基于单个主时间线;
    ?t=N
    能正确冻结。
  3. 已在开始/中间/结束时刻截图——符合需求,无瑕疵。
  4. 遵循
    prefers-reduced-motion
    (简化或跳过时间线)。
  5. 缓动效果符合预期——空间动效无意外的
    linear
    (线性)缓动。
已配置好定位工具的完整可运行模板请参考
examples/standalone-template.html

Quick reference

速查表

GoalAPI
Sequence with overlap
tl.from(...).from(..., "-=0.3")
Pin while scrolling
scrollTrigger:{ pin:true, end:"+=N" }
Scrub to scroll
scrollTrigger:{ scrub:1, ease:"none" }
Reveal lines of text
SplitText.create(el,{type:"lines"})
Animate layout change
Flip.getState
→ mutate →
Flip.from
Fast pointer follow
gsap.quickTo(el,"x",{...})
Kill everything
ScrollTrigger.getAll().forEach(t=>t.kill())
目标API
带重叠的序列动画
tl.from(...).from(..., "-=0.3")
滚动时固定元素
scrollTrigger:{ pin:true, end:"+=N" }
动画进度绑定滚动
scrollTrigger:{ scrub:1, ease:"none" }
逐行文本渐显
SplitText.create(el,{type:"lines"})
布局变化动画
Flip.getState
→ 修改DOM →
Flip.from
快速指针跟随
gsap.quickTo(el,"x",{...})
销毁所有触发器
ScrollTrigger.getAll().forEach(t=>t.kill())

Reference files

参考文件

  • references/scrolltrigger-cookbook.md
    — pin, scrub, horizontal scroll, snap,
    ScrollTrigger.batch()
    reveals, parallax, nested triggers, and SPA cleanup patterns with full code.
  • references/scrolltrigger-lenis.md
    — full ScrollTrigger + Lenis/Locomotive smooth-scroll sync: canonical wiring,
    scrollerProxy
    , anchor links, reduced-motion, React (
    useGSAP
    /
    useEffect
    ), debugging checklist, and symptom→cause table.
  • examples/hero-timeline.js
    — a complete, runnable hero entrance timeline with SplitText, staggered reveals, and reduced-motion handling.
  • examples/standalone-template.html
    — the deliverable template: self-contained (CDN GSAP), one master timeline, the
    ?t=N
    seek harness for screenshot verification, and reduced-motion handling.
  • references/scrolltrigger-cookbook.md
    —— 固定、滚动绑定、横向滚动、吸附、
    ScrollTrigger.batch()
    渐显、视差、嵌套触发器以及SPA清理模式的完整代码。
  • references/scrolltrigger-lenis.md
    —— ScrollTrigger + Lenis/Locomotive平滑滚动同步的完整指南:标准配置、
    scrollerProxy
    、锚点链接、减少动效适配、React(
    useGSAP
    /
    useEffect
    )、调试清单以及问题排查表。
  • examples/hero-timeline.js
    —— 完整可运行的首屏入场时间线,包含SplitText、交错渐显和减少动效处理。
  • examples/standalone-template.html
    —— 交付模板:独立运行(CDN引入GSAP)、单个主时间线、用于截图验证的
    ?t=N
    定位工具以及减少动效处理。