返回目录

从补间到叙事: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 }); },
  });
}