SMIL,SVG 的原生动画规范,曾经风光无限,凭借着其强大的功能和高效的渲染能力,在 SVG 动画领域呼风唤雨。然而,时过境迁,SMIL 的支持在 WebKit 中日渐式微,而微软的 IE 和 Edge 浏览器更是从未支持过 SMIL,也几乎不可能在未来支持。
别担心!我们今天就来探讨一些 SMIL 特有的功能,并深入研究如何用其他方法来实现相同的效果,以确保你的动画能拥有更广泛的浏览器兼容性。
🏃🏻♀️ 沿着路径运动
SMIL 最吸引人的地方之一就是它能够让 SVG 对象沿着路径运动,从而实现更加逼真的动画效果。毕竟,现实生活中很少有物体是沿着直线运动的,沿着路径运动可以让我们模拟现实生活中的各种运动轨迹。
在过去,你需要将 SVG 路径数据传递给 animateMotion
元素,并使用 path
属性来定义路径数据。然后,你可以通过 xlink:href
属性来指定要进行动画的元素。
<animateMotion
xlink:href="#lil-guy"
dur="3s"
repeatCount="indefinite"
fill="freeze"
path="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" />
替代方案:CSS
幸运的是,现在 CSS 也支持沿着路径运动的功能了!虽然目前支持的浏览器还不多(仅限于 Chrome、Opera 和 Android),但 Sara Soueidan 已经提议在 Edge 中加入该功能,并且得到了强烈的支持,在本文发布时已经获得了超过 420 票。请加入我们,一起呼吁该功能早日实现!Firefox 的投票页面 在这里。
至于 Safari,据我所知,它的支持情况可能需要单独处理。我已经注册了一个 bug 报告,并请求在 CSS 中添加沿着路径运动的功能。
为了在 CSS 中使用沿着路径运动,你需要将路径数据传递给 offset-path
属性,就像这样:
.move-me {
offset-path: path('M3.9,74.8c0,0,0-106.4,75.5-42.6S271.8,184,252.9,106.9s-47.4-130.9-58.2-92s59.8,111.2-32.9,126.1 S5.9,138.6,3.9,74.8z');
}
我通常会在 Illustrator 中创建 SVG,然后使用 SVGOMG 进行优化,以获取路径数据。
在这个例子中,我希望动画对象沿着路径从起点运动到终点,并且路径是一个闭合路径,因为路径数据末尾有一个 z
。这意味着路径是一个循环,所以这个小生物最终会回到起点。我在关键帧中设置了这些参数,只指定了 100%
的值,因为默认值为 0
:
@keyframes motionpathguy {
100% {
motion-offset: 100%;
}
}
然后,将动画应用于元素:
.move-me {
animation: motionpathguy 10s linear infinite both;
}
替代方案:GreenSock 的沿着路径运动
如果你想要最广泛的浏览器支持和最灵活的实现方式,那么你应该使用 GreenSock。GSAP 的 Bezier 插件(默认情况下包含在 TweenMax 中)支持 IE7 及更高版本(非 SVG 元素),以及 IE9 及更高版本(SVG 元素),这是目前最广泛的 SVG 动画支持。它在移动设备上也运行得很好。
我之前在 David Walsh 博客上 写过关于这个插件的文章,但这里只是简要回顾一下,并介绍一些自那以后发布的新功能:
最初,你需要传递一个值数组:
bezier: {
type: "soft",
values:[{x:10, y:30}, {x:-30, y:20}, {x:-40, y:10}, {x:30, y:20}, {x:10, y:30}],
autoRotate: true
}
但正如你所看到的,你还可以选择自动旋转(或不旋转),就像 SMIL 的 rotate
属性一样。如果你想使用 SMIL 中的 auto-reverse
或 auto:n
参数来指定旋转的初始位置或旋转角度,GSAP 允许你使用 rotation:90
来更改旋转角度,或者如果你需要更精细的控制,可以使用更具体的设置:
autorotate: [
first position property, like "x",
second position property, like "y",
rotation property, typically "rotation" but can be “rotationY”,
integer for radians/degrees the rotation starts from like 10,
boolean for radians or degrees- radians is true
]
在 SMIL 中,你可以对路径或组进行变换,以改变动画对象在运动过程中的方向。在 GSAP 中,你可以通过 autoRotate: false
轻松实现这一点,并使用 set
初始化旋转。你也可以像在 SMIL 中一样,在 SVG 属性本身对元素进行变换,但这有点不太优雅,而且在工作时更难跟踪。
TweenMax.set("#foo" {
rotation: 90 // or whatever number
});
你还可以将 type
属性设置为 thru
、soft
、quadratic
或 cubic
。有关这些属性的更多文档,请查看 GreenSock API 文档。thru
属性的一个很好的用途是能够影响元素的弯曲程度。如果你将这些点视为弹跳的坐标,那么弯曲程度将控制在这些点之间采取的路径的直接程度。0
表示直线路径,1
表示稍微松散的路径,2
表示一个漂亮的曲线,而 3
及更高的值将开始在自身上缠绕。
graph LR
subgraph 0
A[0]
end
subgraph 1
B[1]
end
subgraph 2
C[2]
end
subgraph 3
D[3]
end
A --> B
B --> C
C --> D
最近,GreenSock 也提供了将路径数据传递给 CSS 和 SMIL 模块的能力,就像使用原生 SMIL 一样。这作为他们 MorphSVG 插件的扩展,因此你需要添加该插件,并像这样使用它:
TweenMax.to("#lil-guy", 3, {
bezier: {
MorphSVGPlugin.pathDataToBezier("#path", {align: "#lil-guy" }),
type: "cubic"
},
ease: Linear.easeNone,
repeat: -1
});
<path id="path" d="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" fill="none" />
默认情况下,会将我要进行动画的组(在本例中为 #lil-guy
)的左上角与路径轨迹对齐。这会导致视觉上的错位。因此,我使用 TweenLite.set
将 #lil-guy
设置为使用中心点:
TweenLite.set("#lil-guy", {xPercent:-50, yPercent:-50});
你还可以通过将一个对象作为该方法的第二个参数传递,并在 pathDataToBezier
中定义 offsetX
和 offsetY
来偏移这些路径,注意,你可能需要扩展 viewBox
,以确保你正在进行动画的组或属性不会被裁剪掉。
// 将路径坐标在 x 轴上偏移 125px,在 y 轴上偏移 50px:
TweenMax.to("#lil-guy", 3, {
bezier: {
values: MorphSVGPlugin.pathDataToBezier("#path", {
offsetX: 125,
offsetY: 50,
align: "#lil-guy"
}),
type: "cubic"
},
ease: Linear.easeNone,
repeat: -1
});
你甚至可以为这个定位定义一个矩阵坐标。
// 将路径坐标放大 1.25 倍
// 并将其在 x 轴上偏移 120px
// 在 y 轴上偏移 30px:
TweenMax.to("#lil-guy", 3, {
bezier: {
values: MorphSVGPlugin.pathDataToBezier("#path", {
matrix:[1.5,0,0,1.5,120,-30],
align:"lil-guy"}),
type: "cubic"
},
ease: Linear.easeNone,
repeat: -1
});
另一个选择是将 align
属性设置为 “relative”
。这将防止动画对象跳跃,因为它会保持每个坐标相对于 x:0
和 y:0
的位置。在之前的示例中,我使用 align
将运动与 #lil-guy
组本身配对。
有关 GreenSock 的 Bezier 插件 API 中这个新功能(新是指在本文发布之日新发布的功能!)的更多信息,请查看他们的 文档,以及这个 很棒的解释视频。
🎭 形状变形
以前,你可以将路径数据作为值传递给 animate
属性,以使形状变形。Noah Blon 有一个很好的例子:
替代方案:Snap.svg 或 SVG Morpheus
一些库提供了变形路径或形状值,例如 Snap.svg 和 SVG Morpheus,但需要注意的是(即使在 SMIL 中也是如此),形状必须具有相同数量的点,否则变形看起来很糟糕,或者完全失败。这在预处理方面令人失望,因为这意味着你必须仔细跟踪你正在制作的内容,或者与你的设计师良好协作,以确保你获得这些(有时是任意的)中间点数据。额外的点也会不必要地膨胀你的代码。
替代方案:GreenSock MorphSVG
我强烈推荐 GSAP 的 MorphSVG 插件,因为它可以很好地变形具有不同数量点的形状和路径。请查看本网站徽标上的切换按钮,以演示变形的效果。这里还有另一个例子:
因为 MorphSVG 插件可以对路径数据进行动画处理,所以如果你需要转换形状,可以使用他们的 convertToPath
选项:
MorphSVGPlugin.convertToPath("ellipse");
// or circle, rect, etc
这使我们能够进行非常复杂的形状动画,并且是 Web 上所有运动的改变者。
这个插件还提供了一些额外的功能,使其更加出色。第一个是实用程序插件 findShapeIndex
。假设你对形状的变形方式不满意(虽然十有八九自动预设会正常工作),你可以加载该插件(别担心,你不需要在生产中添加额外的重量,因为它不需要),并将两个值传递给它:要进行动画的第一个形状的 ID 和第二个形状的 ID。一个 GUI 会弹出,你可以在其中切换值,它还会自动使用 repeat: -1
,以便它会不断地在形状之间循环。
findShapeIndex("#hex", "#star");
// you can comment out above line to automatically disable findShapeIndex() UI
一旦你有了这个额外的值,你就可以在 morphSVG
对象中传递 shapeIndex
:
TweenLite.to("#hex", 1, {morphSVG: { shape: "#star", shapeIndex: 1 }});
第二个额外的功能是该插件能够解析剪切路径,这是其他库无法提供的。最后,你还可以重用第一个起始 ID(而不是必须存储该路径数据以供重用)。值得一提的是,当该插件首次发布时,这些功能不可用,但 GreenSock 认识到需要支持这些功能,因此将其包含在内。
现在,我们不再受限于指定的点数,我们拓宽了各种效果的可能性。下面,我制作了一些烟雾:
🖱️ DOM 事件
SMIL 中很好地集成了诸如悬停和点击之类的事件。为了启动动画,可以指定 begin="click"
或 begin="hover"
。
<animate
xlink:href="#rectblue"
attributeName="x"
from="0"
to="300"
dur="1s"
begin="click"
values="20; 50"
keyTimes="0; 1"
fill="freeze" />
替代方案:JavaScript
有像 onmouseenter
和 onmouseleave
这样的原生 DOM 事件,用于悬停,以及 click
事件,用于点击。你可以使用它们来更改事件触发器,从而触发基于 JavaScript 的动画。
替代方案:JavaScript + CSS
你可以使用 JavaScript 来更改类名或直接更改 CSS 样式。以下是一种可能性:更改 animation-play-state
以从事件触发器启动动画。
.st0 {
animation: moveAcross 1s linear both;
animation-play-state: paused;
}
@keyframes moveAcross {
to {
transform: translateX(100px);
}
}
document.getElementById("rectblue").addEventListener("click", function() {
event.target.style.animationPlayState = "running";
});
或者在 jQuery 中:
$(".st0").on("click", function() {
$(this).css("animation-play-state", "running");
});
这种实现不会像 SMIL 示例那样立即将动画重置到开头。如果你想实现这一点,CSS-Tricks 上的一篇之前的文章详细介绍了几种实现方法。
替代方案:Greensock
在 GSAP 中,重启更加简单。我们可以将动画添加到时间线中,将其设置为暂停,然后在点击时重启它。这种实现更接近你对 SMIL 的预期,因为我们不需要做任何 hacky 的事情,比如克隆/重新插入 DOM 节点或更改元素上设置的任何属性。
// 实例化一个 TimelineLite
var tl = new TimelineLite();
// 将一个动画添加到时间线
tl.to(foo, 0.5, { left: 100 });
$(".st0").on("click", function() {
tl.restart();
});
⏱️ 在“Y”完成后运行“X”
SMIL 还允许更复杂的时间事件,例如 begin="circ-anim.begin + 1s"
。这在链接动画时特别有用。
替代方案:CSS
在 CSS 中,我们可以通过在第二个值上设置延迟来链接动画:
.foo {
animation: foo-move 2s ease both;
}
.bar {
animation: bar-move 4s 2s ease both;
/* 2 秒的值对应于第一个动画的迭代长度。 */
}
这种方法有点令人沮丧,因为你必须确保记住更改第一个间隔以及延迟。
替代方案:CSS 预处理
如果我们使用(例如)Sass 中的变量,维护和管理这些间隔会更容易:
$secs: 2s;
.foo {
animation: foo-move $secs ease both;
}
.bar {
animation: bar-move 4s $secs ease both;
}
现在我们知道,如果我们更新一个值,它们将保持同步。
但是,如果我们想始终检测动画何时完成,JavaScript 提供了一些不错的原生功能,比如 animationEnd
:
$("#rectblue").on("animationend", function() {
$(this).closest("svg").find("#rectblue2").css("animation-play-state", "running");
});
#rectblue2 {
animation: moveAcross 2s 1s ease both;
animation-play-state: paused;
}
🕰️ 计时器
SMIL 还提供了一个 set
属性,它允许你设置一个计时器,以便在指定的时间后执行某些操作。
<set
attributeName="visibility"
to="visible"
begin="2s"
fill="freeze" />
替代方案:JavaScript
我们可以使用 setTimeout
来实现相同的行为。
setTimeout(function() {
document.getElementById("rectblue").style.visibility = "visible";
}, 2000);
替代方案:GreenSock
GreenSock 提供了 delay
属性,可以实现相同的效果。
TweenLite.to("#rectblue", 0, {
delay: 2,
visibility: "visible"
});
🔄 循环
SMIL 允许你通过 repeatCount
属性来控制动画的循环次数。
<animateMotion
xlink:href="#lil-guy"
dur="3s"
repeatCount="indefinite"
fill="freeze"
path="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" />
替代方案:CSS
在 CSS 中,你可以使用 animation-iteration-count
属性来控制动画的循环次数。
.foo {
animation: foo-move 2s ease infinite both;
}
infinite
值意味着动画将无限循环。你也可以指定一个具体的数字,例如 animation-iteration-count: 3
,表示动画将循环三次。
替代方案:GreenSock
GreenSock 提供了 repeat
属性,可以实现相同的行为。
TweenLite.to("#lil-guy", 3, {
bezier: {
values: MorphSVGPlugin.pathDataToBezier("#path", {align: "#lil-guy" }),
type: "cubic"
},
ease: Linear.easeNone,
repeat: -1
});
repeat: -1
表示动画将无限循环。你也可以指定一个具体的数字,例如 repeat: 3
,表示动画将循环三次。
🎬 动画组
SMIL 允许你使用 animate
元素来创建动画组,并通过 begin
属性来控制组内动画的执行顺序。
<animate
xlink:href="#rectblue"
attributeName="x"
from="0"
to="300"
dur="1s"
begin="click"
values="20; 50"
keyTimes="0; 1"
fill="freeze" />
<animate
xlink:href="#rectblue"
attributeName="y"
from="0"
to="300"
dur="1s"
begin="click + 1s"
values="20; 50"
keyTimes="0; 1"
fill="freeze" />
替代方案:CSS
在 CSS 中,你可以使用 animation-delay
属性来控制动画的延迟时间。
.foo {
animation: foo-move 2s ease both;
}
.bar {
animation: bar-move 4s 1s ease both;
/* 1 秒的值对应于第一个动画的迭代长度。 */
}
这种方法有点令人沮丧,因为你必须确保记住更改第一个间隔以及延迟。
替代方案:CSS 预处理
如果我们使用(例如)Sass 中的变量,维护和管理这些间隔会更容易:
$secs: 2s;
.foo {
animation: foo-move $secs ease both;
}
.bar {
animation: bar-move 4s $secs ease both;
}
现在我们知道,如果我们更新一个值,它们将保持同步。
替代方案:GreenSock
GreenSock 提供了 TimelineLite
类,可以用来创建动画组,并通过 delay
属性来控制组内动画的执行顺序。
var tl = new TimelineLite();
tl.to("#rectblue", 1, { x: 300 });
tl.to("#rectblue", 1, { y: 300 }, "+=1");
+=1
表示第二个动画将在第一个动画完成 1 秒后开始。
总结
SMIL 曾经是 SVG 动画的王者,但随着浏览器支持的减少,我们不得不寻找其他替代方案。幸运的是,CSS、JavaScript 和 GreenSock 等工具提供了强大的功能,可以让我们实现 SMIL 中的所有功能,甚至更多。
选择哪种方法取决于你的需求和偏好。如果你需要最广泛的浏览器支持,那么 GreenSock 是一个不错的选择。如果你更喜欢使用 CSS,那么 CSS 动画是一个不错的选择。如果你需要更灵活的控制,那么 JavaScript 是一个不错的选择。
无论你选择哪种方法,都希望你能够轻松地创建出令人惊叹的 SVG 动画!