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