简介
CSS关键帧动画非常棒。它们是CSS中最强大、最通用的工具之一,我们可以用它们来做各种有趣的事情。
但是它们也经常被误解。它们有点古怪,如果你不了解这些古怪的东西,使用它们就会很令人沮丧。
在本教程中,我们将深入研究CSS关键帧。我们将弄清楚它们是如何工作的,并看看如何用它们来制作一些相当华丽的动画。✨
目标受众
本教程是为试图更容易掌握CSS的JavaScript开发人员编写的。但它也适合于所有对HTML/CSS的基础知识比较熟悉的开发者。
如果你对CSS有很深的了解,你可能会知道我们所讲的大部分内容,但在本篇文章的最后,我确实分享了一些非常酷和晦涩的东西。😄
语法
CSS关键帧动画的主要思想是,它将在不同的CSS块之间进行插值。
例如,我们在这里定义了一个关键帧动画,它将平滑地将一个元素的水平位置从-100%
到0%
。
@keyframes slide-in {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0%);
}
}
每个@keyframes
语句都需要一个名字!在这个例子中,我们选择将其命名为slide-in
。你可以把它看作是一个全局变量。*
关键帧动画是为了通用和可重复使用。我们可以通过animation
属性将它们应用于特定的选择器。
<style>
/* Create the animation... */
@keyframes slide-in {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0%);
}
}
/* ...and then apply it: */
.box {
animation: slide-in 1000ms;
}
</style>
<div class="box">
Hello World
</div>
(要重新运行动画,通过点击图标刷新 "结果 "窗格。)
与transition
属性一样,animation
需要一个持续时间。这里我们说动画应该持续1秒(1000ms)。
浏览器将在我们的from
和to
块中的声明,在指定的持续时间内进行插值。一旦属性被设置,这就立即发生。
我们可以在同一个动画声明中对多个属性进行动画处理。这里有一个改变多个属性的更复杂的例子。
<style>
@keyframes drop-in {
from {
transform:
rotate(-30deg) translateY(-100%);
opacity: 0;
}
to {
transform:
rotate(0deg) translateY(0%);
opacity: 1;
}
}
.box {
animation: drop-in 1000ms;
}
</style>
<div class="box">
Hello World
</div>
定时功能
我们可以为我们的关键帧动画使用同样的定时函数库。而且,与transition
一样,默认值是ease
。
我们可以用animation-timing-function
属性来覆盖它。
<style>
@keyframes slide-in {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0%);
}
}
.box {
animation: slide-in 1000ms;
animation-timing-function: linear;
}
</style>
<div class="box">
Hello World
</div>
循环动画
默认情况下,关键帧动画将只运行一次,但我们可以用animation-iteration-count
属性来控制。
<style>
@keyframes slide-in {
from {
transform: translateX(-100%);
opacity: 0.25;
}
to {
transform: translateX(0%);
opacity: 1;
}
}
.box {
animation: slide-in 1000ms;
animation-iteration-count: 3;
}
</style>
<div class="box">
Hello World
</div>
像这样指定一个整数有点罕见,但有一个特殊的值很方便:infinite
。
例如,我们可以用它来创建一个加载旋钮。
<style>
@keyframes spin {
from {
transform: rotate(0turn);
}
to {
transform: rotate(1turn);
}
}
.spinner {
animation: spin 1000ms;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
</style>
<img
class="spinner"
alt="Loading…"
src="/images/keyframe-animations/loader.svg"
/>
请注意,对于旋转器,我们一般希望使用linear
定时函数,以便运动是恒定的(尽管这有点主观--尝试改变它,看看你怎么想!)。
多步动画
除了from
和to
关键词之外,我们还可以使用百分比。这使我们可以增加2个以上的步骤。
<style>
@keyframes fancy-spin {
0% {
transform: rotate(0turn) scale(1);
}
25% {
transform: rotate(1turn) scale(1);
}
50% {
transform: rotate(1turn) scale(1.5);
}
75% {
transform: rotate(0turn) scale(1.5);
}
100% {
transform: rotate(0turn) scale(1);
}
}
.spinner {
animation: fancy-spin 2000ms;
animation-iteration-count: infinite;
}
</style>
<img
class="spinner"
alt="Loading…"
src="/images/keyframe-animations/loader.svg"
/>
百分比指的是通过动画的进度。from
实际上只是语法糖?为0%
。而to
,则是100%
的糖。
重要的是,计时功能适用于每个步骤。我们不会在整个动画中得到一个单一的轻松。
在这个操场上,两个旋转器在2秒内完成一个完整的旋转。但是multi-step-spin
,把它分成4个不同的步骤,每个步骤都应用了计时函数。
<style>
@keyframes spin {
0% {
transform: rotate(0turn);
}
100% {
transform: rotate(1turn)
}
}
@keyframes multi-step-spin {
0% {
transform: rotate(0turn);
}
25% {
transform: rotate(0.25turn);
}
50% {
transform: rotate(0.5turn);
}
75% {
transform: rotate(0.75turn);
}
100% {
transform: rotate(1turn);
}
}
.spinner {
animation: spin 2000ms;
animation-iteration-count: infinite;
}
.multi-step-spinner {
animation: multi-step-spin 2000ms;
animation-iteration-count: infinite;
}
</style>
<img
class="spinner"
alt="Loading…"
src="/images/keyframe-animations/loader.svg"
/>
<img
class="multi-step-spinner"
alt="Loading…"
src="/images/keyframe-animations/loader.svg"
/>
不幸的是,我们不能用CSS关键帧动画来控制这种行为,尽管它可以用Web Animations API来配置。
交替动画
让我们假设,我们希望一个元素能够 "呼吸",充气和放气。
我们可以把它设置成一个三步动画。
<style>
@keyframes grow-and-shrink {
0% {
transform: scale(1);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
.box {
animation: grow-and-shrink 4000ms;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
</style>
<div class="box"></div>
它在前一半的时间里成长为其默认大小的1.5倍。一旦它达到了这个峰值,它就在后半部分缩回到1倍。
这很有效,但还有一个更优雅的方法来达到同样的效果。我们可以使用animation-direction
属性。
<style>
@keyframes grow-and-shrink {
0% {
transform: scale(1);
}
100% {
transform: scale(1.5);
}
}
.box {
animation: grow-and-shrink 2000ms;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
</style>
<div class="box"></div>
animation-direction
控制序列的顺序。默认值是 ,在指定的时间内从0%到100%。normal
我们也可以把它设置为reverse
。这将使动画向后播放,从100%到0%。
有趣的是,我们可以把它设置为alternate
,这样在随后的迭代中就会在normal
和reverse
之间进行乒乓。
我们不需要有一个大的动画来增长和缩小,而是将我们的动画设置为增长,然后在下一次迭代时将其逆转,使其缩小。
缩短一半的时间
最初,我们的 "呼吸 "动画持续了4秒。然而,当我们改用另一种策略时,我们将持续时间减半,降至2秒。
这是因为每次迭代只执行了一半的工作。它总是花2秒来增长,2秒来缩小。之前,我们有一个4秒长的单一动画。现在,我们有一个2秒长的动画,需要2次迭代来完成一个完整的周期。
速记值
在这一课中,我们已经掌握了很多动画属性,而且还打了很多字!幸运的是,就像学习 一样,我们已经掌握了很多动画属性。
幸运的是,与transition
一样,我们可以使用animation
的速记法来组合所有这些属性。
上面的动画可以被重写。
.box {
/*
From this:
animation: grow-and-shrink 2000ms;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
...to this:
*/
animation: grow-and-shrink 2000ms ease-in-out infinite alternate;
}
这里也有一个好消息:**顺序并不重要。**在大多数情况下,你可以按你想要的任何顺序抛出这些属性。你不需要记住一个特定的顺序。
有一个例外。animation-delay
,一个我们很快就会谈论的属性,需要放在持续时间之后,因为这两个属性采取相同的值类型(毫秒/秒)。
出于这个原因,我倾向于将延迟排除在速记之外。
.box {
animation: grow-and-shrink 2000ms ease-in-out infinite alternate;
animation-delay: 500ms;
}
填充模式
关键帧动画中最令人困惑的方面可能是填充模式。它们是我们在实现关键帧自信的道路上的最大障碍。
让我们从一个问题开始。
我们想让我们的元素淡出。动画本身运行良好,但当它结束时,元素又突然出现了。
<style>
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.box {
animation: fade-out 1000ms;
}
</style>
<div class="box">
Hello World
</div>
如果我们把这个元素的不透明度随时间变化的图形化,它将看起来像这样。
为什么该元素会跳回完全可见?嗯,from
和to
块中的声明只在动画运行时适用。
在1000ms过后,动画会自己打包上路。to
块中的声明会消失,留给我们的元素的是其他地方定义的CSS声明。由于我们没有在其他地方为这个元素设置opacity
,所以它又回到了它的默认值(1
)。
解决这个问题的一个方法是在.box
选择器上添加一个opacity
的声明。
<style>
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.box {
animation: fade-out 1000ms;
/*
Change the "default" value for opacity,
so that it reverts to 0 when the
animation completes.
*/
opacity: 0;
}
</style>
<div class="box">
Hello World
</div>
当动画运行时,@keyframes
语句中的声明会推翻.box
选择器中的不透明度声明。但是,一旦动画结束,该声明就会生效,并保持盒子的隐藏。
特殊性?
在CSS中,冲突是根据选择器的 "特定性 "来解决的。一个ID选择器(#login-form
)将赢得与一个类选择器(.thing
)的斗争。
但关键帧动画呢?它们的特异性是什么?
事实证明,特异性并不是思考这个问题的正确方式;相反,我们需要思考级联的起源问题。
所以,我们可以更新我们的CSS,使元素的属性与to
块相匹配,但这真的是最好的方法吗?
向前填充
与其依赖回退声明,让我们考虑另一种方法,使用animation-fill-mode
。
<style>
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.box {
animation: fade-out 1000ms;
animation-fill-mode: forwards;
}
</style>
<div class="box">
Hello World
</div>
animation-fill-mode
让我们把动画中的最终值往前坚持。
"forwards "是一个非常混乱的名字,但希望在这个图上看到它能让我们更清楚一些
当动画结束时,animation-fill-mode: forwards
将复制/粘贴最后一个块中的声明,将它们向前持久化。
向后填充
我们并不总是想让我们的动画立即开始!我们可以指定动画的时间。正如transition
,我们可以指定一个延迟,用animation-delay
属性。
不幸的是,我们遇到了一个类似的问题。
<style>
@keyframes slide-in {
from {
transform: translateX(-100%);
opacity: 0.25;
}
to {
transform: translateX(0%);
opacity: 1;
}
}
.box {
animation: slide-in 1000ms;
animation-delay: 500ms;
}
</style>
<div class="box">
Hello World
</div>
在最初的半秒钟里,这个元素是完全可见的。
from
和to
块中的CSS只在动画运行时被应用。令人沮丧的是,animation-delay
的时间并不算数。因此,在最初的半秒里,from
块中的CSS就好像不存在一样。
animation-fill-mode
有另一个值可以帮助我们: 。这将在backwards
时间上向后应用第一个块的CSS。
"向前 "和 "向后 "是令人困惑的值,但这里有一个比喻可能会有帮助:想象一下,如果我们从页面加载的那一刻起就记录了用户的会话。我们可以在视频中向前和向后擦写。我们可以在动画开始之前向后刷,也可以在动画结束之后向前刷。
<style>
@keyframes slide-in {
from {
transform: translateX(-100%);
opacity: 0.25;
}
to {
transform: translateX(0%);
opacity: 1;
}
}
.box {
animation: slide-in 1000ms;
animation-delay: 500ms;
animation-fill-mode: backwards;
}
</style>
<div class="box">
Hello World
</div>
如果我们想把动画向前和向后持久化呢?我们可以使用第三个值,both
,它在两个方向上都能持续。
就个人而言,我希望both
是默认值。它是如此的直观!虽然这可能会使我们更难理解某个特定的CSS值在哪里被设置。
就像我们正在讨论的所有动画属性一样,它可以被扔到animation
速记沙拉里。
.box {
animation: slide-in 1000ms ease-out both;
animation-delay: 500ms;
}
使用CSS变量的动态动画
关键帧动画本身就够酷的了,但是当我们把它们和CSS变量(又称CSS自定义属性)混合在一起时,事情就变得⚡️下一个层次了。
让我们假设一下,我们想用本课学到的所有知识来创建一个弹跳球动画。
<style>
@keyframes bounce {
from {
transform: translateY(0px);
}
to {
transform: translateY(-20px);
}
}
.box {
animation:
bounce 300ms
alternate infinite
cubic-bezier(.2, .65, .6, 1);
}
</style>
<div class="box"></div>
立体贝赛尔?
为了使弹跳动画更加逼真,我使用了一个自定义的计时函数,使用cubic-bezier
。
CSS动画的目的是通用的和可重复使用的,但这个动画总是会使一个元素弹跳20px。如果不同的元素可以提供不同的 "弹跳高度",那不是很好吗?
通过CSS变量,我们可以做到这一点。
<style>
@keyframes bounce {
from {
transform: translateY(0px);
}
to {
transform: translateY(
var(--bounce-offset)
);
}
}
.box {
animation:
bounce alternate infinite
cubic-bezier(.2, .65, .6, 1);
}
.box.one {
--bounce-offset: -20px;
animation-duration: 200ms;
}
.box.two {
--bounce-offset: -30px;
animation-duration: 300ms;
}
.box.three {
--bounce-offset: -40px;
animation-duration: 400ms;
}
</style>
<section>
<div class="box one"></div>
<div class="box two"></div>
<div class="box three"></div>
</section>
我们的@keyframes
动画已经被更新了,它不再弹跳到-20px
,而是访问了--bounce-offset
属性的值。由于该属性在每个盒子中都有不同的值,所以它们各自的弹跳量也不同。
这种策略使我们能够创建可重复使用的、可定制的关键帧动画。想想看,它就像React组件的道具一样!
衍生的数值与计算
所以,上面的例子有一点困扰着我。
通过translateY
函数,正值将元素向下移动,负值将元素向上移动。我们想让元素向上移动,所以我们必须使用一个负值。
但这是一个执行细节。当我想应用这个动画时,我需要使用一个负值,这很奇怪。
当CSS变量具有语义时,它们的工作效果最好。与其将--bounce-offset
设置为一个负值,我更愿意这样做。
.box.one {
--bounce-height: 20px;
}
使用calc
,我们可以在我们的@keyframes
at-rule中,从提供的值中得出真实的值。
@keyframes bounce {
from {
transform: translateY(0px);
}
to {
transform: translateY(
calc(var(--bounce-height) * -1)
);
}
}
我们只定义了这个关键帧动画一次,但我们可能会多次使用它。值得为 "消费者 "方面进行优化,以使它尽可能地令人愉快地使用,即使它使定义变得有点复杂。
calc
让我们为我们的关键帧动画制作出完美的API。💯
刚刚开始
当我在制作最后几个演示时,我意识到CSS在过去的几年里有了很大的发展!
它已经成为一种令人难以置信的语言,表现力强、灵活而强大。我喜欢写CSS。
然而,许多前端开发者与这种语言的关系却非常不同。我曾与数以百计的JavaScript开发者交谈,他们发现CSS令人沮丧和困惑。有时,完全相同的CSS会有完全不同的表现!这让人感觉很不一致。这感觉太不一致了。
我对此有一个理论:与JS不同,CSS的很多东西都是隐含的和幕后的。仅仅知道这些属性是不够的,你还需要知道驱动它们的原理。
在过去的一年里,我花了全职时间来编写一门课程,以帮助在更深、更基本的层次上教授CSS。