动画是网络中不可或缺的一部分。与互联网早期使用 GIF 图像不同,现在的动画更加细腻和高雅。设计师和前端开发者利用动画使网站看起来更加精致,不仅提升用户体验,还吸引用户关注重要的元素,以传达信息。
本篇文章我们就来一起学习学习如何在 CSS 中实现 SVG 动画。
开篇:CSS 与 SVG 相关核心概念
在实践动画之前,你需要了解 svg 的内部工作原理。SVG 与 HTML 类似,我们可以使用 XML语法定义 SVG 元素,并使用 CSS 对它们进行样式上的设置,你把它们当做是 HTML 一样就行。
不过,与 HTML 不同的是,SVG 元素专门用于绘制图形。例如,我们可以使用 <rect>
来绘制矩形,使用 <circle>
来绘制圆等等。svg 还定义了 <ellipse>、<line>、<polyline>、<polygon> 和 <path>
用于绘制图形的元素。
SVG 元素的完整列表甚至包括 <animate>
,它允许你使用同步多媒体集成语言(SMIL)创建动画。然而,它的未来是不确定的,因为 Chromium 团队建议尽可能使用基于CSS 或javascript 的方法来创建 svg 动画。
而元素可用的属性取决于元素本身。例如 <rect>
具有宽度和高度属性,而 <circle>
元素具有定义其半径的 r
属性。
同时需要注意一点:虽然大多数HTML元素可以有子元素,但大多数 SVG 元素不能有子元素。group
元素 <g>
是一个例外,因为可以使用它来同时对多个元素应用 CSS 样式。
<svg>
元素及其属性
HTML 和 SVG 之间的另一个重要区别是我们如何定位元素,特别是通过给定的外部 < SVG >
元素的 viewBox
属性。
这个属性取值由四个数字组成,分别是:min-x、min-y、width
和height
,中间用空格或逗号分隔。它们一起指定了我们希望浏览器呈现多少 SVG 图形。同时该区域将根据 <svg>
元素的宽度和高度属性进行缩放,以适应视口的边界。
不过, 视口 viewport 的宽度和高度属性的比例可能确实不同于 viewBox 属性的宽度和高度部分的比例。
默认情况下,SVG 画布的长宽比将被保留,代价是 viewBox 比指定的要大,从而导致viewport 内呈现的字体更小。但是你可以通过 preserveAspectRatio
属性指定不同的行为。它能使我们能够独立绘制图像,并且无论上下文或渲染大小如何,所有元素都将正确定位。
下面我们一起来感受一下。
基础示例
CSS 的 transition
属性允许我们定义属性变化的速率和持续时间。
transition: margin-right 4s ease-in-out 1s; /* property name | duration | easing function | delay */
例如,下面这个例子,当你用鼠标悬停在 SVG 圆圈上时,它的颜色会发生变化,而不是立即从起始值跳到结束值。
代码语言:javascript复制<svg viewBox="0 0 300 200">
<circle cx="150" cy="100" r="60" class="spot" />
</svg>
代码语言:javascript复制html {
height: 100%;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
svg {
max-width: 50vw;
max-height: 80vh;
}
.spot {
fill: #204ecf;
transition: fill 0.5s;
}
.spot:hover {
fill: #03cc83;
}
我们可以为多个CSS属性定义过渡,每个属性都可以有单独的过渡值。然而,这种方法有两个明显的限制。
第一个限制是,当属性值发生变化时,会自动触发转换。这在某些场景下是不方便的。例如,我们不能有一个无限循环的动画。
第二个限制是转换总是有两个步骤:初始状态和最终状态。我们可以延长动画的持续时间,但不能添加不同的关键帧。
于是,这就催生了一个更强大的概念: CSS animation
。使用 CSS animation,我们可以有多个关键帧和一个无限循环。例如下面这个例子:
<svg viewBox="0 0 300 200">
<rect width="100%" height="100%" class="background" />
<g class="cross">
<line x1="130" y1="80" x2="170" y2="120" />
<line x1="130" y1="120" x2="170" y2="80" />
</g>
</svg>
代码语言:javascript复制@keyframes move-around {
0% {
transform: translate(-40%, -35%);
}
25% {
transform: translate(40%, -35%);
}
50% {
transform: translate(40%, 35%);
}
75% {
transform: translate(-40%, 35%);
}
100% {
transform: translate(-40%, -35%);
}
}
html {
height: 100%;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
svg {
max-width: 50vw;
max-height: 80vh;
}
.background {
fill: #03cc83;
}
.cross {
animation: move-around 5s infinite;
stroke: #262d3d;
stroke-width: 10px;
}
要在多个关键帧上使用 animation 属性,我们需要使用 @keyframes
规则来定义关键帧。关键帧的时间是用相对单位(百分比)来定义的。每个关键帧描述一个或多个 CSS 属性在那个时间点的值。CSS animation 将确保关键帧之间的平滑过渡。
我们使用 animation 属性将具有描述的关键帧的动画应用到所需的元素上。与 transition属性类似,它接受一个持续时间、一个缓和函数和一个延迟。
唯一的区别是第一个参数是我们的 @keyframes
称,而不是属性名称:
/* @keyframes name | duration | easing-function | delay */ animation: my-sliding-animation 3s linear 1s;
示例:为汉堡菜单添加切换动画
现在我们对svg动画的工作原理有了基本的了解。我们可以开始构建一个菜单切换的动画:
我们发现这个菜单能够巧妙地吸引了用户的注意力,告诉用户可以使用图标关闭菜单。
接下来我们来一起解析具体的代码。
首先我们创建一个 svg 元素,用于创建“汉堡”菜单图形:
代码语言:javascript复制<svg class="hamburger">
<line x1="0" y1="50%" x2="100%" y2="50%" class="hamburger__bar hamburger__bar--top" />
<line x1="0" y1="50%" x2="100%" y2="50%" class="hamburger__bar hamburger__bar--mid" />
<line x1="0" y1="50%" x2="100%" y2="50%" class="hamburger__bar hamburger__bar--bot" />
</svg>
代码中,每行有两组属性。其中,x1
和 y1
代表直线的起点坐标,而 x2
和 y2
代表直线的终点坐标。你会发现我使用相对单位 % 来设置位置,这是一种确保图像内容调整大小以适应包含 SVG 元素的简单方法。虽然这种方法在这种情况下有效,但有一个很大的缺点:
我们无法维护以这种方式定位的元素的长宽比。为此,我们必须使用<svg>
元素的 viewBox 属性。
注意,我们对 SVG 元素应用了 CSS 类,应用了一些基本样式。
在这个样式中,我们设置了 <svg>
元素的大小,并更改光标类型以表明它是可单击的。但是要设置线条的颜色和粗细,我们将使用 stroke和stroke-width
属性。
.hamburger {
width: 62px;
height: 62px;
cursor: pointer;
}
.hamburger__bar {
stroke: white;
stroke-width: 10%;
}
如果我们现在渲染,我们会看到所有三条线都有相同的大小和位置,彼此完全重叠。不幸的是,我们不能通过 CSS 独立地改变开始和结束的位置。但是我们可以使用 CSS transform 属性移动整个元素的顶部和底部的条:
代码语言:javascript复制.hamburger__bar--top {
transform: translateY(-40%);
}
.hamburger__bar--bot {
transform: translateY(40%);
}
通过移动 Y 轴上的条,我们最终得到了一个看起来不错的汉堡菜单图形。
现在继续编写菜单的第二个状态: 关闭按钮。
我们将依赖于应用于SVG元素的 .is-opened
类来在这两种状态之间切换。为了使结果更易于访问,让我们将SVG包装在 <button>
元素中,并处理该级别上的单击。
添加和删除 .is-opened
类的过程将由一个简单的 JavaScript 处理:
const hamburger = document.querySelector("button");
hamburger.addEventListener("click", () => {
hamburger.classList.toggle("is-opened");
});
为了创建 X
图形,我们可以对每一条 line
应用不同的变换属性。因为新的变换属性将覆盖旧的。
从那里,我们可以将顶部杆绕其中心顺时针旋转 45
度,并将底部杆 逆时针旋转 45 度
。我们可以水平缩小中间条,直到它足够窄,让它隐藏在 X
的中心后面:
.is-opened .hamburger__bar--top {
transform: rotate(45deg);// 顺时针旋转 `45` 度
}
.is-opened .hamburger__bar--mid {
transform: scaleX(0.1); // 水平缩小中间条
}
.is-opened .hamburger__bar--bot {
transform: rotate(-45deg); // 逆时针旋转 45 度
}
默认情况下,SVG 元素的 transform-origin
属性通常为 0,0
。这意味着我们的条将围绕视口的左上角旋转,但我们希望它们围绕中心旋转。为了解决这个问题,让我们将.hamburger__bar
类的transform-origin
属性设置为 center
。
transition 属性
transition 属性告诉浏览器在两种不同状态的 CSS 属性之间平滑过渡。这里,我们想把我们对 transform
属性的改变做成动画,它能决定了条形条的位置、方向和比例。
我们还可以使用 transition-duration
属性控制转换的持续时间。为了使动画和最终的SVG 转换看起来更简洁,我们将设置0.3
秒的持续时间:
.hamburger__bar {
transition-property: transform;
transition-duration: 0.3s;
...
我们唯一需要的JavaScript代码就是使图标状态变成可切换的:
代码语言:javascript复制const hamburger = document.querySelector("button");
hamburger.addEventListener("click", () => {
hamburger.classList.toggle("is-opened");
});
这里,我们使用 querySelector()
通过 .mute
类选择外部 SVG 元素。然后,我们添加一个单击事件侦听器。当触发 click
事件时,我们只在 <svg>
本身上切换 .is-active
类,而不是在层次结构中更深入地切换。因为我们让CSS动画只应用于带有.is-active
类的元素,所以切换这个类会激活和关闭动画。
最后,我们将HTML主体转换为一个 Flex 容器,这将帮助我们在水平和垂直方向上居中图标:
代码语言:javascript复制 body {
display: flex;
justify-content: center;
align-items: center;
background-color: #222;
height: 100vh;
}
这样,我们就使用一些基本的 CSS 和一个简短的 JavaScript 片段构建了一个功能齐全的动画按钮。
使用来自矢量图形编辑器的 SVG 数据
前面我们一起实现的汉堡菜单非常简单。但是如果我们想做更复杂的东西呢? 这就是 SVG 变得困难的地方,这个时候需要借助矢量图形编辑软件。
我们的第二个 SVG 动画是一个显示耳机图标的静音按钮。当音乐激活时,图标会跳动和跳舞;静音后,图标会被划掉:
代码语言:javascript复制<svg class="mute is-active" viewBox="0 0 100 100">
<g class="mute__headphones">
<path d="M92.6,50.075C92.213,26.775 73.25,7.938 50,7.938C26.75,7.938 7.775,26.775 7.388,50.075C3.112,51.363 -0.013,55.425 -0.013,60.25L-0.013,72.7C-0.013,78.55 4.575,83.3 10.238,83.3L18.363,83.3L18.363,51.6C18.4,51.338 18.438,51.075 18.438,50.813C18.438,33.275 32.6,19 50,19C67.4,19 81.563,33.275 81.563,50.813C81.563,51.088 81.6,51.338 81.638,51.6L81.638,83.313L89.763,83.313C95.413,83.313 100.013,78.563 100.013,72.713L100.013,60.263C100,55.438 96.875,51.362 92.6,50.075Z" />
<path d="M70.538,54.088L70.538,79.588C70.538,81.625 72.188,83.275 74.225,83.275L74.225,83.325L78.662,83.325L78.662,50.4L74.225,50.4C72.213,50.4 70.538,52.063 70.538,54.088Z" />
<path d="M25.75,50.4L21.313,50.4L21.313,83.325L25.75,83.325L25.75,83.275C27.788,83.275 29.438,81.625 29.438,79.588L29.438,54.088C29.45,52.063 27.775,50.4 25.75,50.4Z" />
</g>
<line x1="12" y1="12" x2="88" y2="88" class="mute__strikethrough" />
</svg>
代码语言:javascript复制body {
display: flex;
justify-content: center;
align-items: center;
background-color: #222;
height: 100vh;
}
.mute {
fill: white;
width: 170px;
height: 70px;
cursor: pointer;
}
.mute__headphones {
transform-origin: center;
transform: scale(0.9);
}
.is-active .mute__headphones {
animation: pulse 2s infinite;
}
.mute__strikethrough {
stroke: red;
opacity: 0.8;
stroke-width: 12px;
}
.is-active .mute__strikethrough {
opacity: 0;
}
@keyframes pulse {
0% {
transform: scale(0.9);
}
40% {
transform: scale(1) rotate(5deg);
}
80% {
transform: scale(1) rotate(-5deg);
}
100% {
transform: scale(0.9) rotate(0);
}
}
代码语言:javascript复制const muteButton = document.querySelector(".mute");
muteButton.addEventListener("click", () => {
muteButton.classList.toggle("is-active");
});
在 svg 元素中,我们使用了来自矢量图形编辑软件的图形信息对耳机进行了绘制。
不过,在矢量图像编辑软件中创建的 SVG 图标不太可能使用相对单位。无论包含图标的SVG 元素的宽高比如何,我们都希望确保图标的宽高比得到维护。因此,为了使这种级别的控制成为可能,我们将使用 viewBox
属性。
在本例中,我将其转换为 100 x 100
像素的 viewBox。
让我们确保图标居中并且大小合适。我们将静音类应用到基本SVG元素,然后添加以下CSS样式:
代码语言:javascript复制.mute {
fill: white;
width: 80px;
height: 70px;
cursor: pointer;
}
SVG 动画的起点
接着上面一节,现在整洁的 SVG 包含一个 <g>
元素,该元素包含三个 <path>
元素。
path
元素允许我们绘制直线、曲线和圆弧。路径用一系列命令来描述,这些命令描述了应该如何绘制形状。由于我们的图标由三个互不相连的形状组成,我们有三条路径来描述它们。
同时在三条路径上应用脉动和舞蹈转换,而不是用 CSS 分别为 SVG 路径添加动画。
代码语言:javascript复制 <svg class="mute" viewBox="0 0 100 100">
<g>
<path d="M92.6,50.075C92.213,26.775 73.25,7.938 50,7.938C26.75,7.938 7.775,26.775 7.388,50.075C3.112,51.363 -0.013,55.425 -0.013,60.25L-0.013,72.7C-0.013,78.55 4.575,83.3 10.238,83.3L18.363,83.3L18.363,51.6C18.4,51.338 18.438,51.075 18.438,50.813C18.438,33.275 32.6,19 50,19C67.4,19 81.563,33.275 81.563,50.813C81.563,51.088 81.6,51.338 81.638,51.6L81.638,83.313L89.763,83.313C95.413,83.313 100.013,78.563 100.013,72.713L100.013,60.263C100,55.438 96.875,51.362 92.6,50.075Z" />
<path d="M70.538,54.088L70.538,79.588C70.538,81.625 72.188,83.275 74.225,83.275L74.225,83.325L78.662,83.325L78.662,50.4L74.225,50.4C72.213,50.4 70.538,52.063 70.538,54.088Z" />
<path d="M25.75,50.4L21.313,50.4L21.313,83.325L25.75,83.325L25.75,83.275C27.788,83.275 29.438,81.625 29.438,79.588L29.438,54.088C29.45,52.063 27.775,50.4 25.75,50.4Z" />
</g>
</svg>
为了让耳机跳动和跳舞,过渡是不够的,需要使用到关键帧动画。
在这种情况下,我们的开始和结束关键帧(分别为0%和100%)使用略微缩小的耳机图标。
于是,对于动画的前40%,我们将图像稍微扩大并倾斜 5 度。然后,对于接下来 40% 的动画,我们将其缩小到 0.9x
,并将其旋转 5 度到另一边。最后,对于动画的最后 20%,图标转换返回到相同的初始参数,以便顺利循环。具体代码如下:
@keyframes pulse {
0% {
transform: scale(0.9);
}
40% {
transform: scale(1) rotate(5deg);
}
80% {
transform: scale(1) rotate(-5deg);
}
100% {
transform: scale(0.9) rotate(0);
}
}
优化
为了展示关键帧是如何工作的,上面的代码中,我们将关键帧设置得过于冗长。其实有三种方法可以缩短它。
因为我们的 100% 关键帧设置了整个变换列表,如果我们完全忽略 rotate()
,它的值将默认为 0:
100% {
transform: scale(0.9);
}
其次,因为循环动画是循环的,因此 0% 和 100% 的关键帧是匹配的。于是,可以使用相同的 CSS 规则定义它们:
代码语言:javascript复制0%, 100% {
transform: scale(0.9);
}
最后,我们将很快应用 transform: scale(0.9);
到 mute__headphones
类,当我们这样做时,我们根本不需要定义开始和结束关键帧!它们将默认为mute__headphones
使用的静态样式。
现在我们已经定义了动画关键帧,我们可以应用动画了。我们将.mute__headphones
类添加到 <g>
元素中,这样它就会影响耳机图标的所有三个部分。
首先,我们再次将 transform-origin
设置为 center
,因为我们希望图标围绕其中心旋转。
接着,我们在只有当 .is-active
父类存在时,使用 animation 属性应用动画。
.mute__headphones {
transform-origin: center;
transform: scale(0.9);
}
.is-active .mute__headphones {
animation: pulse 2s infinite;
}
同时,我们在状态之间切换所需的JavaScript也遵循与汉堡菜单相同的方式:
代码语言:javascript复制const muteButton = document.querySelector(".mute");
muteButton.addEventListener("click", () => {
muteButton.classList.toggle("is-active");
});
最后一部分,我们将添加的是当图标处于非 active 状态时出现的划线。由于这是一个简单的svg 元素,我们可以手动绘制它。我们知道画布的边缘是 0 和 100,所以很容易计算出线的开始和结束位置:
代码语言:javascript复制<line x1="12" y1="12" x2="88" y2="88" class="mute__strikethrough" />
因为我们将一个类直接应用于划线 <line>
元素,所以我们可以通过 CSS 对它进行样式化。我们只需要确保当图标处于活动状态时,这一直线是不可见的:
.mute__strikethrough {
stroke: red;
opacity: 0.8;
stroke-width: 12px;
}
.is-active .mute__strikethrough {
opacity: 0;
}
我们还可以将.is-active
类直接添加到 SVG 中。这将使动画在页面加载时立即开始。
现在我们终于完成了这个动画过程。
结尾
目前,我们只接触 CSS 动画的皮毛,例如知道了如何手工绘制 SVG 代码以实现简单的动画。但知道如何以及何时使用外部矢量编辑器创建的图形也很重要。同时,对于复杂的动画场景,开发者可以去探索一下像 GSAP 或 animejs 这样的动画库实现更复杂的动画。