CSS 动画介绍及语法
首先,我们来简单介绍一下 CSS 动画。
最新版本的 CSS 动画由规范 -- CSS Animations Level 1[1] 定义。
CSS 动画用于实现元素从一个 CSS 样式配置转换到另一个 CSS 样式配置。
动画包括两个部分: 描述动画的样式规则和用于指定动画开始、结束以及中间点样式的关键帧。
简单来说,看下面的例子:
-
div {
-
animation: change 3s;
-
}
-
@keyframes change {
-
0% {
-
color: #f00;
-
}
-
100% {
-
color: #000;
-
}
-
}
-
animation: move 1s
部分就是动画的第一部分,用于描述动画的各个规则; -
@keyframes move {}
部分就是动画的第二部分,用于指定动画开始、结束以及中间点样式的关键帧;
一个 CSS 动画一定要由上述两部分组成。
CSS 动画的语法
接下来,我们简单看看 CSS 动画的语法。
创建动画序列,需要使用 animation 属性或其子属性,该属性允许配置动画时间、时长以及其他动画细节,但该属性不能配置动画的实际表现,动画的实际表现是由 @keyframes 规则实现。
animation 的子属性有:
-
animation-name:指定由 @keyframes 描述的关键帧名称。
-
animation-duration:设置动画一个周期的时长。
-
animation-delay:设置延时,即从元素加载完成之后到动画序列开始执行的这段时间。
-
animation-direction:设置动画在每次运行完后是反向运行还是重新回到开始位置重复运行。
-
animation-iteration-count:设置动画重复次数, 可以指定 infinite 无限次重复动画
-
animation-play-state:允许暂停和恢复动画。
-
animation-timing-function:设置动画速度, 即通过建立加速度曲线,设置动画在关键帧之间是如何变化。
-
animation-fill-mode:指定动画执行前后如何为目标元素应用样式
-
@keyframes 规则,当然,一个动画想要运行,还应该包括 @keyframes 规则,在内部设定动画关键帧
其中,对于一个动画:
-
必须项:
animation-name
、animation-duration
和@keyframes
规则 -
非必须项:
animation-delay
、animation-direction
、animation-iteration-count
、animation-play-state
、animation-timing-function
、animation-fill-mode
,当然不是说它们不重要,只是不设置时,它们都有默认值
上面已经给了一个简单的 DEMO, 就用上述的 DEMO,看看结果:
这就是一个最基本的 CSS 动画,本文将从 animation 的各个子属性入手,探究 CSS 动画的方方面面。
animation-name / animation-duration 详解
整体而言,单个的 animation-name
和 animation-duration
没有太多的技巧,非常好理解,放在一起。
首先介绍一下 animation-name
,通过 animation-name
,CSS 引擎将会找到对应的 @keyframes 规则。
当然,它和 CSS 规则命名一样,也存在一些骚操作。譬如,他是支持 emoji 表情的,所以代码中的 animation-name
命名也可以这样写:
-
div {
-
animation: 😄 3s;
-
}
-
@keyframes 😄 {
-
0% {
-
color: #f00;
-
}
-
100% {
-
color: #000;
-
}
-
}
而 animation-duration
设置动画一个周期的时长,上述 DEMO 中,就是设定动画整体持续 3s
,这个也非常好理解。
animation-delay 详解
animation-delay
就比较有意思了,它可以设置动画延时,即从元素加载完成之后到动画序列开始执行的这段时间。
简单的一个 DEMO:
-
<div></div>
-
<div></div>
-
div {
-
width: 100px;
-
height: 100px;
-
background: #000;
-
animation-name: move;
-
animation-duration: 2s;
-
}
-
div:nth-child(2) {
-
animation-delay: 1s;
-
}
-
@keyframes move {
-
0% {
-
transform: translate(0);
-
}
-
100% {
-
transform: translate(200px);
-
}
-
}
比较下列两个动画,一个添加了 animation-delay
,一个没有,非常直观:
上述第二个 div,关于 animation
属性,也可以简写为 animation: move 2s 1s
,第一个时间值表示持续时间,第二个时间值表示延迟时间。
animation-delay 可以为负值
关于 animation-delay
,最有意思的技巧在于,它可以是负数。也就是说,虽然属性名是动画延迟时间,但是运用了负数之后,动画可以提前进行。
假设我们要实现这样一个 loading 动画效果:
有几种思路:
-
初始 3 个球的位置就是间隔 120°,同时开始旋转,但是这样代码量会稍微多一点
-
另外一种思路,同一个动画,3 个元素的其中两个延迟整个动画的 1/3,2/3 时间出发
方案 2 的核心伪代码如下:
-
.item:nth-child(1) {
-
animation: rotate 3s infinite linear;
-
}
-
.item:nth-child(2) {
-
animation: rotate 3s infinite 1s linear;
-
}
-
.item:nth-child(3) {
-
animation: rotate 3s infinite 2s linear;
-
}
但是,在动画的前 2s,另外两个元素是不会动的,只有 2s 过后,整个动画才是我们想要的:
此时,我们可以让第 2、3 个元素的延迟时间,改为负值,这样可以让动画延迟进行 -1s
、-2s
,也就是提前进行 1s
、2s
:
-
.item:nth-child(1) {
-
animation: rotate 3s infinite linear;
-
}
-
.item:nth-child(2) {
-
animation: rotate 3s infinite -1s linear;
-
}
-
.item:nth-child(3) {
-
animation: rotate 3s infinite -2s linear;
-
}
这样,每个元素都无需等待,直接就是运动状态中的,并且元素间隔位置是我们想要的结果:
利用 animation-duration 和 animation-delay 构建随机效果
还有一个有意思的小技巧。
同一个动画,我们利用一定范围内随机的 animation-duration
和一定范围内随机的 animation-delay
,可以有效的构建更为随机的动画效果,让动画更加的自然。
我在下述两个纯 CSS 动画中,都使用了这样的技巧:
-
纯 CSS 实现华为充电动画[2]:
纯 CSS 实现华为充电动画
-
纯 CSS 实现火焰动画[3]:
纯 CSS 实现火焰动画
以纯 CSS 实现华为充电动画为例子,简单讲解一下。
仔细观察这一部分,上升的一个一个圆球,抛去这里的一些融合效果,只关注不断上升的圆球,看着像是没有什么规律可言:
我们来模拟一下,如果是使用 10 个 animation-duration
和 animation-delay
都一致的圆的话,核心伪代码:
-
<ul>
-
<li></li>
-
<!--共 10 个...-->
-
<li></li>
-
</ul>
-
ul {
-
display: flex;
-
flex-wrap: nowrap;
-
gap: 5px;
-
}
-
li {
-
background: #000;
-
animation: move 3s infinite 1s linear;
-
}
-
@keyframes move {
-
0% {
-
transform: translate(0, 0);
-
}
-
100% {
-
transform: translate(0, -100px);
-
}
-
}
这样,小球的运动会是这样的整齐划一:
要让小球的运动显得非常的随机,只需要让 animation-duration
和 animation-delay
都在一定范围内浮动即可,改造下 CSS:
-
@for $i from 1 to 11 {
-
li:nth-child(#{$i}) {
-
animation-duration: #{random(2000)/1000 + 2}s;
-
animation-delay: #{random(1000)/1000 + 1}s;
-
}
-
}
我们利用 SASS 的循环和 random()
函数,让 animation-duration
在 2-4 秒范围内随机,让 animation-delay
在 1-2 秒范围内随机,这样,我们就可以得到非常自然且不同的上升动画效果,基本不会出现重复的画面,很好的模拟了随机效果:
CodePen Demo -- 利用范围随机 animation-duration 和 animation-delay 实现随机动画效果[4]
animation-timing-function 缓动函数
缓动函数在动画中非常重要,它定义了动画在每一动画周期中执行的节奏。
缓动主要分为两类:
-
cubic-bezier-timing-function 三次贝塞尔曲线缓动函数
-
step-timing-function 步骤缓动函数(这个翻译是我自己翻的,可能有点奇怪)
三次贝塞尔曲线缓动函数
首先先看看三次贝塞尔曲线缓动函数。在 CSS 中,支持一些缓动函数关键字。
-
/* Keyword values */
-
animation-timing-function: ease; // 动画以低速开始,然后加快,在结束前变慢
-
animation-timing-function: ease-in; // 动画以低速开始
-
animation-timing-function: ease-out; // 动画以低速结束
-
animation-timing-function: ease-in-out; // 动画以低速开始和结束
-
animation-timing-function: linear; // 匀速,动画从头到尾的速度是相同的
关于它们之间的效果对比:
除了 CSS 支持的这 5 个关键字,我们还可以使用 cubic-bezier()
方法自定义三次贝塞尔曲线:
animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);
这里有个非常好用的网站 -- cubic-bezier[5] 用于创建和调试生成不同的贝塞尔曲线参数。
三次贝塞尔曲线缓动对动画的影响
关于缓动函数对动画的影响,这里有一个非常好的示例。这里我们使用了纯 CSS 实现了一个钟的效果,对于其中的动画的运动,如果是 animation-timing-function: linear
,效果如下:
b
而如果我们我把缓动函数替换一下,变成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29)
,它的曲线对应如下:
整个钟的动画律动效果将变成这样,完全不一样的感觉:
CodePen Demo - 缓动不同效果不同[6]
对于许多精益求精的动画,在设计中其实都考虑到了缓动函数。我很久之前看到过一篇《基于物理学的动画用户体验设计》,可惜如今已经无法找到原文。其中传达出的一些概念是,动画的设计依据实际在生活中的表现去考量。
譬如 linear 这个缓动,实际应用于某些动画中会显得很不自然,因为由于空气阻力的存在,程序模拟的匀速直线运动在现实生活中是很难实现的。因此对于这样一个用户平时很少感知到的运动是很难建立信任感的。这样的匀速直线运动也是我们在进行动效设计时需要极力避免的。
步骤缓动函数
接下来再讲讲步骤缓动函数。在 CSS 的 animation-timing-function
中,它有如下几种表现形态:
-
{
-
/* Keyword values */
-
animation-timing-function: step-start;
-
animation-timing-function: step-end;
-
/* Function values */
-
animation-timing-function: steps(6, start)
-
animation-timing-function: steps(4, end);
-
}
在 CSS 中,使用步骤缓动函数最多的,就是利用其来实现逐帧动画。假设我们有这样一张图(图片大小为 1536 x 256
,图片来源于网络):
可以发现它其实是一个人物行进过程中的 6 种状态,或者可以为 6 帧,我们利用 animation-timing-function: steps(6)
可以将其用一个 CSS 动画串联起来,代码非常的简单:
<div class="box"></div>
-
.box {
-
width: 256px;
-
height: 256px;
-
background: url('https://github.com/iamalperen/playground/blob/main/SpriteSheetAnimation/sprite.png?raw=true');
-
animation: sprite .6s steps(6, end) infinite;
-
}
-
@keyframes sprite {
-
0% {
-
background-position: 0 0;
-
}
-
100% {
-
background-position: -1536px 0;
-
}
-
}
简单解释一下上述代码,首先要知道,刚好 256 x 6 = 1536
,所以上述图片其实可以刚好均分为 6 段:
-
我们设定了一个大小都为
256px
的 div,给这个 div 赋予了一个animation: sprite .6s steps(6) infinite
动画; -
其中
steps(6)
的意思就是将设定的 @keyframes 动画分为 6 次(6帧)执行,而整体的动画时间是0.6s
,所以每一帧的停顿时长为0.1s
; -
动画效果是由
background-position: 0 0
到background-position: -1536px 0
,由于上述的 CSS 代码没有设置background-repeat
,所以其实background-position: 0 0
是等价于background-position: -1536px 0
,就是图片在整个动画过程中推进了一轮,只不过每一帧停在了特点的地方,一共 6 帧;
将上述 1、2、3,3 个步骤画在图上简单示意:
从上图可知,其实在动画过程中,background-position
的取值其实只有 background-position: 0 0
,background-position: -256px 0
,background-position: -512px 0
依次类推一直到 background-position: -1536px 0
,由于背景的 repeat 的特性,其实刚好回到原点,由此又重新开始新一轮同样的动画。
所以,整个动画就会是这样,每一帧停留 0.1s 后切换到下一帧(注意这里是个无限循环动画),:
完整的代码你可以戳这里 -- CodePen Demo -- Sprite Animation with steps() [7]
animation-duration 动画长短对动画的影响
在这里再插入一个小章节,animation-duration
动画长短对动画的影响也是非常明显的。
在上述代码的基础上,我们再修改 animation-duration
,缩短每一帧的时间就可以让步行的效果变成跑步的效果,同理,也可以增加每一帧的停留时间。让每一步变得缓慢,就像是在步行一样。
需要提出的是,上文说的每一帧,和浏览器渲染过程中的 FPS 的每一帧不是同一个概念。
看看效果,设置不同的 animation-duration
的效果(这里是 0.6s -> 0.2s),GIF 录屏丢失了一些关键帧,实际效果会更好点:
当然,在 steps()
中,还有 steps(6, start)
和 steps(6, end)
的差异,也就是其中关键字 start
和 end
的差异。对于上述的无限动画而言,其实基本是可以忽略不计的,它主要是控制动画第一帧的开始和持续时长,比较小的一个知识点但是想讲明白需要比较长的篇幅,限于本文的内容,在这里不做展开,读者可以自行了解。
同个动画效果的补间动画和逐帧动画演绎对比
上述的三次贝塞尔曲线缓动和步骤缓动,其实就是对应的补间动画和逐帧动画。
对于同个动画而言,有的时候两种缓动都是适用的。我们在具体使用的时候需要具体分析选取。
假设我们用 CSS 实现了这样一个图形:
现在想利用这个图形制作一个 Loading 效果,如果利用补间动画,也就是三次贝塞尔曲线缓动的话,让它旋转起来,得到的效果非常的一般:
-
.g-container{
-
animation: rotate 2s linear infinite;
-
}
-
@keyframes rotate {
-
0% {
-
transform: rotate(0);
-
}
-
100% {
-
transform: rotate(360deg);
-
}
-
}
动画效果如下:
但是如果这里,我们将补间动画换成逐帧动画,因为有 20 个点,所以设置成 steps(20),再看看效果,会得到完全不一样的感觉:
-
.g-container{
-
animation: rotate 2s steps(20) infinite;
-
}
-
@keyframes rotate {
-
0% {
-
transform: rotate(0);
-
}
-
100% {
-
transform: rotate(360deg);
-
}
-
}
动画效果如下:
整个 loading 的圈圈看上去好像也在旋转,实际上只是 20 帧关键帧在切换,整体的效果感觉更适合 Loading 的效果。
因此,两种动画效果都是很有必要掌握的,在实际使用的时候灵活尝试,选择更适合的。
上述 DEMO 效果完整的代码:CodePen Demo -- Scale Loading steps vs linear[8]
animation-play-state
接下来,我们讲讲 animation-play-state
,顾名思义,它可以控制动画的状态 -- 运行或者暂停。类似于视频播放器的开始和暂停。是 CSS 动画中有限的控制动画状态的手段之一。
它的取值只有两个(默认为 running):
-
{
-
animation-play-state: paused | running;
-
}
使用起来也非常简单,看下面这个例子,我们在 hover 按钮的时候,实现动画的暂停:
-
<div class="btn stop">stop</div>
-
<div class="animation"></div>
-
.animation {
-
width: 100px;
-
height: 100px;
-
background: deeppink;
-
animation: move 2s linear infinite alternate;
-
}
-
@keyframes move {
-
100% {
-
transform: translate(100px, 0);
-
}
-
}
-
.stop:hover ~ .animation {
-
animation-play-state: paused;
-
}
一个简单的 CSS 动画,但是当我们 hover 按钮的时候,给动画元素添加上 animation-play-state: paused
:
animation-play-state 小技巧,默认暂停,点击运行
正常而言,按照正常思路使用 animation-play-state: paused
是非常简单的。