返回目录

从补间到叙事:GSAP 动画实战 · 公开阅读

第17章 MotionPathPlugin

沿 SVG 路径运动,处理自动旋转与路径测量。

本章小节

17.1 MotionPath是什么

MotionPathPlugin 让元素沿着你画的任意路径运动。把元素想象成一辆小火车,路径就是它行驶的轨道。这条轨道可以是 SVG <path> 元素、一组坐标点的数组,也可以是你手绘的曲线。

没有 MotionPath 时,要让元素走曲线只能手动计算:

// 手动计算贝塞尔曲线
gsap.to(".ball", { x: 300, y: 200, bezier: { curviness: 1.5 } });

有了 MotionPath,直接引用 SVG 路径即可:

// 沿SVG路径运动
gsap.to(".ball", {
  motionPath: {
    path: "#myPath",
    align: "#myPath",
    autoRotate: true,
  },
  duration: 3,
});

元素会沿路径精确运动。想让它走什么路线,画出来即可,不需要手动算坐标。

17.2 为什么需要MotionPath

场景传统做法MotionPath
飞机沿航线飞行手动算每个航点的x, y坐标一条路径搞定
元素沿 SVG 路径运动手工取点与坐标映射MotionPath 提供路径插值与 align
粒子曲线运动物理引擎 + 逐帧计算路径就是轨迹
菜单项弧线飞入多段关键帧 + 贝塞尔曲线一条弧线路径

MotionPath 解决的核心问题:把路径从数学计算变成可视化声明。 不再需要手动算坐标,画出来即可。

17.3 基本用法

SVG路径动画

先在 SVG 里画好"铁轨",然后让小球沿着轨道跑:

<svg viewBox="0 0 600 400">
  <!-- 定义运动路径 -->
  <path id="track" d="M50,200 C150,50 250,350 350,200 S500,50 550,200"
        fill="none" stroke="#ccc" stroke-width="2"/>

  <!-- 运动元素 -->
  <circle id="ball" r="15" fill="#e74c3c"/>
</svg>

<script>
// 注册插件
gsap.registerPlugin(MotionPathPlugin);

gsap.to("#ball", {
  duration: 4,
  repeat: -1,
  ease: "none",
  motionPath: {
    path: "#track",       // 路径ID
    align: "#track",      // 对齐路径
    autoRotate: true,     // 自动旋转朝向
  },
});
</script>

path vs align 的区别:

  • path:从哪里获取路径数据
  • align:元素对齐到哪个路径

path 提供轨迹几何数据;align 用来映射目标元素与路径的坐标系,并非另一条独立运动路线。SVG viewBox 缩放、父元素变换和 HTML/SVG 混用时,这个映射尤其重要。

自定义坐标路径——不需要SVG也能画铁轨

直接用坐标数组,GSAP 自动帮你把点连成平滑曲线:

gsap.to(".dot", {
  duration: 3,
  motionPath: {
    path: [
      { x: 0, y: 0 },
      { x: 100, y: -50 },
      { x: 200, y: -80 },
      { x: 300, y: -20 },
      { x: 400, y: 50 },
      { x: 500, y: 0 },
    ],
    curviness: 1.5,    // 曲线平滑度(0=直线连线, 2=最大弯曲)
    autoRotate: true,
  },
});

坐标数组自动转换为平滑贝塞尔曲线。给出关键站点,GSAP 会自动把站点之间连成平滑曲线。

17.4 核心参数详解

