我们使用 transition 已经可以实现动画效果,但其有一定局限性,例如一个很简单的需求,我要让一个绝对定位的盒子打开网页后从左挪到右,再从右挪到左,这样循环往复运动,如果使用 transition,则会在 left 发生改变的时候自动添加动画,但这里的问题是我就必须得有一个东西去触发这个改变,比如可以设置 CSS 的 hover 伪类,但是必须鼠标悬停,比如也可以使用 JS 的定时器来操纵,每隔几秒修改一下 left 属性值,但这其实都不理想。从这个例子也不难发现,虽然 transition 可以实现一些动画效果,但本质上不过也是在状态之间自动添加一些过渡的动画,哪怕添加了过渡动画,如果其属性不发生改变也无法生效,而要让其属性发生改变本身就有一定的触发条件,比如鼠标悬浮,比如 JS 控制,它并不能实现类似全自动的动画效果,比如我就是要让一个盒子跑来跑去,这就无法实现。这就得引入 CSS 动画。
CSS 3 的动画并不是万能的,只能实现一些基础效果,不过大多数时候也够用,如果真的要实现一些特别复杂的动画可能需要引入 JS 。
CSS 提供了 animation 属性就是专门来干动画的,这个 animation 属性和 transition 属性有很多类似的地方,该属性是以下属性的复合属性
- animation-name
- animation-duration,
- animation-timing-function
- animation-delay
- animation-iteration-count
- animation-direction
- animation-fill-mode
- animation-play-state
这八个属性中,animation-duration,animation-delay ,animation-timing-function 这三个可以参考 12.transition,这里只做简要介绍,animation-duration 表示动画执行的总时长,或者说就是一状态变到另一个状态的总时长。animation-delay 表示动画延迟,有时候我并不希望立马执行动画,希望触发事件之后等一会儿再执行,这个属性就可以设置延迟多少时间之后再进行动画。animation-timing-function 属性就是设置动画过程中的速度变化方式,或者也可以说是运动状态,比如可以设置匀速的动画,可以设置先快后慢的动画。以上三个属性由于在过渡部分介绍过类似属性,所以这里并不详细展开,只介绍一些细节,主要介绍没见过的属性。
首先,你要使用动画,必须得先有动画吧,所以我们在使用动画之前必须先定义一个动画,定义动画的方式就是 @keyframes 规则。
at 规则:以
@开头的 CSS 规则都属于 at 规则,这个东西在前面我们见到过,就是@font-face,一般的 CSS 规则都需要选择器,将属性直接作用到元素上,但是 at 规则则不是,比如@font-face就是用来定义一个字体,@keyframes就是用来定义一个关键帧动画,比如@charset就是用来指定字符编码。显然,这些规则并没有选择器,并且从含以上也都不是直接作用到某一个元素上的东西,是区别于 CSS 规则的比较特别的规则。
我们使用以下语法来定义关键帧动画
css
@keyframes run{/*run是动画名*/
0%{/*注意百分号和大括号之间没有其他符号*/
/*CSS属性*/
}/*注意这里每一个块中间没有分号*/
/*中间可以自己填,如下
10%{
CSS属性
}
填多少都可以
*/
100%{
/*CSS属性*/
}
/*关键帧选择器可以并列去写*/
/*
0%,100%{//这里定义了两个关键帧
//CSS属性
}
*/
}参考以下代码:
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
@keyframes run {
0% {
left: 0;
top: 0;
}
25% {
left: 100px;
top: 100px;
}
50% {
left: 50px;
top: 150px;
}
75% {
left: 300px;
top: 300px;
}
100% {
left: 0px;
top: 0px;
}
}
* {
margin: 0;
padding: 0;
}
.test {
animation: run 4s linear;
position: absolute;
height: 100px;
width: 100px;
background-color: aqua;
}
</style>
</head>
<body>
<div class="test">
</div>
</body>
</html>在以上代码中,定义了一个名为 run 的关键帧动画,动画里定义了五个关键帧,然后在这个盒子上使用了这个动画,这个动画一共会在四秒钟内完成,这里的 linear 表示线性动画,也就是每两个状态之间的转变是线性的,或者说匀速的,效果如下图所示。

