如何在CSS中处理SVG动画的问题

439 阅读16分钟

如何在CSS中处理SVG动画

动画使网站看起来更精致、更令人兴奋,并有助于改善用户体验。探索如何利用SVG和CSS的综合潜力来创建动画,而不依赖外部库。

动画是网络中无处不在的一部分。与互联网早期困扰网站的闪烁的GIF图像不同,今天的动画更加微妙和有品位。设计师和前端专家使用它们来使网站看起来更有质感,增强用户体验,唤起对重要元素的关注,并传达信息。

网络开发者可以从结合SVGCSS的力量来创建动画而不使用外部库中受益。这个SVG动画教程展示了如何为真实世界的项目建立自定义动画。

使用CSS制作SVG动画。核心概念

在用CSS制作SVG动画之前,开发者需要了解SVG的内部工作原理。幸运的是,它与HTML很相似。我们用XML语法定义SVG元素,然后用CSS对其进行样式处理,就像HTML一样。

SVG元素是为绘制图形而特意建立的。我们可以用<rect> 来画矩形,用<circle> 来画圆,等等。SVG还定义了<ellipse><line><polyline><polygon><path>

注意: SVG元素的完整列表 甚至包括<animate> ,它允许你使用同步多媒体集成语言(SMIL)创建动画。然而,它的未来是不确定的,Chromium团队建议尽可能地使用基于CSS或JavaScript的方法来制作SVG的动画。

可用的属性取决于元素,因此,虽然<rect>widthheight 属性,但<circle> 元素有r 属性,它定义了它的半径。

选择基本的SVG元素;坐标是相对于原点的(SVG视口的左上角)。

虽然大多数HTML元素可以有子元素,但大多数SVG元素不能。一个例外是组元素<g> ,我们可以用它来同时对多个元素应用CSS样式和变换。

<svg> 元素和它的属性

HTML和SVG的另一个重要区别是我们如何定位元素,特别是通过一个给定的外部<svg> 元素的viewBox 属性。它的值由四个数字组成,用空格或逗号分隔。min-x,min-y,width, 和height 。这些数字共同指定了我们希望浏览器渲染多少SVG图画。该区域将被缩放以适应视口的边界,如widthheight 属性所定义的<svg> 元素。

当涉及到信箱时,视口的widthheight 属性的比例确实可能与viewBox 属性的widthheight 部分的比例不同。

默认情况下,SVG画布的长宽比将以大于指定的viewBox 为代价被保留,从而导致在视口内呈现出较小的信箱状的画面。但是你可以通过属性指定不同的行为 preserveAspectRatio属性指定不同的行为。

这使我们能够孤立地绘制图像,并确信所有的元素都会被正确地定位,不管是在什么情况下或渲染的大小。

虽然你可以用手来编码SVG图像,但更复杂的图像可能需要一个矢量图形程序(我们的SVG动画教程演示了这两种技术)。我选择的编辑器是Affinity Designer,但任何编辑器都应该为这里所涉及的简单操作提供足够的功能。

CSS 过渡和动画

CSS过渡允许我们定义属性变化的速度和持续时间。在这个例子中,当你用鼠标悬停在一个SVG圆圈上时,该圆圈的颜色会发生变化,而不是瞬间从起始值跳到结束值,而是平稳地过渡。

请看CodePen上Filip Defar (@dabrorius)的PenTransition例子

我们可以用transition 属性来定义过渡,该属性接受我们想要过渡的属性名称、过渡的持续时间、过渡的定时函数(也称为缓和函数),以及效果开始前的延迟长度。

/* property name | duration | easing function | delay */
transition: margin-right 4s ease-in-out 1s;

我们可以为多个CSS属性定义过渡,每个属性都可以有单独的过渡值。然而,这种方法有两个明显的限制。

第一个限制是,当一个属性值发生变化时,会自动触发过渡。这在某些用例中是不方便的。例如,我们不可能有一个无限循环的动画。

