从补间到叙事:GSAP 动画实战 · 公开阅读
附录B:常见动画模式与代码模板
查阅入场、退场、滚动、交互和循环动画的常用模式。
本章小节
每个模板提供5-15行核心代码,可直接复制使用。所有模板假设
gsap和ScrollTrigger已注册。
B.1 入场动画模板
fadeIn
元素从透明到可见,最简单的入场方式。
gsap.from(".element", {
opacity: 0,
duration: 0.6,
ease: "power2.out",
});slideIn
元素从四个方向滑入,带淡入效果。
// 从下方滑入
gsap.from(".element", {
y: 60,
opacity: 0,
duration: 0.8,
ease: "power3.out",
});
// 从左侧滑入
gsap.from(".element", { x: -100, opacity: 0, duration: 0.8 });
// 从右侧滑入
gsap.from(".element", { x: 100, opacity: 0, duration: 0.8 });
// 从上方滑入
gsap.from(".element", { y: -60, opacity: 0, duration: 0.8 });scaleIn
元素从0倍放大到1倍,back缓动提供弹性回弹效果。
gsap.from(".element", {
scale: 0,
opacity: 0,
duration: 0.8,
ease: "back.out(1.7)",
});staggerIn
多个元素逐个入场,支持从中间向两边扩散。
gsap.from(".item", {
opacity: 0,
y: 40,
duration: 0.6,
stagger: 0.08, // 每个间隔0.08s
ease: "power2.out",
});
// 从中间向两边展开
gsap.from(".item", {
opacity: 0,
scale: 0.5,
duration: 0.5,
stagger: { each: 0.05, from: "center" },
});组合入场
标题、描述、按钮依次出现:
const tl = gsap.timeline({ defaults: { ease: "power3.out" } });
tl.from(".title", { y: 40, opacity: 0, duration: 0.7 })
.from(".desc", { y: 20, opacity: 0, duration: 0.5 }, "-=0.3")
.from(".btn", { scale: 0, duration: 0.5, ease: "back.out(2)" }, "-=0.2");把这一节跑一遍:首页入场编排
入场模板先确定初态、编排与可用性。这个组合示例包含标题、说明和视觉按钮;复制到页面前准备对应 DOM,并在组件清理时 revert。
实验:首页入场编排
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:修改时长与重叠,解释总时长。入场结束后的交互不应继续依赖动画播放状态。
B.2 退场动画模板
fadeOut
元素从可见到透明。
gsap.to(".element", {
opacity: 0,
duration: 0.4,
ease: "power2.in",
});slideOut
向上/下/左/右滑出并淡出。
// 向上滑出并淡出
gsap.to(".element", {
y: -40,
opacity: 0,
duration: 0.4,
ease: "power2.in",
});scaleOut
缩小并淡出,适合弹窗关闭或元素移除。
gsap.to(".element", {
scale: 0.8,
opacity: 0,
duration: 0.3,
ease: "power2.in",
});翻牌退场
卡片绕Y轴旋转90度消失,适合删除或切换内容。
gsap.to(".card", {
rotationY: 90,
opacity: 0,
duration: 0.5,
ease: "power2.in",
onComplete: () => card.remove(), // 动画结束后移除DOM
});把这一节跑一遍:to / from / fromTo 对比
退场通常用 to 从当前状态过渡到隐藏状态,入场用 from 以当前布局作终点。这个对比实验帮助确认方向;真正删除 DOM 要在退场完成后执行,并处理被中断的情况。
实验:to / from / fromTo 对比
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:观察 from 与 to 的方向后,把 to 的 x 替换成 autoAlpha:0。思考如果用户中途撤销删除,应如何逆放或取消旧回调。
B.3 滚动驱动模板
revealOnScroll
页面滚动到元素进入视口时触发入场动画。
// 基础滚动揭示:从下方淡入
gsap.utils.toArray(".reveal").forEach(el => {
gsap.from(el, {
scrollTrigger: {
trigger: el,
start: "top 85%", // 元素顶部进入视口85%时触发
toggleActions: "play none none none",
markers: false, // 开发时改为true
},
y: 60,
opacity: 0,
duration: 0.8,
ease: "power3.out",
});
});批量滚动揭示(使用batch)
多个元素统一处理,省去逐个绑定。
ScrollTrigger.batch(".reveal", {
onEnter: batch => gsap.to(batch, {
opacity: 1, y: 0, stagger: 0.1, overwrite: true,
}),
start: "top 85%",
});
// 配合CSS初始状态:.reveal { opacity: 0; transform: translateY(40px); }parallax
滚动时不同层次以不同速度移动,产生深度空间感。
// 背景层慢速移动
gsap.to(".parallax-bg", {
scrollTrigger: {
trigger: ".parallax-section",
scrub: true, // 同步滚动
},
y: (i, target) => -target.dataset.speed * 200,
ease: "none",
});
// 前景层快速移动
gsap.to(".parallax-fg", {
scrollTrigger: { trigger: ".parallax-section", scrub: true },
y: 200, // 比背景移动更快
ease: "none",
});pinSection
固定一个区块,滚动时播放区块内的动画。
// 固定一个区块,在其内部播放动画
ScrollTrigger.create({
trigger: ".pin-section",
start: "top top",
end: "+=1000", // 1000px的滚动区域
pin: true,
pinSpacing: true,
markers: false,
animation: gsap.timeline()
.to(".box", { x: 500, duration: 1 })
.to(".box", { rotation: 360, duration: 1 }),
scrub: 1,
});水平滚动画廊
将垂直滚动转换为水平移动。
const gallery = document.querySelector(".horizontal-gallery");
const scrollDistance = gallery.scrollWidth - window.innerWidth;
gsap.to(gallery, {
scrollTrigger: {
trigger: ".gallery-container",
start: "top top",
end: `+=${scrollDistance}`,
pin: true,
scrub: 1,
},
x: -scrollDistance,
ease: "none",
});计数动画
滚动到指定区域时数字从0递增到目标值。
gsap.utils.toArray(".counter").forEach(counter => {
const target = +counter.dataset.target;
const obj = { val: 0 };
gsap.to(obj, {
scrollTrigger: { trigger: counter, start: "top 90%" },
val: target,
duration: 2,
ease: "power2.out",
onUpdate: () => { counter.textContent = Math.round(obj.val).toLocaleString(); },
});
});把这一节跑一遍:固定区块与占位
滚动模板先确认触发目标、scroller 和占位方式,再添加内部动画。只复制 pin:true 没有 start/end/布局约束,容易得到无法解释的留白。
实验:固定区块与占位
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:对照模板与本实验读数,明确每个 start/end 字符串的两端分别指元素和视口。
B.4 交互反馈模板
hoverScale
鼠标悬停时元素放大。
document.querySelectorAll(".hover-card").forEach(card => {
card.addEventListener("mouseenter", () => {
gsap.to(card, { scale: 1.05, duration: 0.3, ease: "power2.out" });
});
card.addEventListener("mouseleave", () => {
gsap.to(card, { scale: 1, duration: 0.3, ease: "power2.out" });
});
});
// 使用quickTo(更高性能)
const scaleSetter = gsap.quickTo(".hover-card", "scale", { duration: 0.3 });
card.addEventListener("mouseenter", () => scaleSetter(1.05));
card.addEventListener("mouseleave", () => scaleSetter(1));pressDepth
按下时元素缩小,松开后弹回。
const btn = document.querySelector(".press-btn");
btn.addEventListener("mousedown", () => {
gsap.to(btn, { scale: 0.95, duration: 0.1, ease: "power2.in" });
});
btn.addEventListener("mouseup", () => {
gsap.to(btn, { scale: 1, duration: 0.2, ease: "elastic.out(1,0.3)" });
});
btn.addEventListener("mouseleave", () => {
gsap.to(btn, { scale: 1, duration: 0.1 });
});toggleSwitch
滑动切换开关,背景色随状态变化。
let isOn = false;
const knob = document.querySelector(".toggle-knob");
document.querySelector(".toggle").addEventListener("click", () => {
isOn = !isOn;
gsap.to(knob, {
x: isOn ? 24 : 0,
duration: 0.3,
ease: "power2.inOut",
});
gsap.to(".toggle-bg", {
backgroundColor: isOn ? "#2ecc71" : "#bdc3c7",
duration: 0.3,
});
});ripple
点击处产生圆形波纹从中心向外扩散。
document.querySelectorAll(".ripple-btn").forEach(btn => {
btn.addEventListener("click", (e) => {
const ripple = document.createElement("span");
const rect = btn.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
ripple.style.cssText = `
position: absolute; border-radius: 50%;
width: ${size}px; height: ${size}px;
left: ${e.clientX - rect.left - size/2}px;
top: ${e.clientY - rect.top - size/2}px;
background: rgba(255,255,255,0.4);
`;
btn.appendChild(ripple);
gsap.fromTo(ripple,
{ scale: 0, opacity: 1 },
{ scale: 2, opacity: 0, duration: 0.6, ease: "power2.out",
onComplete: () => ripple.remove() }
);
});
});把这一节跑一遍:悬停、聚焦与按压反馈
反馈模板要覆盖键盘聚焦、鼠标与按压取消。连续输入应该收敛到当前状态,不能让旧 Tween 把新状态覆盖回去。
实验:悬停、聚焦与按压反馈
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:用 Tab 和空格完成一次交互,再快速移入移出,看缩放是否回到 1。
B.5 循环动画模板
rotate
无限旋转,适合加载指示器。
gsap.to(".spinner", {
rotation: 360,
duration: 2,
repeat: -1,
ease: "none",
});
// 或使用时间线实现变速旋转
gsap.to(".spinner", {
rotation: "+=360",
duration: 1.5,
repeat: -1,
ease: "power1.inOut",
});pulse
缩放循环,产生类似呼吸的效果。
gsap.to(".pulse", {
scale: 1.1,
opacity: 0.7,
duration: 0.8,
repeat: -1,
yoyo: true,
ease: "power1.inOut",
});float
元素上下浮动,适合装饰性元素。
gsap.to(".float", {
y: -15,
duration: 2,
repeat: -1,
yoyo: true,
ease: "sine.inOut",
});
// 浮动+旋转组合
gsap.to(".float-rotate", {
y: -10,
rotation: 5,
duration: 3,
repeat: -1,
yoyo: true,
ease: "sine.inOut",
});shake
左右快速抖动,适合错误提示。
function shake(el, intensity = 5) {
return gsap.fromTo(el,
{ x: -intensity },
{ x: intensity, duration: 0.1, repeat: 5, yoyo: true, ease: "power2.inOut" }
);
}
// 使用
shake(document.querySelector(".error-input"));marquee
内容水平无限滚动,适合公告条、品牌墙。
const items = gsap.utils.toArray(".marquee-item");
const totalWidth = items.reduce((sum, el) => sum + el.offsetWidth + 20, 0);
gsap.to(".marquee-track", {
x: -totalWidth / 2,
duration: 15,
repeat: -1,
ease: "none",
repeatRefresh: true,
});
// 需要复制一份内容实现无缝循环typewriter
文字逐字显示。需要 TextPlugin 支持。
function typewriter(el, text, duration = 2) {
const chars = text.length;
el.textContent = "";
const tl = gsap.timeline({ repeat: -1, repeatDelay: 1 });
tl.fromTo(el,
{ text: { value: "", speed: duration } },
{ text: { value: text, speed: duration }, duration: duration, ease: "none" }
);
return tl;
}
// 使用(需要TextPlugin)
typewriter(document.querySelector(".typewriter"), "Hello World", 2);把这一节跑一遍:循环、往返与总进度
公开阅读页的实验使用有限 repeat,避免几十个无限动画长期占用资源。实际加载状态需要循环时,应按可见性与生命周期启动、暂停和清理。
实验:循环、往返与总进度
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:通过有限 repeat 计算总时长,再思考 repeat:-1 时为什么没有有限的总进度终点。
B.6 导航与翻页动画
页面切换动画
旧页面淡出,新页面淡入。
function pageTransition(enterEl, leaveEl) {
const tl = gsap.timeline();
// 当前页面退出
tl.to(leaveEl, {
opacity: 0,
y: -30,
duration: 0.4,
ease: "power2.in",
});
// 新页面进入
tl.fromTo(enterEl,
{ opacity: 0, y: 30 },
{ opacity: 1, y: 0, duration: 0.5, ease: "power2.out" },
"-=0.2"
);
return tl;
}手风琴展开/折叠
点击标题,内容区平滑展开或收起。
document.querySelectorAll(".accordion-header").forEach(header => {
header.addEventListener("click", function() {
const body = this.nextElementSibling;
const isOpen = body.classList.contains("open");
if (isOpen) {
gsap.to(body, { height: 0, opacity: 0, duration: 0.3, ease: "power2.in" });
body.classList.remove("open");
} else {
body.classList.add("open");
gsap.fromTo(body,
{ height: 0, opacity: 0 },
{ height: "auto", opacity: 1, duration: 0.4, ease: "power2.out" }
);
}
});
});把这一节跑一遍:手风琴可逆过渡
同一个时间线正放展开、逆放收起,避免点击时为同一高度堆叠多个 Tween。隐藏期间 inert 限制焦点进入,按钮的 aria-expanded 与目标状态同步。
实验:手风琴可逆过渡
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:在展开中途再次点击收起,观察从当前位置逆放。高度为 auto 时若内容异步改变,需要重新测量或重建对应动画。
模态框弹出
背景遮罩淡入,弹窗从小到大弹出。
function openModal(modal) {
const overlay = modal.querySelector(".overlay");
const content = modal.querySelector(".modal-content");
modal.style.display = "block";
const tl = gsap.timeline();
tl.fromTo(overlay, { opacity: 0 }, { opacity: 1, duration: 0.3 })
.fromTo(content, { scale: 0.8, opacity: 0, y: 30 },
{ scale: 1, opacity: 1, y: 0, duration: 0.4, ease: "back.out(1.7)" },
"-=0.1");
}
function closeModal(modal) {
gsap.to(modal, {
opacity: 0,
duration: 0.2,
onComplete: () => { modal.style.display = "none"; gsap.set(modal, { opacity: 1 }); },
});
}