关键帧动画其实就是一个一个关键帧的切换,有点类似于什么呢,给这个盒子加上过渡,然后按照时间依次切换这些状态,直到 100%。 从代码中可以看到,我的要求在四秒钟内完成动画,并且定义了五个关键帧,从 0% 到 25%,一共走过了 25%,所以这两个状态变化会用到总时长的 25%,也就是 1 秒钟,后面的也同理,这个百分数其实就是用来分割执行时间的。
上面的 run,其实就是 animation-name,当我们给一个元素设置上 animation-name 和 animation-duration 之后,动画就可以直接开始执行,其他参数均为可选。
一般来说我们都使用百分比来定义关键帧,但
0%可以写成from,100%可以写成to,只有这两个关键帧是可以替换的,其余关键帧必须写成百分比,所以索性不如全写成百分比
我们还可以为一个元素添加多个关键帧动画,这些个关键帧动画会依次执行
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
@keyframes run2 {
0% {
background-color: aqua;
}
100% {
background-color: aquamarine;
}
}
@keyframes run {
0% {
left: 0;
top: 0;
}
25% {
left: 100px;
top: 100px;
}
50% {
left: 50px;
top: 150px;
}
75% {
left: 300px;
top: 300px;
}
100% {
left: 0px;
top: 0px;
}
}
* {
margin: 0;
padding: 0;
}
.test {
animation-name: run, run2;
animation-duration: 3s, 4s;
/*animation:run 3s,run2 4s; 也可以这样写*/
position: absolute;
height: 100px;
width: 100px;
background-color: aqua;
}
</style>
</head>
<body>
<div class="test">
</div>
</body>
</html>以上就添加了多个动画,可以看到,多个动画的时间并不需要一模一样,可以为多个动画定义不同的执行时间,也可以为多个动画划分不同的关键帧,通过一系列动画的叠加组合最终其实可以实现很多炫酷的效果。
让多个动画叠加使用,不应当为多个动画添加同样的属性值,比如动画 1 在改变
left属性,动画 2 还在改变left属性,由于这两个动画是并发的,或者说是叠加执行的,最后的结果往往并不是我们希望的效果。
接下来我们做这样一个测试。
css
.test {
animation-name: run, run2;
animation-duration: 10s, 10s;
}我给 .test 添加了一个动画