第二个限制是,过渡总是有两个步骤:初始状态和最终状态。我们可以延长动画的持续时间,但我们不能添加不同的关键帧。

这就是为什么存在一个更强大的概念。CSS动画。通过CSS动画,我们可以有多个关键帧和一个无限循环。

要在多个关键帧上为CSS属性制作动画,首先我们需要使用@keyframes at-rule来定义关键帧。关键帧的时间是以相对单位(百分比)定义的,因为在这一点上,我们还没有定义动画的持续时间。每个关键帧都描述了一个或多个CSS属性在该时间点上的值。CSS动画将确保关键帧之间的平滑过渡。

我们使用animation 属性将描述关键帧的动画应用于所需的元素。与transition 属性类似,它接受一个持续时间、一个缓和函数和一个延迟。

唯一不同的是,第一个参数是我们的@keyframes ,而不是一个属性名称。

/* @keyframes name | duration | easing-function | delay */
animation: my-sliding-animation 3s linear 1s;

汉堡包菜单切换的动画

现在我们对SVG的动画制作有了基本的了解,我们可以开始制作一个经典的动画--在 "汉堡包 "图标和关闭按钮(一个 "X")之间平滑过渡的菜单切换。

这是一个微妙但有价值的动画。它吸引了用户的注意力,告诉他们可以用这个图标来关闭菜单。

我们通过创建一个有三条线的SVG元素来开始我们的演示。

<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类。有很多属性可以通过CSS来改变,所以让我们给我们的SVG元素应用一些基本的样式设计。

我们将设置<svg> 元素的大小,以及改变光标类型以表明它是可点击的。但是为了设置线条的颜色和粗细,我们将使用strokestroke-width 属性。你可能期望使用colorborder ,但与<svg> 本身不同,SVG子元素不是HTML元素,所以它们通常有不同的属性名称。

.hamburger {
  width: 62px;
  height: 62px;
  cursor: pointer;
}
.hamburger__bar {
  stroke: white;
  stroke-width: 10%;
}

如果我们在这一点上进行渲染,我们会看到这三行都有相同的大小和位置,完全重叠在一起。不幸的是,我们不能通过CSS独立地改变开始和结束的位置,但我们可以移动整个元素。让我们用transform CSS属性来移动顶部和底部的条形图。

.hamburger__bar--top {
  transform: translateY(-40%);
}
.hamburger__bar--bot {
  transform: translateY(40%);
}

通过在Y轴上移动条形图,我们最终得到了一个看起来不错的汉堡包。

现在是时候为我们的第二个状态编码了:关闭按钮。我们依靠一个应用于SVG元素的.is-opened CSS类来在两种状态之间切换。为了使结果更容易理解,让我们把我们的SVG包裹在一个<button> 元素中,并在该层处理点击。

添加和删除该类的过程将由一个简单的JavaScript片段来处理。

const hamburger = document.querySelector("button");
hamburger.addEventListener("click", () => {
  hamburger.classList.toggle("is-opened");
});

为了创建我们的X,我们可以将不同的transform 属性应用到我们的汉堡包栏。因为新的transform 属性将覆盖旧的属性,我们的起点将是三个条的原始共享位置。

从那里,我们可以将顶部的条形图围绕其中心顺时针旋转45度,并将底部的条形图逆时针旋转45度。我们可以水平收缩中间的横条,直到它窄到可以隐藏在X的中心后面。

.is-opened .hamburger__bar--top {
  transform: rotate(45deg);
}
.is-opened .hamburger__bar--mid {
  transform: scaleX(0.1);
}
.is-opened .hamburger__bar--bot {
  transform: rotate(-45deg);
}

默认情况下,SVG元素的transform-origin 属性通常为0,0 。这意味着我们的条形图将围绕视口的左上角旋转,但我们希望它们能围绕中心旋转。为了解决这个问题,让我们把transform-origin 属性设置为center ,用于.hamburger__bar 类。

用以下方法对CSS属性进行动画处理transition

