CSS 动画使得无需任何 JavaScript 就能实现简单的动画。
JavaScript 可以用来控制 CSS 动画,并用很少的代码使它们变得更好。
CSS 过渡
CSS 过渡的概念很简单。我们描述一个属性及其变化应该如何动画化。当属性发生变化时,浏览器会绘制动画。
也就是说,我们只需要改变属性,浏览器就会完成流畅的过渡。
例如,下面的 CSS 代码将 background-color
的变化动画化,持续 3 秒。
.animated {
transition-property: background-color;
transition-duration: 3s;
}
现在,如果一个元素具有 .animated
类,那么 background-color
的任何变化都会在 3 秒内动画化。
点击下面的按钮来动画背景
<button id="color">Click me</button>
<style>
#color {
transition-property: background-color;
transition-duration: 3s;
}
</style>
<script>
color.onclick = function() {
this.style.backgroundColor = 'red';
};
</script>
有 4 个属性来描述 CSS 过渡
transition-property
transition-duration
transition-timing-function
transition-delay
我们稍后会介绍它们,现在让我们注意到,常见的 transition
属性允许以以下顺序将它们一起声明:property duration timing-function delay
,以及同时对多个属性进行动画处理。
例如,此按钮同时对 color
和 font-size
进行动画处理
<button id="growing">Click me</button>
<style>
#growing {
transition: font-size 3s, color 2s;
}
</style>
<script>
growing.onclick = function() {
this.style.fontSize = '36px';
this.style.color = 'red';
};
</script>
现在,让我们逐个介绍动画属性。
transition-property
在 transition-property
中,我们编写一个要进行动画处理的属性列表,例如:left
、margin-left
、height
、color
。或者我们可以写 all
,这意味着“对所有属性进行动画处理”。
请注意,有些属性无法进行动画处理。但是,大多数常用的属性都是可动画的。
transition-duration
在 transition-duration
中,我们可以指定动画应该持续多长时间。时间应该使用 CSS 时间格式:以秒 s
或毫秒 ms
为单位。
transition-delay
在 transition-delay
中,我们可以指定动画之前的延迟。例如,如果 transition-delay
为 1s
,transition-duration
为 2s
,则动画在属性更改后 1 秒开始,总持续时间为 2 秒。
负值也是可能的。然后动画会立即显示,但动画的起点将在给定值(时间)之后。例如,如果 transition-delay
为 -1s
,transition-duration
为 2s
,则动画从中途开始,总持续时间为 1 秒。
这里动画使用 CSS translate
属性将数字从 0
转换为 9
stripe.onclick = function() {
stripe.classList.add('animate');
};
#digit {
width: .5em;
overflow: hidden;
font: 32px monospace;
cursor: pointer;
}
#stripe {
display: inline-block
}
#stripe.animate {
transform: translate(-90%);
transition-property: transform;
transition-duration: 9s;
transition-timing-function: linear;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
Click below to animate:
<div id="digit"><div id="stripe">0123456789</div></div>
<script src="script.js"></script>
</body>
</html>
transform
属性的动画处理方式如下
#stripe.animate {
transform: translate(-90%);
transition-property: transform;
transition-duration: 9s;
}
在上面的示例中,JavaScript 将类 .animate
添加到元素中 - 然后动画开始
stripe.classList.add('animate');
我们也可以从过渡的中间某个位置开始,从一个确切的数字开始,例如对应于当前秒,使用负 transition-delay
。
这里,如果您点击数字 - 它将从当前秒开始动画
stripe.onclick = function() {
let sec = new Date().getSeconds() % 10;
stripe.style.transitionDelay = '-' + sec + 's';
stripe.classList.add('animate');
};
#digit {
width: .5em;
overflow: hidden;
font: 32px monospace;
cursor: pointer;
}
#stripe {
display: inline-block
}
#stripe.animate {
transform: translate(-90%);
transition-property: transform;
transition-duration: 9s;
transition-timing-function: linear;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
Click below to animate:
<div id="digit"><div id="stripe">0123456789</div></div>
<script src="script.js"></script>
</body>
</html>
JavaScript 使用额外的行来完成此操作
stripe.onclick = function() {
let sec = new Date().getSeconds() % 10;
// for instance, -3s here starts the animation from the 3rd second
stripe.style.transitionDelay = '-' + sec + 's';
stripe.classList.add('animate');
};
transition-timing-function
定时函数描述了动画过程如何在时间线上分布。它会先慢后快,还是反过来。
乍一看,它似乎是最复杂的属性。但是,如果我们花点时间研究它,它就会变得非常简单。
该属性接受两种类型的值:贝塞尔曲线或步骤。让我们从曲线开始,因为它使用得更多。
贝塞尔曲线
定时函数可以设置为 贝塞尔曲线,它有 4 个控制点,满足以下条件
- 第一个控制点:
(0,0)
。 - 最后一个控制点:
(1,1)
。 - 对于中间点,
x
的值必须在0..1
区间内,y
可以是任何值。
CSS 中贝塞尔曲线的语法:cubic-bezier(x2, y2, x3, y3)
。这里我们只需要指定第二和第三个控制点,因为第一个固定为 (0,0)
,第四个为 (1,1)
。
定时函数描述了动画过程的速度。
x
轴表示时间:0
– 开始,1
–transition-duration
的结束。y
轴指定了过程的完成度:0
– 属性的初始值,1
– 最终值。
最简单的变体是动画以相同的线性速度均匀进行。这可以通过曲线 cubic-bezier(0, 0, 1, 1)
指定。
以下是该曲线的形状
…正如我们所见,它只是一条直线。随着时间 (x
) 的推移,动画的完成度 (y
) 稳定地从 0
变为 1
。
以下示例中的火车以恒定速度从左向右移动(点击它)
.train {
position: relative;
cursor: pointer;
width: 177px;
height: 160px;
left: 0;
transition: left 5s cubic-bezier(0, 0, 1, 1);
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">
</body>
</html>
CSS transition
基于该曲线
.train {
left: 0;
transition: left 5s cubic-bezier(0, 0, 1, 1);
/* click on a train sets left to 450px, thus triggering the animation */
}
…我们如何展示一辆减速的火车?
我们可以使用另一个贝塞尔曲线:cubic-bezier(0.0, 0.5, 0.5 ,1.0)
。
图形
正如我们所见,过程开始很快:曲线迅速上升,然后越来越慢。
以下是定时函数的实际应用(点击火车)
.train {
position: relative;
cursor: pointer;
width: 177px;
height: 160px;
left: 0px;
transition: left 5s cubic-bezier(0.0, 0.5, 0.5, 1.0);
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">
</body>
</html>
CSS
.train {
left: 0;
transition: left 5s cubic-bezier(0, .5, .5, 1);
/* click on a train sets left to 450px, thus triggering the animation */
}
有几个内置曲线:linear
、ease
、ease-in
、ease-out
和 ease-in-out
。
linear
是 cubic-bezier(0, 0, 1, 1)
的简写 – 一条直线,我们上面已经描述过。
其他名称是以下 cubic-bezier
的简写
ease * |
ease-in |
ease-out |
ease-in-out |
---|---|---|---|
(0.25, 0.1, 0.25, 1.0) |
(0.42, 0, 1.0, 1.0) |
(0, 0, 0.58, 1.0) |
(0.42, 0, 0.58, 1.0) |
*
– 默认情况下,如果没有定时函数,则使用 ease
。
因此,我们可以使用 ease-out
来实现减速的火车
.train {
left: 0;
transition: left 5s ease-out;
/* same as transition: left 5s cubic-bezier(0, .5, .5, 1); */
}
但它看起来有点不同。
贝塞尔曲线可以使动画超出其范围。
曲线上的控制点可以具有任何 y
坐标:甚至负数或很大的数。然后贝塞尔曲线也会延伸到非常低或非常高的地方,使动画超出其正常范围。
以下示例中的动画代码是
.train {
left: 100px;
transition: left 5s cubic-bezier(.5, -1, .5, 2);
/* click on a train sets left to 450px */
}
属性 left
应该从 100px
动画到 400px
。
但是,如果你点击火车,你会看到
- 首先,火车向后移动:
left
变得小于100px
。 - 然后它向前移动,比
400px
稍微远一点。 - 然后又向后移动 - 到
400px
。
.train {
position: relative;
cursor: pointer;
width: 177px;
height: 160px;
left: 100px;
transition: left 5s cubic-bezier(.5, -1, .5, 2);
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='400px'">
</body>
</html>
如果我们看一下给定贝塞尔曲线的图形,原因就很明显了
我们将第二个点的 y
坐标移动到零以下,并将第三个点的 y
坐标设置为大于 1
,因此曲线超出了“常规”象限。y
超出了“标准”范围 0..1
。
众所周知,y
表示“动画过程的完成程度”。y = 0
对应于起始属性值,y = 1
对应于结束值。因此,y<0
将属性移动到起始 left
之外,y>1
将属性移动到最终 left
之外。
这当然是一种“柔和”的变化。如果我们将 y
值设置为 -99
和 99
,那么火车将跳出范围更多。
但是,我们如何为特定任务创建贝塞尔曲线呢?有很多工具。
- 例如,我们可以在 https://cubic-bezier.com 网站上进行操作。
- 浏览器开发者工具也对 CSS 中的贝塞尔曲线提供了特殊支持
- 使用 F12(Mac:Cmd+Opt+I)打开开发者工具。
- 选择
Elements
选项卡,然后注意右侧的Styles
子面板。 - 包含单词
cubic-bezier
的 CSS 属性将在该单词之前有一个图标。 - 点击此图标以编辑曲线。
步骤
计时函数 steps(number of steps[, start/end])
允许将过渡拆分为多个步骤。
让我们用数字来看一个例子。
这里有一组数字,没有动画,只是一个源
#digit {
border: 1px solid red;
width: 1.2em;
}
#stripe {
display: inline-block;
font: 32px monospace;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="digit"><div id="stripe">0123456789</div></div>
</body>
</html>
在 HTML 中,一组数字被包含在一个固定长度的 <div id="digits">
中
<div id="digit">
<div id="stripe">0123456789</div>
</div>
#digit
div 具有固定宽度和边框,因此看起来像一个红色窗口。
我们将创建一个计时器:数字将以离散的方式逐个出现。
为了实现这一点,我们将使用 overflow: hidden
将 #stripe
隐藏在 #digit
之外,然后逐步将 #stripe
向左移动。
将有 9 个步骤,每个数字对应一个步进移动。
#stripe.animate {
transform: translate(-90%);
transition: transform 9s steps(9, start);
}
steps(9, start)
的第一个参数是步数。变换将被分成 9 部分(每部分 10%)。时间间隔也会自动分成 9 部分,因此 transition: 9s
为整个动画提供了 9 秒的时间——每个数字 1 秒。
第二个参数是两个词中的一个:start
或 end
。
start
表示在动画开始时需要立即执行第一步。
实际操作
#digit {
width: .5em;
overflow: hidden;
font: 32px monospace;
cursor: pointer;
}
#stripe {
display: inline-block
}
#stripe.animate {
transform: translate(-90%);
transition-property: transform;
transition-duration: 9s;
transition-timing-function: steps(9, start);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
Click below to animate:
<div id="digit"><div id="stripe">0123456789</div></div>
<script>
digit.onclick = function() {
stripe.classList.add('animate');
}
</script>
</body>
</html>
点击数字会立即将其更改为 1
(第一步),然后在下一秒开始时更改。
该过程按以下方式进行
0s
–-10%
(在第一秒开始时立即进行第一次更改)1s
–-20%
- …
8s
–-90%
- (最后一秒显示最终值)。
这里,由于 steps
中的 start
,第一次更改是立即进行的。
备选值 end
表示更改不应在开始时应用,而应在每秒结束时应用。
因此,steps(9, end)
的过程将如下所示
0s
–0
(在第一秒内没有任何变化)1s
–-10%
(在第一秒结束时进行第一次更改)2s
–-20%
- …
9s
–-90%
以下是 steps(9, end)
的实际操作(注意第一个数字更改之前的暂停)
#digit {
width: .5em;
overflow: hidden;
font: 32px monospace;
cursor: pointer;
}
#stripe {
display: inline-block
}
#stripe.animate {
transform: translate(-90%);
transition-property: transform;
transition-duration: 9s;
transition-timing-function: steps(9, end);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
Click below to animate:
<div id="digit"><div id="stripe">0123456789</div></div>
<script>
digit.onclick = function() {
stripe.classList.add('animate');
}
</script>
</body>
</html>
steps(...)
还有一些预定义的简写形式
step-start
– 等同于steps(1, start)
。也就是说,动画立即开始并执行 1 步。因此,它立即开始并结束,就像没有动画一样。step-end
– 等同于steps(1, end)
:在transition-duration
结束时以单步执行动画。
这些值很少使用,因为它们不代表真正的动画,而是一个单步更改。我们在这里提到它们是为了完整性。
事件:“transitionend”
当 CSS 动画结束时,transitionend
事件将触发。
它被广泛用于在动画完成后执行操作。我们还可以组合动画。
例如,下面的示例中,船在被点击时开始来回航行,每次都向右航行得更远
动画由函数 go
启动,该函数在每次过渡结束后重新运行,并翻转方向。
boat.onclick = function() {
//...
let times = 1;
function go() {
if (times % 2) {
// sail to the right
boat.classList.remove('back');
boat.style.marginLeft = 100 * times + 200 + 'px';
} else {
// sail to the left
boat.classList.add('back');
boat.style.marginLeft = 100 * times - 200 + 'px';
}
}
go();
boat.addEventListener('transitionend', function() {
times++;
go();
});
};
transitionend
的事件对象具有一些特定属性。
event.propertyName
- 已完成动画的属性。如果我们同时对多个属性进行动画,这可能很有用。
event.elapsedTime
- 动画持续的时间(以秒为单位),不包括
transition-delay
。
关键帧
我们可以使用 @keyframes
CSS 规则将多个简单动画组合在一起。
它指定了动画的“名称”和规则——动画的内容、时间和位置。然后使用 animation
属性,我们可以将动画附加到元素并为其指定其他参数。
以下是一个带有解释的示例。
<div class="progress"></div>
<style>
@keyframes go-left-right { /* give it a name: "go-left-right" */
from { left: 0px; } /* animate from left: 0px */
to { left: calc(100% - 50px); } /* animate to left: 100%-50px */
}
.progress {
animation: go-left-right 3s infinite alternate;
/* apply the animation "go-left-right" to the element
duration 3 seconds
number of times: infinite
alternate direction every time
*/
position: relative;
border: 2px solid green;
width: 50px;
height: 20px;
background: lime;
}
</style>
关于 @keyframes
的文章很多,还有一个 详细规范。
你可能不会经常使用 @keyframes
,除非你的网站上所有内容都在不断运动。
性能
大多数 CSS 属性都可以进行动画,因为它们大多数都是数值。例如,width
、color
、font-size
都是数字。当你对它们进行动画时,浏览器会逐帧逐渐改变这些数字,从而产生平滑的效果。
然而,并非所有动画都能像你希望的那样平滑,因为不同的 CSS 属性的更改成本不同。
更技术性的细节是,当样式发生变化时,浏览器会经过 3 个步骤来呈现新的外观。
- 布局:重新计算每个元素的几何形状和位置,然后
- 绘制:重新计算每个元素在它们的位置上的外观,包括背景、颜色,
- 合成:将最终结果渲染到屏幕上的像素中,如果存在 CSS 变换,则应用它们。
在 CSS 动画期间,此过程会在每一帧重复。但是,从不影响几何形状或位置的 CSS 属性(例如 color
)可能会跳过布局步骤。如果 color
发生变化,浏览器不会计算任何新的几何形状,它会转到绘制 → 合成。还有一些属性可以直接转到合成。你可以在 https://csstriggers.com 找到更长的 CSS 属性列表以及它们触发的阶段。
计算可能需要时间,尤其是在包含许多元素和复杂布局的页面上。这些延迟实际上在大多数设备上都是可见的,导致动画“抖动”,不流畅。
跳过布局步骤的属性的动画速度更快。如果绘制也被跳过,则效果会更好。
transform
属性是一个不错的选择,因为
- CSS 变换会影响目标元素框的整体(旋转、翻转、拉伸、移动它)。
- CSS 变换不会影响相邻元素。
…因此浏览器在合成阶段将 `transform` 应用于已有的布局和绘制计算之上。
换句话说,浏览器首先计算布局(尺寸、位置),然后在绘制阶段用颜色、背景等进行绘制,最后将 `transform` 应用于需要变换的元素框。
`transform` 属性的更改(动画)不会触发布局和绘制步骤。更重要的是,浏览器利用图形加速器(CPU 或显卡上的专用芯片)来处理 CSS 变换,从而使其效率非常高。
幸运的是,`transform` 属性非常强大。通过在元素上使用 `transform`,你可以旋转和翻转它,拉伸和缩小它,移动它,以及 更多功能。因此,我们可以使用 `transform: translateX(…)` 代替 `left/margin-left` 属性,使用 `transform: scale` 来增加元素大小,等等。
`opacity` 属性也不会触发布局(在 Mozilla Gecko 中也会跳过绘制)。我们可以用它来实现显示/隐藏或淡入/淡出效果。
将 `transform` 与 `opacity` 配合使用通常可以满足我们的大部分需求,提供流畅、美观的动画。
例如,这里点击 `#boat` 元素会添加一个包含 `transform: translateX(300px)` 和 `opacity: 0` 的类,从而使其向右移动 `300px` 并消失。
<img src="https://js.cx/clipart/boat.png" id="boat">
<style>
#boat {
cursor: pointer;
transition: transform 2s ease-in-out, opacity 2s ease-in-out;
}
.move {
transform: translateX(300px);
opacity: 0;
}
</style>
<script>
boat.onclick = () => boat.classList.add('move');
</script>
这是一个更复杂的例子,使用了 `@keyframes`。
<h2 onclick="this.classList.toggle('animated')">click me to start / stop</h2>
<style>
.animated {
animation: hello-goodbye 1.8s infinite;
width: fit-content;
}
@keyframes hello-goodbye {
0% {
transform: translateY(-60px) rotateX(0.7turn);
opacity: 0;
}
50% {
transform: none;
opacity: 1;
}
100% {
transform: translateX(230px) rotateZ(90deg) scale(0.5);
opacity: 0;
}
}
</style>
总结
CSS 动画允许对一个或多个 CSS 属性进行平滑(或逐步)动画更改。
它们适用于大多数动画任务。我们也可以使用 JavaScript 来实现动画,下一章将专门介绍这一点。
与 JavaScript 动画相比,CSS 动画的局限性
- 简单的事情简单地完成。
- 对 CPU 来说快速且轻量级。
- JavaScript 动画更灵活。它们可以实现任何动画逻辑,例如元素的“爆炸”。
- 不仅仅是属性更改。我们可以在 JavaScript 中创建新的元素作为动画的一部分。
在本节前面的示例中,我们对 `font-size`、`left`、`width`、`height` 等进行了动画处理。在实际项目中,我们应该使用 `transform: scale()` 和 `transform: translate()` 来获得更好的性能。
大多数动画可以使用本章所述的 CSS 来实现。`transitionend` 事件允许在动画结束后运行 JavaScript,因此它可以很好地与代码集成。
但在下一章中,我们将进行一些 JavaScript 动画来涵盖更复杂的情况。
评论
<code>
标签,对于多行代码,请使用<pre>
标签,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)