可以看到,当我把这个盒子的 test 类名去掉之后,动画会立刻终止,当我给这个盒子添加上 .test 之后,动画立刻重新开始。这里就希望读者可以理解动画的本质,CSS 动画本质上还是一个样式,只不过是可以动的样式,我添加上这个动画属性后,这个盒子就可以按照我的规则去运动,当我把这个属性删掉之后,就会停止运动回归原位。这其实也说明我们其实是可以使用 JS 通过设置类名的方式给元素添加一个 CSS 动画。
我们会为一个动画设置很多关键帧,可以设置一个运动状态,比如先快后慢这种,这种运动状态是作用到两个关键帧中间的,比如我有三个关键帧 k1, k2, k3,我设置该动画的运动状态为 ease-out,这就是一个先快后慢的动画,这实际上是设置每两个关键帧之间都是先快后慢的动画,即 k1 和 k2 之间的变化是先快后慢,k2 和 k3 中间的动画也是先快后慢。而不是说 k1 到 k3 这个整个动画都是先快后慢的。这里也体现出 CSS 3 动画的一点缺陷,如果我要设置 k1 和 k2 是先快后慢,k2 到 k3 是先慢后快,这个需求单纯的 CSS 3 动画就实现不了了,往往需要 JS 参与控制。
这种关键帧动画还有一个天然的局限性,就是无法实现复杂的曲线动画,比如我要实现一个抛物线,要想用纯 CSS 就必须要一帧一帧去写,这是很麻烦的,完全不现实。当然如果是旋转这种简单的曲线运动,可以通过
transform来实现,这个会在后面涉及到。
我们可以看到,默认情况下,动画只执行一次就结束了,如果我要多执行几次,就要用 animation-iteration-count 属性,这个属性规定了动画的执行次数,默认是 1,也就是执行一次,如果我要让其无限循环,就用 infinite 关键字即可。
animation-iteration-count可以为小数,比如 0.5 就是动半次
css
.test {
animation-name: run, run2;
animation-duration: 1s, 1s;
animation-iteration-count: infinite, 2;
}这里的 CSS 就是规定 run 动画无限循环,run 2动画只执行两次。
css
.test {
animation-name: run, run2;
animation-duration: 1s, 1s;
animation-iteration-count: infinite, 2;
animation-delay: 2s, 2s;
}这里可以配合上 delay 进行说明,这个属性可以让两个动画都延迟两秒再执行,注意,循环动画只有第一次才会延迟执行,第二次开始不再延迟。
在前面的动画里面,我们的动画都是从 0% 一直走到 100% 的,如果是循环,则会继续从 0% 一直走到 100%,我们也可以让它倒着走,也就是从 100% 逆向走回 0%,这就得使用 animation-direction 属性,这个 direction 表示方向的意思,在 flex 中的 flex-direction 表示的也是主轴方向,这里积累一个高频词汇。
这个值默认是 normal,也就是从 0% 一直到 100%,然后第二次也是从 0% 到 100%。如果设置成 reverse,则最开始会是 100% 的状态,然后从下往上依次执行到 0%。
如果将这个属性设为 alternate,第一次执行是 0% 到 100%,第二次执行是 100% 到 0%,即奇数次执行为正向,偶数次执行为逆向。由于涉及到多次,所以这个属性得在多次动画才有实际效果,也就是 animation-iteration-count 不能是 1。例如我设置一个从左往右的动画,让它无限执行,如果不设置 animation-direction,那么就是从左往右,执行完后盒子立刻到左边,再从左往右,这样执行,如果设置了 animation-direction:alternate;,那么就是第一次从左往右,第二次从右往左,第三次再从左往右,这样就是一个往复运动。
还有一个值是
alternate-reverse,这个值就是反过来,第一次是从100%到0%,第二次是0%到100%,如此往复。
为了理解 animation-direction,用下面的动画来演示
css
@keyframes run {
0% {
left: 0;
}
100% {
left: 100px;
}
}以上面的动画为例,是一个从左往右的变化。




alternate 和 alternate-reverse 的区别其实就是起始位置不一样,alternate 第一次正向的,alternate-reverse 第一次是反向的。
这个动画也可以使用 CSS 来暂停,animation-play-state 可以控制动画的状态,默认值是 running 也就是运行,所以动画默认是一直运行的,我也可以将他设置为 pause,如果一个元素的 animation-play-state 属性被设置为了 pause,那么它所有动画都会暂停,直到该属性重新变成 running,有了这个东西之后,我就可以实现鼠标悬浮让动画暂停的效果。
css
.test:hover {
animation-play-state: paused;
}
有了这个属性后,我们就可以使用 JS 在合适的时间节点暂停动画。
最后一个属性是 animation-fill-mode,这里先来看一个动画
css
@keyframes run {
0% {
left: 0;
background-color: aqua;
}
100% {
left: 100px;
background-color: chartreuse;
}
}这个动画会让盒子向左移动 100 像素,然后会让它变色。动画只执行一次,来看下效果。

这里就有一个问题,当我动画结束之后会立刻回到最开始的状态,如果我希望动画结束之后,停留在最后一帧,也就是不回去,就可以使用 animation-fill-mode,将这个属性设置为 forwards,就可以让动画结束后保持在最后一帧,也就是 100% 处的样式,如下所示:

这个属性还有一个值,就是 backwards,这个属性是设置动画开始之前,我们用这个例子来说明
css
@keyframes run {
0% {
left: 0;
background-color: chartreuse;
}
100% {
left: 100px;
background-color: chartreuse;
}
}此时我加上 animation-delay: 1s;,会看到以下效果。