transition CSS属性告诉浏览器在CSS属性的两个不同状态之间平滑过渡。在这里,我们要对transform 属性的变化进行动画处理,它决定了条形图的位置、方向和比例。

我们还可以用transition-duration 属性来控制过渡的持续时间。为了使动画看起来更敏捷,我们将设置一个0.3秒的短持续时间。

.hamburger__bar {
  transition-property: transform;
  transition-duration: 0.3s;
  ...
}

我们唯一需要的JavaScript是使图标状态可切换的部分。

const hamburger = document.querySelector("button");
hamburger.addEventListener("click", () => {
  hamburger.classList.toggle("is-opened");
});

在这里,我们使用querySelector() ,通过它的.mute 类选择外部SVG元素。然后我们添加一个点击事件监听器。当点击事件被触发时,我们只在<svg> 本身上切换.is-active 类,而不在层次结构的更深处。因为我们使CSS动画只适用于具有.is-active 类的元素,切换这个类将激活和关闭动画。

作为最后一步,我们将把HTML主体转换为柔性容器,这将帮助我们把图标在水平和垂直方向上居中。我们还将把背景颜色更新为非常深的灰色,把图标颜色更新为白色,以达到一个光滑的 "黑暗模式 "的外观和感觉。

body {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #222;
  height: 100vh;
}

就这样,我们用一些基本的CSS和一个简短的JavaScript片段建立了一个全功能的动画按钮。要改变我们所应用的变换来制作各种动画是很容易的。读者可以简单地使用CodePen--其中包括一些额外的CSS来进行修饰--来发挥创意。

使用来自外部编辑器的SVG数据

我们的汉堡包菜单是非常简单的。如果我们想做一些更复杂的东西呢?这时,手工编码SVG就变得很困难了,而矢量图形编辑软件可以帮助我们。

我们的第二个SVG动画是一个静音按钮,显示一个耳机图标。当音乐处于激活状态时,图标会跳动和舞动;当音乐被静音时,图标会被划掉。

绘制图标将超出本教程的范围(可能也超出了你的工作范围),所以我们将从一个预先绘制的SVG图标开始。我们还想采用与我们的汉堡包菜单例子相同的body 风格。

在使用SVG之前,你可能想清理一下它的代码。你可以用svgo来做,这是一个开源的、基于Node.js的SVG优化工具。这将删除不必要的元素,使代码更容易手工编辑,你需要这样做,以便添加类和组合不同的元素。

在图像编辑软件中创建的SVG图标不太可能使用相对单位。此外,我们想确保图标的长宽比得到保持,而不管包含它的SVG元素的长宽比是多少。为了使这种程度的控制成为可能,我们将使用viewBox 属性。

调整SVG的大小是个好主意,这样viewBox ,可以设置成一些容易使用的值。在这种情况下,我把它转换为一个100×100像素的viewBox

让我们确保图标居中且大小合适。我们将把mute 类应用于我们的基础SVG元素,然后添加以下CSS样式。

.mute {
  fill: white;
  width: 80px;
  height: 70px;
  cursor: pointer;
}

在这里,width 要比height 稍微大一些,以避免在我们的动画旋转过程中出现剪切。

我们的SVG动画起点

现在干净的SVG包含一个单一的<g> 元素,其中包含三个<path> 元素。

路径元素允许我们绘制直线、曲线和弧线。路径是用一系列的命令来描述形状的绘制方式的。由于我们的图标由三个不相连的形状组成,我们有三个路径来描述它们。

gSVG元素是一个用于分组其他SVG元素的容器。我们用它来同时对所有三条路径进行脉动和跳舞的变换。

<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>

要让耳机脉动和跳舞,transition 是不够的。这是一个复杂到需要关键帧的例子。

在这个例子中,我们的开始和结束关键帧(分别在动画的0%和100%)使用了一个略微缩小的耳机图标。在动画的前40%时间里,我们将图像略微放大,并将其倾斜5度。然后,在接下来的40%的动画中,我们将其缩小到0.9倍,并将其向另一侧旋转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);
  }
}