motionPath: {
  // ===== 路径来源(四选一) =====
  path: "#svgPath",           // 方式1: SVG路径选择器
  path: "#svgElement",        // 方式2: SVG任意形状元素
  path: [{x, y}, {x, y}],   // 方式3: 坐标数组
  path: {x, y, angle},       // 方式4: 直接属性(不常用)

  // ===== 路径对齐 =====
  align: "#svgPath",          // 元素对齐到指定路径
  alignOrigin: [0.5, 0.5],   // 对齐原点([0,0]=左上, [0.5,0.5]=中心, [1,1]=右下)

  // ===== 旋转 =====
  autoRotate: true,           // 自动旋转元素朝向路径方向
  autoRotate: 90,             // 自动旋转 + 角度偏移
  autoRotate: false,          // 不旋转(元素直直地平移,不转弯)

  // ===== 路径分段 =====
  start: 0,                   // 从路径的哪个位置出发(0-1,路径百分比)
  end: 1,                     // 走到路径的哪个位置(0-1)
  fromCurrent: true,          // 从当前位置开始(已废弃,用start替代)

  // ===== 路径处理 =====
  curviness: 1.5,             // 坐标数组的曲线平滑度(越大拐弯越圆润)
  resolution: 200,            // 路径采样精度(默认200,越大越精确但初始化稍慢)
  type: "cubic",              // 插值类型:"cubic"(平滑曲线) | "linear"(直线连接)
  useRadians: false,          // 旋转值是否使用弧度

  // ===== 相对运动 =====
  relative: false,            // 坐标数组是否相对于起始位置
}

alignOrigin — 元素对齐点

// 元素中心对齐路径(默认)
alignOrigin: [0.5, 0.5]

// 元素左上角对齐路径
alignOrigin: [0, 0]

// 元素右下角对齐路径
alignOrigin: [1, 1]

// 元素底部中心对齐路径
alignOrigin: [0.5, 1]

[0, 0] 表示元素左上角贴着轨道走;[0.5, 0.5] 表示元素中心对齐轨道;[1, 1] 表示元素右下角贴轨道。

autoRotate — 自动旋转朝向

// 朝向路径前进方向
autoRotate: true,

// 加偏移量:如果元素图标默认向上(如飞机图标),需要90度补偿
autoRotate: 90,

// 反转方向
autoRotate: -180,

旋转角度基于路径的切线方向。如果元素默认朝向"上方"(如火箭图标),设置 autoRotate: 90 进行补偿。

start / end — 只走一段

// 只走路径的前30%
gsap.to(".ball", {
  motionPath: { path: "#track", start: 0, end: 0.3 },
  duration: 1,
});

// 从路径中间走到末尾
gsap.to(".ball", {
  motionPath: { path: "#track", start: 0.5, end: 1 },
  duration: 2,
});

分段路径可以实现"先走一半停下,再继续走"的效果,如进度条动画可分成两段。

把这一节跑一遍:MotionPath 路径运动

SVG 路径坐标由 viewBox 定义,align 将其映射到目标坐标系。autoRotate 根据切线方向旋转,初始箭头朝右可直接使用 true;默认朝向不同则需补偿角度。

实验:MotionPath 路径运动

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:切换 autoRotate,定位到曲线拐点。看 x/y 与 rotation 读数,区分“沿路径改变位置”与“按切线改变朝向”。

17.5 获取路径数据和实用方法

MotionPathHelper — 可视化路径编辑器

在开发环境加载 MotionPathHelper,它会在浏览器里打开一个 GUI 工具,让你像画图一样拖拽控制点来调整路径:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/MotionPathHelper.min.js"></script>
<script>
// 本例假设同版本的 GSAP 核心和 MotionPathPlugin 已先加载
gsap.registerPlugin(MotionPathPlugin, MotionPathHelper);
// 在浏览器中可视化编辑路径
MotionPathHelper.create(".ball", {
  path: "#myPath",    // 路径ID
  autoRotate: true,
});
</script>

调整完毕直接复制生成的代码。

MotionPathPlugin.getRawPath() — 获取路径原始坐标

// RawPath 是数值数组的数组;每段记录起点和三次贝塞尔控制点
const rawPath = MotionPathPlugin.getRawPath("#track");
console.log(rawPath);  // [[x0, y0, cx1, cy1, cx2, cy2, x1, y1, ...], ...]

// 用于计算自己的逻辑
const totalLength = MotionPathPlugin.getLength(rawPath);

MotionPathPlugin.getPositionOnPath() — 获取路径某位置的坐标

// 获取路径上某百分比位置的坐标
const pathData = MotionPathPlugin.getRawPath("#track");
const pos = MotionPathPlugin.getPositionOnPath(pathData, 0.5);
// pos 包含路径自身坐标系中的 x、y