在等待的这一秒钟的时间里,盒子的颜色还是原来的颜色,也就是青色,而我关键帧里 0% 处的颜色其实是绿色。如果我希望在等待的时间里按照 0% 那一帧的样式去等待,或者说在等待的时间里让它的样式变成第一帧动画的样式,就可以将 animation-fill-mode 设置为 backwards

此时在等待的这一秒钟里,就会按照第一帧动画的样式进行等待。
综上,animation-fill-mode 其实设置的是动画执行前和执行后的展示效果,设置为 backwards 可以让动画在延迟等待的时间里按照第一帧的样式去等待,而不按照其原始的样式。如果设置为 forwards 则会在动画执行之后保持在最后一帧的样式,而不回到自己的样式上去。
可见 backwards 和 forwards 一个设置动画前,一个设置动画后,如果我既要设置动画前又要设置动画后,就可以使用 both 关键字。
一个比较通用的理解就是,动画本身是一个过程,animation-delay 结束之后,动画就算开始,动画最后一帧结束之后,动画就算结束。animation-delay 所设置的延迟时间里,就可以看做是动画开始之前,而动画结束之后就是最后一帧结束之后。默认情况下,不管是动画开始之前,还是动画结束之后,元素使用的样式都是自身的样式,只有动画的过程中才会使用动画样式。而这个属性的作用就是用动画的第一帧的样式去替代元素原有的样式,也可以用动画最后一帧的样式去替换动画结束之后的样式。
一般使用这个属性就设置为
both即可
和 transition 一样,animation 在特定的时间节点也会触发一些事件,我们可以通过 JS 去监听它,可以用 JS 监听以下四个事件:
- animationstart:运动开始时触发该事件
- animationend:运动结束时触发该事件(如果是无限循环的动画,那么不会触发该事件)
- animationcancel:动画被取消的时候触发该事件,或者说动画被替换
- animationiteration:完成一次动画的时候触发(和 animation-iteration-count 相关)
练习
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
@keyframes space-change {
0% {
opacity: .3;
}
25% {
opacity: 1;
}
50% {
opacity: .3;
}
75% {
opacity: .1;
}
100% {
opacity: .3;
}
}
@keyframes sunRise {
0% {
opacity: 0;
}
10% {
opacity: 1;
transform: scale(.7, .7) translate(0, 0);
}
30% {
opacity: 1;
transform: scale(.5, .5) translate(0, -500px);
}
50% {
opacity: 0;
transform: scale(.7, .7) translate(400px, 0);
}
100% {
opacity: 0;
}
}
@keyframes moonRise {
0% {
opacity: 0;
transform: translate(0, 0);
}
30% {
transform: translate(0, 0);
}
50% {
opacity: 0;
}
70% {
transform: translate(0, -300px);
opacity: 1;
}
80% {
transform: translate(0, -300px);
opacity: 1;
}
90% {
opacity: 0;
}
100% {
transform: translate(0, 0);
opacity: 0;
}
}
body {
background-color: black;
}
.space {
height: 500px;
background-image: linear-gradient(to bottom, rgba(0, 130, 255, 1), white);
animation: space-change 10s cubic-bezier(.5, 0, .5, 1) infinite;
}
.sun {
position: absolute;
left: calc(50% - 25px);
top: calc(50% - 25px);
width: 50px;
height: 50px;
background-color: #fff;
border-radius: 50%;
box-shadow:
0px 0px 100px 50px #fff,
0px 0px 250px 125px #ff0;
animation: sunRise 10s infinite;
}
.moon {
position: absolute;
left: calc(50% + 400px);
top: calc(50% - 50px);
height: 100px;
width: 100px;
background-color: #fff;
border-radius: 50%;
animation: moonRise 10s ease infinite;
box-shadow: 0px 0px 16px #fff,
inset 0px 0px 8px black;
}
.moon::after {
content: "";
position: absolute;
width: 90px;
height: 90px;
background-color: #000;
border-radius: 50%;
left: -10px;
top: -10px;
}
</style>
</head>
<body>
<div class="space"></div>
<div class="sun"></div>
<div class="moon"></div>
</body>
</html>