MorphSVG
快速入门
CDN 链接
gsap.registerPlugin(MorphSVGPlugin)
最简用法
gsap.to("#circle", { duration: 1, morphSVG: "#hippo" });
描述
MorphSVGPlugin 通过动画处理 SVG path 的内部数据来实现形态变换。d
你可以使用一行代码将圆形变成河马:
gsap.to("#circle", { duration: 1, morphSVG: "#hippo" });
加载中...
它是如何工作的?
在这个示例中,MorphSVGPlugin 会查找 ID 为 "circle" 的路径和 ID 为 "hippo" 的路径,并自动计算如何向圆中添加足够的点以实现极其平滑的形态变化。它会快速解析所有丑陋的路径数据,将所有内容转换为三次贝塞尔曲线,并在必要时动态细分它们,增加点数以使起点和终点的数量匹配(但视觉上看起来相同)。这一切都在后台无缝完成。
特性
- 形态变化
<path>
数据即使起点和终点形状的点数量(和类型)完全不同,它也能正常工作!大多数其他 SVG 形状形态变换工具要求点数必须一致。 - 将一个
<polyline>
或者<polygon>
变换到一组不同的点。 - 绘制最终的形状到
<canvas>
(通过设置MorphSVGPlugin.defaultRender
一个渲染函数 - 参见defaultRender 文档获取更多信息)。
阅读更多...
- 这里有一个实用函数,
MorphSVGPlugin.convertToPath()
,可以将诸如<circle>
,<rect>
,<ellipse>
,<polygon>
,<polyline>
,以及<line>
等基础图形直接转换为外观相同的<path>
路径,并立即替换到 DOM 中。 - 可选地定义一个
shapeIndex
来控制点的映射方式。这会影响动画过程中中间状态的显示效果。 - 可以使用线性插值(默认值),也可以使用
rotational
类型来获得更自然的形态变化效果。 - 除了传入文本形式的原始路径数据,你还可以直接传入选择器文本或元素,插件会从那里提取所需数据,使工作流程更加方便。
当只指定一个形状时,MorphSVGPlugin 可以接受广泛的值。
//selector string
gsap.to("#circle", {duration: 1, morphSVG: "#hippo"});
//an SVG element
var endShape = document.getElementById("hippo");
gsap.to("#circle", {duration: 1, morphSVG: endShape);
//points for or elements:
gsap.to("#polygon", {duration: 2, morphSVG: "240,220 240,70 70,70 70,220"});
//strings for elements
gsap.to("#path", {duration: 2, morphSVG: "M10 315 L 110 215 A 30 50 0 0 1 162.55 162.45 L 172.55 152.45 A 30 50 -45 0 1 215.1 109.9 L 315 10"});
如果传入的形状是一个<rect>
, <circle>
, <ellipse>
(或类似对象),MorphSVGPlugin 会在内部从这些形状创建路径数据。
配置对象
MorphSVG 可以作为形状的简写(如下所述),也可以是包含以下任意属性的配置对象:
- 字符串 | 选择器 | 元素- 要变形的目标形状。
- "rotational" | "linear"
默认情况下,形状中的所有锚点和控制点都是按线性方式插值(
type: "linear"
),这通常已经足够好,但你也可以设置type: "rotational"
以让 MorphSVG 使用旋转和长度数据进行插值,在某些情况下可以获得更自然的形态变化。它还能完全消除在补间动画中间可能出现的光滑锚点卡顿现象。要使用这种替代方式的形态变化,只需在对象中设置 type: "rotational":gsap.to("#shape1", {
duration: 2,
morphSVG:{
shape: "#shape2",
type: "rotational"
}
})这个概念最好的理解方式是可视化观看,因此这里有一些视频和演示……
查看更多详细信息
线性和旋转形态变化的交互式比较
- 字符串
设置旋转的原点。默认值为
50% 50%
。格式可以是一个包含两个百分比值的字符串,或者如果起始形状和结束形状有不同的值,则可以是包含四个值的字符串。设置自定义原点:
gsap.to("#shape1", {
duration: 2,
morphSVG: {
shape: "#shape2",
type: "rotational",
origin: "20% 60%", //or "20% 60%,35% 90%" if there are different values for the start and end shapes.
},
});有时候围绕某一点旋转的效果看起来不理想,在这种情况下,最好进行实验并设置自己的自定义原点以进一步改善效果。我们为此创建了一个 findMorphOrigin() 实用函数,帮助实现此目的……
查看更多详细信息
findMorphOrigin 允许你简单地传入起始和结束形状,然后它会叠加一个你可以拖动的原点,以便精确查看它对形态变化的影响!在下面的演示中,进入 JS 面板并取消注释 findMorphIndex() 的那行代码,你就能确切了解它是如何工作的。拖动原点并观察它如何影响形态变化。
- 数值
shapeIndex 属性允许你调整起始形状中的点是如何映射的。以下代码会将正方形中的第三个点映射到星形的第一个点。
gsap.to("#square", {
duration: 1,
morphSVG: { shape: "#star", shapeIndex: 3 },
});查看更多详细信息
为了避免动画期间点漂移严重,MorphSVGPlugin 需要在起始路径中找到一个靠近目标路径第一个点的点。一旦找到该点,它就会将起始路径中的下一个点映射到目标路径中的第二个点(以此类推)。
由于矢量图的复杂性,有时你可能希望更改起始路径中哪个点被映射到目标路径的第一个点。这时就需要使用 shapeIndex。
注意事项- shapeIndex 仅适用于闭合路径。
- 如果你提供的是负数 shapeIndex,则起始路径将被完全反转(这可能非常有用)
- 大小 | 位置 | 复杂度
如果你的路径内的子段在起始和结束形状之间的匹配情况不如预期,你可以使用
map
来告诉 MorphSVGPlugin 使用哪种算法优先匹配:"size"
(默认值) - 基于整体大小匹配段落。如果有多个段落大小接近,它会使用位置数据进行匹配。这种模式通常能提供最直观的形态变化。"position"
- 主要基于位置进行匹配。"complexity"
- 仅基于锚点数量进行匹配。这是最快的算法,可以通过在 SVG 编辑工具中手动添加锚点来“欺骗”系统进行匹配,从而使你想匹配的部分包含相同数量的锚点(虽然这完全是可选的)。
gsap.to("#id", {
duration: 1,
morphSVG: { shape: "#otherID", map: "complexity" },
});查看更多详细信息
注意事项
map
默认模式通常已经足够优秀,因此是否使用是完全可选的。- 如果地图模式中没有一个能将路径段匹配成你想要的样子,那最好只是把你的路径拆分成为多个路径,然后分别变形每个路径。这样你可以获得完全的控制权。
- 数值
默认情况下,为了最大化性能和减少字符串长度,MorphSVGPlugin 会将数值四舍五入到小数点后两位,但如果你需要不同的设置,可以指定小数位数。
precision
设置为你希望的小数位数。例如,precision: 5
会四舍五入到小数点后五位:gsap.to("#id", { morphSVG: { shape: "#other-id", precision: 5 } });
- 函数
定义一个渲染函数,该函数会在路径更新时被调用。更多信息请参见渲染到画布
- 数组
让 MorphSVGPlugin 执行其所有初始计算,并返回一个包含转换后的字符串数组,将其记录到控制台,你可以复制粘贴回补间动画中。这样,当补间开始时就可以直接获取所有值,而不是进行复杂的计算。
更多信息请参见预编译
属性
描述
提示
MorphSVG 同样在其目标的变形补间上存储了原始路径数据,因此你可以轻松地补间回到原始形状。(像data-original="M490.1,280.649c0,44.459-36.041,80..."
findShapeIndex() 工具
尝试使用shapeIndex
可能有点猜谜游戏的感觉。为让事情更简单,我们创建了一个独立的工具函数叫做findShapeIndex()
。该函数提供了一个交互式的用户界面来帮助你可视化起点的位置、修改它并预览动画。
你可以加载findShapeIndex()
来自这个下载链接.
加载完成后,只需告诉它要使用的形状。
findShapeIndex("#square", "#star");
或传递原始数据:
findShapeIndex(
"#square",
"M10 315 L 110 215 A 30 50 0 0 1 162.55 162.45 L 172.55 152.45 A 30 50 -45 0 1 215.1 109.9 L 315 10"
);
入门的最佳方法是将你的 SVG 拖放到代码片段中,并更改 ID 以匹配你的 SVG。
加载中...
将 SVG 形状转换为路径
技术上讲,只能对<path>
路径元素或<polyline>
/<polygon>
多边形元素进行变形,<circle>
, <rect>
, <ellipse>
,或者<line>
但是如果想变形的是矩形怎么办?没问题——只需调用这个工具函数,让插件为您完成转换:
MorphSVGPlugin.convertToPath("#elementID");
你可以传入一个元素或者选择器文本,因此也可以通过一行代码转换所有的这些元素:
MorphSVGPlugin.convertToPath("circle, rect, ellipse, line, polygon, polyline");
这实际上是在 DOM 中直接替换每一个为对应的路径元素,外观应该绝对相同。它会保留属性,如“id”属性。因此在转换之后,你应该能够很容易地定位到这些元素,就像之前一样。
//An svg <rect> Like this:
<rect id="endShape" width="100" height="100" fill="red"/>
//becomes
<path id="endShape" fill="red" d="M100,0 v100 h-100 v-100 h100z"></path>
向多个形状变形
因为 MorphSVGPlugin 与 GSAP 紧密集成在一起,所以顺序播放多个变形效果非常容易。看看如何轻松地让圆变形为河马、星星、大象再回到圆形。
tl.to(circle, { duration: 1, morphSVG: "#hippo" }, "+=1")
.to(circle, { duration: 1, morphSVG: "#star" }, "+=1")
.to(circle, { duration: 1, morphSVG: "#elephant" }, "+=1")
.to(circle, { duration: 1, morphSVG: circle }, "+=1");
加载中...
性能
提前定义一个 shapeIndex
shapeIndex
,MorphSVGPlugin 默认的shapeIndex: "auto"
会执行一系列计算以重新组织点,使它们自然地匹配。但如果你定义了一个数值型的shapeIndex
(比如shapeIndex: 5
),它就会跳过这些计算。路径中的每个线段都需要一个shapeIndex
,因此多个值以数组形式传递,例如shapeIndex:[5, 1, -8, 2]
。但是你会如何知道要传什么数字呢?对于单线段路径,有findShapeIndex()
工具有帮助,那么多线段路径怎么办?提供一个 GUI 来实现这一点是一件相当复杂的事情。
阅读更多...
通常默认的"auto"
模式工作得很好,但这里的目标是避免计算,因此有一个"log"
值的行为类似于"auto"
,但它还会同时输出console.log()
垂直位置shapeIndex
值。这样,你可以在浏览器中运行一次补间动画,查看控制台中显示的由"auto"
模式生成的数字。然后只需将该值复制粘贴到你的补间动画中取代之前的"log"
即可。
举例来说:
//logs a value like "shapeIndex:[3]"
gsap.to("#id", {
duration: 1,
morphSVG: { shape: "#otherID", shapeIndex: "log" },
});
//now you can grab the value from the console and drop it in...
gsap.to("#id", {
duration: 1,
morphSVG: { shape: "#otherID", shapeIndex: [3] },
});
预编译
最大的性能提升来自于预编译,这包括让 MorphSVGPlugin 执行上述所有初始化计算,然后输出一个包含转换后的字符串的数组,并将其记录到控制台,你可以再将其复制粘贴回补间动画中。这样,当补间动画启动时,它可以直接获取所有值而非进行昂贵的计算。
显示示例...
//logs a value like precompile:["M0,0 C100,200 120,500 300,145 34,245 560,46","M0,0 C200,300 100,400 230,400 100,456 400,300"]
gsap.to("#id", {
duration: 1,
morphSVG: { shape: "#otherID", precompile: "log" },
});
//now you can grab the value from the console and drop it in...
gsap.to("#id", {
duration: 1,
morphSVG: {
shape: "#otherID",
precompile: [
"M0,0 C100,200 120,500 300,145 34,245 560,46",
"M0,0 C200,300 100,400 230,400 100,456 400,300",
],
},
});
举个例子,这里是 Dave Rupert 的一个非常酷的 CodePen在未启用预编译之前的版本。注意第一次点击切换按钮时,可能会有些卡顿,因为整个大脑是一个有很多线段的路径,必须将其与所有字母匹配并计算出每个字母的shapeIndex
(这是很消耗性能的)。相比之下,这里是启用了预编译功能的变种CodePen。您可能会注意到它的启动更加流畅。
注意事项
-
precompile
是仅在<path>
path<polyline>
/<polygon>
元素上可用(不在其他元素上)。MorphSVGPlugin.convertToPath("polygon, polyline");
-
预编译仅提高首次(也是最耗资源的一次)渲染的性能。如果整个变形过程在补间过程中都很卡顿,很可能与 GSAP 没有关系;可能是你的 SVG 对浏览器来说过于复杂,无法快速渲染。换句话说,瓶颈可能在于浏览器的图形渲染程序。不幸的是,这方面 GSAP 无能为力,你需要简化 SVG 图像内容,和/或减小其显示尺寸。
-
预编译的值包含了
shapeIndex
的调整。换句话说,shapeIndex
会被预先处理进去。 -
在大多数情况下,你可能不需要进行预编译;它是专为从极其复杂的变形中榨取每一滴性能而设计的一种高级技巧。
-
如果你修改了原始的起始或结束形状/矢量图,请确保再次预编译,以便数值反映你的更改。
渲染到画布
SVG 非常棒,但有时开发者会有基于 Canvas 的项目(通常出于渲染性能的原因)。MorphSVG 插件允许你定义一个render
每次路径更新时都会调用的函数,并且它会接收到两个参数:
阅读更多...
-
rawPath
[数组]:RawPath 本质上是一个数组,其中包含每个连续段的子数组,每个子数组中交替存放着 x、y、x、y 的三次贝塞尔曲线数据。这类似于 SVG<path>
其中每个M
命令对应一个段(数组)。该段(数组)以交替的 x/y 格式包含所有三次贝塞尔坐标(与 SVG 路径数据一样),使用原始的数字形式,好处是你不需要解析长字符串并进行转换。例如,这个 SVG
<path>
有两个独立的段,因为存在两个M
命令:<path d="M0,0 C10,20,15,30,5,18 M0,100 C50,120,80,110,100,100"></path>
所以最终的RawPath将会是:[
[0, 0, 10, 20, 15, 30, 5, 18],
[0, 100, 50, 120, 80, 110, 100, 100],
];为了简化,上面的例子在每个段中只包含了一个三次贝塞尔曲线,但事实上每个段中可以包含无限数量的贝塞尔曲线。无论原始
<path>
数据字符串中包含什么路径命令(三次、二次、弧线、直线等),最终生成的 RawPath始终都是三次贝塞尔曲线。 -
target
[对象]:补间动画的目标(通常是<path>
)
这意味着你甚至可以将变形效果渲染到高性能引擎如PixiJS或者任何可以绘制三次贝塞尔曲线的工具!
示例:MorphSVG Canvas 渲染
加载中...
这里是一个补间动画和一个渲染函数的示例,该函数会将变形图形绘制到 Canvas 上:
let canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
vw = (canvas.width = window.innerWidth),
vh = (canvas.height = window.innerHeight);
ctx.fillStyle = "#ccc";
gsap.to("#hippo", {
duration: 2,
morphSVG: {
shape: "#circle",
render: draw,
},
});
function draw(rawPath, target) {
let l, segment, j, i;
ctx.clearRect(0, 0, vw, vh);
ctx.beginPath();
for (j = 0; j < rawPath.length; j++) {
segment = rawPath[j];
l = segment.length;
ctx.moveTo(segment[0], segment[1]);
for (i = 2; i < l; i += 6) {
ctx.bezierCurveTo(
segment[i],
segment[i + 1],
segment[i + 2],
segment[i + 3],
segment[i + 4],
segment[i + 5]
);
}
if (segment.closed) {
ctx.closePath();
}
}
ctx.fill("evenodd");
}
设置所有补间动画的默认渲染方式:
MorphSVGPlugin.defaultRender = yourFunction;
updateTarget: false
默认情况下,MorphSVG 会更新补间动画的原始目标(通常是 SVG<path>
元素),但如果你只在绘制到 Canvas,你可以这样告诉 MorphSVG 跳过更新目标:
gsap.to("#hippo", {
duration: 2,
morphSVG: {
shape: "#circle",
render: draw,
updateTarget: false,
},
});
设置默认的updateTarget
值用于所有补间动画(这样你就不需要为每个补间动画添加):
MorphSVGPlugin.defaultUpdateTarget = false;
视频操作指南
用于复杂变形的高级功能
性能建议
属性
MorphSVGPlugin.defaultRender: 函数 | 设置每当 MorphSVG 补间动画更新时应调用的默认函数。当你渲染到 |
MorphSVGPlugin.defaultType:字符串 | 设置所有 MorphSVG 动画的默认 |
MorphSVGPlugin.defaultUpdateTarget:布尔值 | 设置所有 MorphSVG 动画的默认 |
方法
MorphSVGPlugin.convertToPath( shape:[Element | String], swap:Boolean ) : Array | 将SVG形状比如 |
MorphSVGPlugin.rawPathToString( rawPath:Array ) : String | 将 RawPath(数组)转换为路径数据字符串,例如 |
MorphSVGPlugin.stringToRawPath( data:String ) : RawPath | 接收一串路径数据字符串(例如 |
示例演示
查看完整系列的SVG 动画演示在 CodePen 上。
MorphSVG 示例