我至今没想到,我也能在 CSS 中实现 SVG 动画了

2023-10-07 19:59:48 浏览数 (2)

动画是网络中不可或缺的一部分。与互联网早期使用 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、widthheight,中间用空格或逗号分隔。它们一起指定了我们希望浏览器呈现多少 SVG 图形。同时该区域将根据 <svg> 元素的宽度和高度属性进行缩放,以适应视口的边界。

不过, 视口 viewport 的宽度和高度属性的比例可能确实不同于 viewBox 属性的宽度和高度部分的比例。

默认情况下,SVG 画布的长宽比将被保留,代价是 viewBox 比指定的要大,从而导致viewport 内呈现的字体更小。但是你可以通过 preserveAspectRatio 属性指定不同的行为。它能使我们能够独立绘制图像,并且无论上下文或渲染大小如何,所有元素都将正确定位。

下面我们一起来感受一下。

基础示例

CSS 的 transition 属性允许我们定义属性变化的速率和持续时间。

代码语言:javascript复制
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,我们可以有多个关键帧和一个无限循环。例如下面这个例子:

代码语言:javascript复制
<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 称,而不是属性名称:

代码语言:javascript复制
/* @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>

代码中,每行有两组属性。其中,x1y1 代表直线的起点坐标,而 x2y2 代表直线的终点坐标。你会发现我使用相对单位 % 来设置位置,这是一种确保图像内容调整大小以适应包含 SVG 元素的简单方法。虽然这种方法在这种情况下有效,但有一个很大的缺点:

我们无法维护以这种方式定位的元素的长宽比。为此,我们必须使用<svg>元素的 viewBox 属性。

注意,我们对 SVG 元素应用了 CSS 类,应用了一些基本样式。

在这个样式中,我们设置了 <svg>元素的大小,并更改光标类型以表明它是可单击的。但是要设置线条的颜色和粗细,我们将使用 stroke和stroke-width 属性。

代码语言:javascript复制
.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 处理:

代码语言:javascript复制
const hamburger = document.querySelector("button");
hamburger.addEventListener("click", () => {
  hamburger.classList.toggle("is-opened");
});

为了创建 X 图形,我们可以对每一条 line 应用不同的变换属性。因为新的变换属性将覆盖旧的。

从那里,我们可以将顶部杆绕其中心顺时针旋转 45 度,并将底部杆 逆时针旋转 45 度 。我们可以水平缩小中间条,直到它足够窄,让它隐藏在 X 的中心后面:

代码语言:javascript复制
.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秒的持续时间:

代码语言:javascript复制
 .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%,图标转换返回到相同的初始参数,以便顺利循环。具体代码如下:

代码语言:javascript复制
@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:

代码语言:javascript复制
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 属性应用动画。

代码语言:javascript复制
.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 对它进行样式化。我们只需要确保当图标处于活动状态时,这一直线是不可见的:

代码语言:javascript复制
.mute__strikethrough {
  stroke: red;
  opacity: 0.8;
  stroke-width: 12px;
}
.is-active .mute__strikethrough {
  opacity: 0;
}

我们还可以将.is-active类直接添加到 SVG 中。这将使动画在页面加载时立即开始。

现在我们终于完成了这个动画过程。

结尾

目前,我们只接触 CSS 动画的皮毛,例如知道了如何手工绘制 SVG 代码以实现简单的动画。但知道如何以及何时使用外部矢量编辑器创建的图形也很重要。同时,对于复杂的动画场景,开发者可以去探索一下像 GSAP 或 animejs 这样的动画库实现更复杂的动画。

0 人点赞