// marker 位于同一个 SVG 坐标系时,可直接使用该坐标
gsap.set(".marker", { x: pos.x, y: pos.y });

HTML 元素与 SVG 的坐标系可能不同,应使用 motionPath 的 align 做映射。getRelativePosition() 用于计算两个元素之间的相对位置,不接收路径进度参数。

17.6 实战示例

示例1:飞机沿航线飞行

<svg viewBox="0 0 800 500">
  <path id="flightPath"
    d="M50,400 C150,300 200,100 400,150 S650,350 750,50"
    fill="none" stroke="#bdc3c7" stroke-width="2" stroke-dasharray="5,5"/>

  <!-- 航点标记 -->
  <circle cx="50" cy="400" r="6" fill="#27ae60"/>
  <circle cx="400" cy="150" r="6" fill="#27ae60"/>
  <circle cx="750" cy="50" r="6" fill="#e74c3c"/>

  <!-- 飞机图标 -->
  <g id="airplane">
    <text font-size="36" text-anchor="middle">✈</text>
  </g>
</svg>

<script>
gsap.registerPlugin(MotionPathPlugin);

const tl = gsap.timeline({ repeat: -1, repeatDelay: 0.5 });

tl.to("#airplane", {
  duration: 6,
  ease: "none",
  motionPath: {
    path: "#flightPath",
    align: "#flightPath",
    autoRotate: 90,      // 飞机图标默认向上,需要90度偏移
    alignOrigin: [0.5, 0.5],
  },
});
</script>

示例2:粒子沿随机曲线运动

<canvas id="particleCanvas" width="800" height="400"></canvas>

<script>
const canvas = document.getElementById("particleCanvas");
const ctx = canvas.getContext("2d");
const particles = [];

// 创建20个粒子,每个沿不同的随机路径运动
for (let i = 0; i < 20; i++) {
  const particle = document.createElement("div");
  // 使用虚拟DOM元素作为GSAP目标(也可以直接用canvas绘制)

  const randomPath = [
    { x: Math.random() * 800, y: Math.random() * 400 },
    { x: Math.random() * 800, y: Math.random() * 400 },
    { x: Math.random() * 800, y: Math.random() * 400 },
    { x: Math.random() * 800, y: Math.random() * 400 },
    { x: Math.random() * 800, y: Math.random() * 400 },
  ];

  gsap.to(particle, {
    duration: 3 + Math.random() * 5,
    repeat: -1,
    yoyo: true,
    ease: "none",
    motionPath: {
      path: randomPath,
      curviness: 1.2,
      autoRotate: false,
    },
    onUpdate: function() {
      const x = gsap.getProperty(particle, "x");
      const y = gsap.getProperty(particle, "y");
      particles[i] = { x, y };
    },
  });
}

// 绘制循环
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  particles.forEach((p, i) => {
    if (!p) return;
    ctx.beginPath();
    ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
    ctx.fillStyle = `hsl(${(i * 18) % 360}, 80%, 60%)`;
    ctx.fill();
  });
  requestAnimationFrame(draw);
}
draw();
</script>

示例3:SVG描边动画 + 路径运动

线条一笔笔画出,小球在笔尖位置运动:

<svg viewBox="0 0 600 300">
  <path id="strokePath"
    d="M30,150 Q150,30 300,150 T570,150"
    fill="none" stroke="#3498db" stroke-width="3"
    stroke-dasharray="800" stroke-dashoffset="800"/>

  <circle id="trailHead" r="8" fill="#e74c3c"/>
</svg>

<script>
gsap.registerPlugin(MotionPathPlugin);

const tl = gsap.timeline();

// 描边动画:线条从0绘制到100%
tl.to("#strokePath", {
  duration: 3,
  strokeDashoffset: 0,
  ease: "power2.inOut",
}, 0);

// 路径动画:小球沿同一条路径运动(带动画尾迹)
tl.to("#trailHead", {
  duration: 3,
  ease: "power2.inOut",
  motionPath: {
    path: "#strokePath",
    align: "#strokePath",
    autoRotate: true,
  },
}, 0);  // 同一个起始时间
</script>

示例4:弧线菜单飞入

