从补间到叙事:GSAP 动画实战 · 公开阅读
第6章:Stagger 交错动画
调整间隔与起始位置,观察网格元素的交错动画。
本章小节
6.1 stagger 是什么
想象一个合唱团——指挥一挥手,所有成员不是同时站起来,而是从第一排到最后一排依次起立。这种"有先有后"的节奏感,就是 stagger 做出来的效果。
没有 stagger 时,10 个元素同时动画——齐刷刷,像机器人列队:
gsap.to(".item", { y: -20, opacity: 1, duration: 0.5 });
// 10 个 .item 齐刷刷一起动加上 stagger,10 个元素就有了"呼吸感":
gsap.to(".item", { y: -20, opacity: 1, duration: 0.5, stagger: 0.1 });
// 10 个 .item 依次动,每个间隔 0.1 秒stagger 让一组元素按固定间隔依次执行动画,产生有节奏的视觉序列效果。
6.2 数值 stagger——最简单的起步
一个数字就能搞定,最容易上手:
gsap.to(".card", {
y: 0,
opacity: 1,
duration: 0.6,
stagger: 0.15, // 每个元素间隔 0.15 秒
});| stagger 值 | 行为 |
|---|---|
0.1 | 元素 1 开始 → 0.1s 后元素 2 开始 → 0.1s 后元素 3 开始 |
0 | 所有元素同时开始(等于没加 stagger) |
-0.1 | 负值等于反向顺序,但绝对值仍是间隔 |
6.3 对象 stagger——全参数详解
只有一个数字的话,控制粒度太粗了。对象形式的 stagger 有 4 个参数联合作战:
gsap.to(".card", {
y: 0,
opacity: 1,
duration: 0.6,
stagger: {
each: 0.15, // 每个元素的间隔时间
from: "center", // 从哪个元素开始
grid: "auto", // 网格模式
amount: 1.2, // 总时间约束
ease: "power2.inOut", // stagger 自身的缓动
}
});6.3.1 each——间隔时间
stagger: { each: 0.15 }
// 等价于 stagger: 0.156.3.2 amount——总时间约束
当不确定间隔应该设多少,但需要所有元素在固定时间内全部启动时,用 amount:
stagger: { amount: 1.5 }
// 不管有多少个元素,1.5 秒内全部开始
// GSAP 自动计算 each = 1.5 / (元素数量 - 1)each 和 amount 互斥——你只需指定其中一个:
// 方式一:精确控制每个间隔
stagger: { each: 0.1 }
// 方式二:精确控制总时间
stagger: { amount: 2 }| 参数 | 适用场景 |
|---|---|
each | 元素数量不固定,希望每个元素有固定的间隔 |
amount | 希望所有元素在规定时间内全部启动 |
把这一节跑一遍:each 与 amount
8 个元素从索引 0 开始。each=0.15 时启动跨度为 7×0.15=1.05 秒;amount=0.15 时整个启动跨度仅 0.15 秒。amount 不包含每个元素自己的 duration。
实验:each 与 amount
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:把同一个 0.15 分别当 each/amount,读取启动时刻列表。默认单个 duration=2,整组结束分别为 3.05 与 2.15 秒。
6.3.3 from——6 种起始位置
from 参数决定交错动画从哪个元素开始:
stagger: { from: "center" } // 从中间往两边扩散
stagger: { from: "edges" } // 从两端往中间收拢
stagger: { from: "end" } // 从最后一个开始
stagger: { from: "start" } // 从第一个开始(默认)
stagger: { from: "random"} // 随机顺序
stagger: { from: 3 } // 从索引 3 的元素开始视觉效果对比
// 不同 from 值的效果对比
// 从左到右依次出现(默认)
gsap.to(".btn", { y: 0, opacity: 1, duration: 0.4, stagger: { each: 0.1, from: "start" } });
// 从右到左依次出现
gsap.to(".btn", { y: 0, opacity: 1, duration: 0.4, stagger: { each: 0.1, from: "end" } });
// 从中间向两边扩散
gsap.to(".btn", { y: 0, opacity: 1, duration: 0.4, stagger: { each: 0.1, from: "center" } });
// 从两边向中间收拢
gsap.to(".btn", { y: 0, opacity: 1, duration: 0.4, stagger: { each: 0.1, from: "edges" } });from: index 用法
当你需要从特定元素开始时:
// 从第 2 个元素(索引为 1)开始
gsap.to(".item", { scale: 1, stagger: { each: 0.1, from: 1 } });6.4 grid 模式——按行列展开
处理 2D 网格布局(照片墙、卡片矩阵)时,grid 参数让交错按行或列展开:
gsap.to(".grid-item", {
scale: 1,
opacity: 1,
duration: 0.5,
stagger: {
each: 0.05,
grid: [4, 5], // [行数, 列数] → 4 行 5 列的网格
from: "center",
}
});grid 详解
grid: [3, 4] // 3 行,每行 4 个元素,共 12 个
grid: "auto" // GSAP 自动感应网格
grid: [4, 5]当使用 grid: "auto" 时,GSAP 会根据元素的位置(getBoundingClientRect)自动判断行列结构。但如果你知道确切的行列数,手动指定 [row, col] 更可靠——不会有自动检测不准的意外。
grid + from 组合效果
// 从左上角开始,逐行展开
stagger: { grid: [3, 4], from: "start" }
// 从中间行向外展开
stagger: { grid: [3, 4], from: "center" }
// 从右下角开始(反向)
stagger: { grid: [3, 4], from: "end" }
// 从网格中心点辐射
stagger: { grid: [3, 4], from: [1, 1] } // 从第 1 行第 1 列开始把这一节跑一遍:网格交错与起点
grid 定义距离计算的行列结构,from 决定距离原点。DOM 排序、显示布局与 grid 不一致时,视觉顺序会让人误以为延迟算错。stagger.ease 影响启动分布,Tween ease 影响每个目标的运动。
实验:网格交错与起点
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:选择 center、edges、end,看最先启动的单元。将每个元素运动变慢,更容易分清“谁先开始”和“开始后怎么动”。
6.5 stagger 与 Timeline 结合
在 Timeline 中,stagger 让交错序列作为时间线的一个"片段"——导航先依次落下,然后标题滑入,最后卡片展开:
const tl = gsap.timeline();
tl.from(".nav-item", {
y: -20,
opacity: 0,
duration: 0.4,
stagger: 0.1, // 导航项依次下落
})
.from(".hero-title", {
y: 50,
opacity: 0,
duration: 0.8,
}, "-=0.2") // 导航还没播完就提前开始标题
.from(".card", {
y: 30,
opacity: 0,
duration: 0.5,
stagger: { each: 0.08, from: "center" }, // 卡片从中间向两边展开
}, "-=0.4");交错在整个 Timeline 中的位置
把时间轴拉直了看:
时间线: |---导航交错(0.4+9*0.1=1.3s)---|---标题(0.8s)---|---卡片交错...---|
↑ -=0.2 让标题提前 ↑ -=0.4 让卡片提前stagger 会为每个目标元素创建一个独立 tween,并按 stagger 值偏移各自的开始时间。
6.6 实战示例
示例一:菜单项依次入场
gsap.from(".menu-item", {
x: -40,
opacity: 0,
duration: 0.5,
stagger: {
each: 0.08,
from: "start",
},
ease: "power2.out",
});示例二:卡片瀑布式出现
gsap.fromTo(".card",
{ y: 100, opacity: 0, rotation: 5 },
{
y: 0,
opacity: 1,
rotation: 0,
duration: 0.7,
stagger: {
each: 0.12,
from: "start",
ease: "power3.out", // stagger 自身的缓动
},
ease: "back.out(1.4)", // 每个卡片的缓动
}
);注意:这里有两个 ease——外层的 ease: "back.out(1.4)" 控制每个卡片动画的缓动;内层 stagger.ease 控制 stagger 分布密度(前面的元素间隔短,后面的间隔长)。
stagger.ease 的效果:
stagger: {
each: 0.1,
ease: "power2.in",
// 前面的元素间隔小(紧密),越往后间隔越大(稀疏)
}示例三:文字逐字动画
// HTML: <h1 class="split-text">Hello World</h1>
// JS 中将每个字拆成 span
const text = document.querySelector(".split-text");
const chars = text.textContent.split("");
text.innerHTML = chars.map(c => `<span>${c}</span>`).join("");
gsap.from(".split-text span", {
y: 80,
opacity: 0,
rotationX: -90,
duration: 0.6,
stagger: 0.05,
ease: "back.out(1.7)",
});示例四:网格卡片——从中心辐射
gsap.from(".gallery-item", {
scale: 0,
opacity: 0,
duration: 0.5,
stagger: {
amount: 1.5,
grid: [3, 4],
from: "center",
},
ease: "back.out(2)",
});示例五:交替方向——左右错落入场
gsap.from(".feature-item", {
x: (i) => i % 2 === 0 ? -100 : 100,
opacity: 0,
duration: 0.8,
stagger: 0.15,
ease: "power3.out",
});
// 通过回调函数,偶数项从左边来,奇数项从右边来6.7 常见陷阱
陷阱一:stagger 与 repeat 不是"各走各的"
现象:设了 stagger 和 repeat: -1,期望每个元素独立循环——但行为完全不对。
原因:外层 repeat 重复整组动画。需要每个目标独立往返时,可把 repeat、yoyo 写进 stagger 对象,例如 stagger: { each:0.1, repeat:1, yoyo:true };也可为各目标创建独立 Tween。
// 错误期望:每个 .item 独立重复
gsap.to(".item", {
y: -10,
repeat: -1,
yoyo: true,
stagger: 0.1,
duration: 0.3,
});
// 实际行为:整个 tween 完成后再重复,不是每个 item 独立循环解决:如果每个元素都需要独立循环,必须给每个元素单独创建 tween:
document.querySelectorAll(".item").forEach((el, i) => {
gsap.to(el, {
y: -10,
repeat: -1,
yoyo: true,
duration: 0.3,
delay: i * 0.1,
});
});陷阱二:grid 参数与实际布局对不上
现象:指定了 grid: [3, 4] 但动画顺序不按预想的走。
原因:grid 参数假设元素按文档流顺序排列。如果 DOM 顺序和视觉顺序不一致,或者用 CSS Grid/Flexbox 重排了视觉位置,顺序就会乱。
解决:
- 确保 DOM 顺序和视觉顺序一致
- 在 gsap.utils.toArray 时手动排序:
const items = gsap.utils.toArray(".grid-item").sort((a, b) => {
// 按自定义逻辑排序
return a.dataset.order - b.dataset.order;
});
gsap.from(items, { opacity: 0, stagger: 0.1 });陷阱三:大量元素时的性能问题
现象:500 个以上元素做 staggered 动画,页面明显卡成幻灯片。
原因:每个 staggered 元素都创建一个独立的 tween 实例,500+ 个 tween 同时运行。
解决:
// 策略一:限制 amount,减少同时运行的 tween
stagger: { amount: 2 }
// 而非 stagger: { each: 0.01 }(后者导致大量 tween 同时运行)
// 策略二:降低复杂度——只对 opacity 做动画
gsap.from(".item", { opacity: 0, stagger: { amount: 1.5 } });
// 避免 transform 叠加:scale + rotation + x/y 同时用
// 策略三:will-change 但谨慎使用
.item { will-change: opacity, transform; }
// 只在动画期间使用,动画结束后移除陷阱四:stagger.ease 和 tween.ease 别搞混
现象:设了 stagger.ease 但感觉效果不明显,或者搞不清两个 ease 的区别。
原因:stagger.ease 影响的是 stagger 的时间分布密度,不是每个元素动画的缓动。两种 ease 各管各的:
// 两个 ease 作用完全不同
gsap.to(".item", {
y: 0,
ease: "back.out", // 每个元素的动画缓动——它怎么"弹"
stagger: {
each: 0.1,
ease: "power2.in", // stagger 的时间分布缓动——它们"排队"的节奏
}
});stagger.ease: "power2.in" 意味着:前面元素间隔小(密集出场),越往后间隔越大(稀疏出场)。
陷阱五:stagger 与 fromTo 的初始状态
fromTo 配合 stagger 不存在“初始状态保持时间耗尽”的机制。每个目标有自己的启动延迟,默认立即渲染起始值。后面的元素异常时,检查是否有另一个 tween、CSS transition 或框架更新写入同一属性。
需要统一初始状态时,可以 gsap.set 后再 to;需要延迟到各自开始时才写起始值时,使用 immediateRender:false 并确认不会出现可见跳变。
gsap.set(items, { opacity: 0, y: 50 });
gsap.to(items, { opacity: 1, y: 0, duration: 0.6, stagger: 0.15 });时长算式:N 个目标、每个时长 d、间隔 e,从第一个到最后一个的启动跨度为 (N-1)*e,整组完成时间为 d+(N-1)*e。