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

news/2024/11/9 3:37:03/

动画是网络中不可或缺的一部分。与互联网早期使用 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 属性允许我们定义属性变化的速率和持续时间。

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

例如,下面这个例子,当你用鼠标悬停在 SVG 圆圈上时,它的颜色会发生变化,而不是立即从起始值跳到结束值。

jcode

<svg viewBox="0 0 300 200"><circle cx="150" cy="100" r="60" class="spot" />
</svg>
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,我们可以有多个关键帧和一个无限循环。例如下面这个例子:

jcode

<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>
@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动画的工作原理有了基本的了解。我们可以开始构建一个菜单切换的动画:

jcode

我们发现这个菜单能够巧妙地吸引了用户的注意力,告诉用户可以使用图标关闭菜单。

接下来我们来一起解析具体的代码。

首先我们创建一个 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 类,应用了一些基本样式。

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

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

如果我们现在渲染,我们会看到所有三条线都有相同的大小和位置,彼此完全重叠。不幸的是,我们不能通过 CSS 独立地改变开始和结束的位置。但是我们可以使用 CSS transform 属性移动整个元素的顶部和底部的条:

.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代码就是使图标状态变成可切换的:

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 容器,这将帮助我们在水平和垂直方向上居中图标:

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

这样,我们就使用一些基本的 CSS 和一个简短的 JavaScript 片段构建了一个功能齐全的动画按钮。

使用来自矢量图形编辑器的 SVG 数据

前面我们一起实现的汉堡菜单非常简单。但是如果我们想做更复杂的东西呢?
这就是 SVG 变得困难的地方,这个时候需要借助矢量图形编辑软件。

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

jcode

<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>
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);}
}
const muteButton = document.querySelector(".mute");
muteButton.addEventListener("click", () => {muteButton.classList.toggle("is-active");
});

在 svg 元素中,我们使用了来自矢量图形编辑软件的图形信息对耳机进行了绘制。

不过,在矢量图像编辑软件中创建的 SVG 图标不太可能使用相对单位。无论包含图标的SVG 元素的宽高比如何,我们都希望确保图标的宽高比得到维护。因此,为了使这种级别的控制成为可能,我们将使用 viewBox 属性。

在本例中,我将其转换为 100 x 100 像素的 viewBox。

让我们确保图标居中并且大小合适。我们将静音类应用到基本SVG元素,然后添加以下CSS样式:

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

SVG 动画的起点

接着上面一节,现在整洁的 SVG 包含一个 <g> 元素,该元素包含三个 <path> 元素。

path 元素允许我们绘制直线、曲线和圆弧。路径用一系列命令来描述,这些命令描述了应该如何绘制形状。由于我们的图标由三个互不相连的形状组成,我们有三条路径来描述它们。

同时在三条路径上应用脉动和舞蹈转换,而不是用 CSS 分别为 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>

为了让耳机跳动和跳舞,过渡是不够的,需要使用到关键帧动画。

在这种情况下,我们的开始和结束关键帧(分别为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 规则定义它们:

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也遵循与汉堡菜单相同的方式:

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

最后一部分,我们将添加的是当图标处于非 active 状态时出现的划线。由于这是一个简单的svg 元素,我们可以手动绘制它。我们知道画布的边缘是 0 和 100,所以很容易计算出线的开始和结束位置:

<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 这样的动画库实现更复杂的动画。

本次的文章就写到这里,关注我,下篇见。


http://www.ppmy.cn/news/1031457.html

相关文章

沁恒ch32V208处理器开发(五)复用与重映射

目录 概述实现方法GPIO 及其复用功能AFIO 的寄存器实现 重映射的功能限制 概述 在ch32V2xx系列处理器中&#xff0c;芯片引脚数从28到68不等&#xff0c;分为几个系列&#xff0c;不同型号之间外设资源有差异&#xff0c;所以&#xff0c;引脚复用和重映射功能 也不一样&#…

代码随想录算法训练营第45天|动态规划part07

8.11 周五 70. 爬楼梯 &#xff08;进阶&#xff09; 322. 零钱兑换 279.完全平方数 详细布置 70. 爬楼梯 &#xff08;进阶&#xff09; 题目&#xff1a;一次可爬1或2个台阶&#xff0c;问n个台阶有多少种方式 题解&#xff1a; 1、转换为完全背包问题&#xff0c;nums[1…

【Math】线性方程组的最小二乘解

在SLAM问题中&#xff0c;经常需要解一些方程数远大于未知量的超定线性方程组 A x 0 Ax0 Ax0或 A x b Axb Axb&#xff0c;那么这种情况下&#xff0c;从解析解的角度上看&#xff0c;是无解的&#xff0c;但是我们又需要给出一个答案&#xff0c;此时求解的就是最小二乘解。…

Python 基础 -- Tutorial(二)

5、数据结构 本章更详细地描述了一些你已经学过的东西&#xff0c;并添加了一些新的东西。 5.1. 更多关于Lists 列表(list)数据类型有更多的方法。下面是列表对象的所有方法: list.append(x) 在列表末尾添加一项。相当于a[len(a):] [x]。 list.extend(iterable) 通过添加可…

报错 | Spring报错详解

Spring报错详解 一、前言二、报错提示三、分层解读1.最下面一层Caused by2.上一层Caused by3.最上层Caused by 四、总结五、解决方案 一、前言 本文主要是记录在初次学习Spring时遇到报错后的解读以及解决方案 二、报错提示 三、分层解读 遇到报错的时候&#xff0c;我们需要…

【云原生】K8S存储卷:PV、PVC详解

目录 一、emptyDir存储卷二、hostPath存储卷三、nfs共享存储卷四、PVC 和 PV4.1 NFS使用PV和PVC4.2创建动态PV 一、emptyDir存储卷 容器磁盘上的文件的生命周期是短暂的&#xff0c;这就使得在容器中运行重要应用时会出现一些问题。首先&#xff0c;当容器崩溃时&#xff0c;ku…

剑指offer11-20

文章目录 11.旋转数组的最小数字12.矩阵中的路径13.机器人的运动范围15.二进制中1的个数16.数值的整数次方17.打印从1到最大的n位数&#xff08;待写&#xff09;18.删除链表的节点19.正则表达式匹配&#xff08;好难&#xff09;20. 没意义算了 11.旋转数组的最小数字 肯定不是…

Python 程序设计入门(018)—— format() 函数的用法详解

Python 程序设计入门&#xff08;018&#xff09;—— format() 函数的用法详解 目录 Python 程序设计入门&#xff08;018&#xff09;—— format() 函数的用法详解一、format() 函数的基本格式二、不提供 format_spec 参数三、设置字符串的对齐方式&#xff08;align&#x…