CSS动画的优化

为了展示关键帧的工作原理,我们让我们的关键帧CSS比它需要的更冗长。我们有三种方法可以缩短它。

由于我们的100% 关键帧设置了整个transform 列表,如果我们完全省略rotate() ,其值将默认为0。

  100% {
    transform: scale(0.9);
  }

其次,我们知道我们希望我们的0%100% 关键帧相匹配,因为我们正在循环播放动画。通过用相同的CSS规则来定义它们,如果我们想改变动画循环中的这个共享点,我们就不必记得修改它们两个。

  0%, 100% {
    transform: scale(0.9);
  }

mute__headphones 最后,我们很快就会将transform: scale(0.9); ,当我们这样做时,我们根本不需要定义开始和结束的关键帧!它们将默认为静态样式。它们将默认为mute__headphones 所使用的静态样式。

现在我们已经定义了动画关键帧,我们可以应用这个动画了。我们将.mute__headphones 类添加到<g> 元素中,这样它就会影响耳机图标的所有三个部分。首先,我们再次将transform-origin 设置为center ,因为我们希望图标围绕中心旋转。我们还缩放它,使其大小与初始动画关键帧相匹配。如果没有这一步,从静态的 "静音 "图标切换到动画的图标,总是会导致尺寸的突然跳跃。(不管怎样,如果用户在比例大于0.9倍的时候点击,切换回静音的时候也会导致比例的跳跃,而且可能还会导致旋转。我们不能仅用CSS来解决这个问题)。

我们使用animation CSS属性来应用动画,但只有当.is-active 父类存在时,类似于我们对汉堡包菜单的动画。

.mute__headphones {
  transform-origin: center;
  transform: scale(0.9);
}
.is-active .mute__headphones {
  animation: pulse 2s infinite;
}

我们需要的让我们在不同状态间切换的JavaScript也遵循汉堡包菜单的相同模式。

const muteButton = document.querySelector(".mute");
muteButton.addEventListener("click", () => {
  muteButton.classList.toggle("is-active");
});

我们要添加的下一项内容是一条删除线,当图标处于非活动状态时出现。由于这是一个简单的设计元素,我们可以手动编码。这就是拥有简单合理的viewBox 值的用处。我们知道画布的边缘在0和100,所以很容易计算出线条的开始和结束位置。

<line x1="12" y1="12" x2="88" y2="88" class="mute__strikethrough" />

调整大小与使用相对单位

使用相对单位而不是调整图像的大小是有道理的。这适用于我们的例子,因为我们只是在我们的图标上添加了一个简单的SVG线条。

在现实世界中,你可能想把来自几个不同来源的更复杂的SVG内容结合起来。这时,让它们都具有统一的尺寸就很有用了,因为我们不能像在我们的例子中那样手动地硬编码相对值。

因为我们直接给我们的删除线<line> 元素应用了一个类,所以我们可以通过CSS给它设置样式。我们只需要确保当图标被激活时,这一行是不可见的。

.mute__strikethrough {
  stroke: red;
  opacity: 0.8;
  stroke-width: 12px;
}
.is-active .mute__strikethrough {
  opacity: 0;
}

作为选择,我们可以直接在SVG中添加.is-active 类。这将使动画在页面加载时立即开始,所以我们有效地将图标的初始状态从非动画(静音)变为动画(非静音)。

基于CSS的SVG动画将继续存在

我们只是触及了CSS动画技术视口工作原理的表面。了解如何手工编写SVG代码以保持简单的动画是值得的,但了解如何以及何时利用外部编辑器创建的图形也很重要。虽然现代浏览器使我们能够仅使用内置功能创建令人印象深刻的动画,但对于(非常)复杂的用例,开发者可能想探索像GSAPanime.js这样的动画库。

动画不一定要保留给奢侈的项目。现代CSS动画技术使我们能够以一种简单的、跨浏览器兼容的方式创建大量吸引人的、有光泽的动画。