点击加号按钮,菜单项沿弧线轨迹飞出:

<div class="arc-menu">
  <button class="menu-trigger">+</button>
  <div class="menu-item" style="--i:0">📷</div>
  <div class="menu-item" style="--i:1">📁</div>
  <div class="menu-item" style="--i:2">⚙</div>
  <div class="menu-item" style="--i:3">🔔</div>
</div>

<script>
gsap.registerPlugin(MotionPathPlugin);

const menuOpen = false;

document.querySelector(".menu-trigger").addEventListener("click", function() {
  const items = document.querySelectorAll(".menu-item");

  items.forEach((item, i) => {
    // 每个菜单项沿弧线路径飞出
    const angle = (-60 + i * 40) * Math.PI / 180;
    const radius = 120;
    const targetX = Math.cos(angle) * radius;
    const targetY = Math.sin(angle) * radius;

    gsap.to(item, {
      duration: 0.5,
      ease: "back.out(1.7)",
      motionPath: {
        path: [
          { x: 0, y: 0 },
          { x: targetX * 0.5, y: targetY * 0.3 },
          { x: targetX, y: targetY },
        ],
        curviness: 1.5,
      },
      opacity: 1,
      stagger: 0.05,
    });
  });
});
</script>

17.7 常见陷阱

陷阱1:SVG viewBox 与页面坐标不对齐

<!-- 问题:SVG有viewBox,元素在HTML中 -->
<svg viewBox="0 0 800 600">
  <path id="path" d="M100,100 Q400,50 700,100"/>
</svg>
<div class="ball" style="position:absolute; top:0; left:0"></div>

<script>
// 元素坐标基于viewport,路径坐标基于SVG viewBox
// 当viewBox != 实际宽高时,需要转换
gsap.to(".ball", {
  motionPath: {
    path: "#path",
    // align: "#path" 时GSAP会自动匹配
    align: "#path",   // 加上align,GSAP处理坐标转换
  }
});
</script>

解决方案:始终设置 align: "#path",让 GSAP 自动处理坐标映射。

陷阱2:autoRotate 角度异常

// 飞机图标是向上的,但路径是水平的
// 错误:autoRotate: true 会让飞机"躺着"飞
autoRotate: true,

// 正确:加90度偏移,让飞机"头朝前"
autoRotate: 90,

// 或者反转方向
autoRotate: -90,

规律:autoRotate 的值 = 元素默认朝向与路径前进方向的夹角。如果你的图标默认朝上,加 90 度就对了。

陷阱3:路径闭合问题

<!-- 闭合路径写法 -->
<path id="closedPath" d="M100,100 Q300,50 500,100 Q300,150 100,100 Z"/>

闭合路径的终点回到起点,start: 0, end: 1 时只走一圈。需要 repeat: -1 才能不停地绕圈。

陷阱4:坐标数组的 curviness 与 type

// curviness: 1.5 是默认值,范围是0-2。越大拐弯越圆润
// type: "cubic" 使用平滑曲线连接各点
// type: "linear" 使用直线连接各点(适合折线路径)

motionPath: {
  path: [{x:0,y:0}, {x:100,y:0}, {x:100,y:100}],
  type: "linear",     // 直角折线
  // type: "cubic",   // 平滑曲线
  curviness: 1.5,
}

陷阱5:性能与 resolution

GSAP 3.13 通常按每段三次贝塞尔曲线使用 12 个采样细分;它不是整条路径固定采样 200 个点。只有需要提高距离测量精度时才调整 resolution:

// 普通路径:默认精度通常足够
motionPath: { path: "#shortPath" },

// 曲线较复杂且距离测量不够精确时,再评估更高采样细分
motionPath: { path: "#complexPath", resolution: 24 },

值越大,测量开销也越大。应根据实际效果调整,不应无条件设为数百。

陷阱6:多个元素沿同一条轨道错开

// 用stagger让多个元素沿同一条路径错开出发
gsap.to(".follower", {
  duration: 5,
  repeat: -1,
  ease: "none",
  motionPath: { path: "#track", align: "#track" },
  stagger: 0.2,  // 每个元素间隔0.2秒出发
});