关键帧动画的交互式指南

307 阅读11分钟

简介

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)。

浏览器将在我们的fromto 块中的声明,在指定的持续时间内进行插值。一旦属性被设置,这就立即发生。

我们可以在同一个动画声明中对多个属性进行动画处理。这里有一个改变多个属性的更复杂的例子。

<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 定时函数,以便运动是恒定的(尽管这有点主观--尝试改变它,看看你怎么想!)。

多步动画

除了fromto 关键词之外,我们还可以使用百分比。这使我们可以增加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 ,这样在随后的迭代中就会在normalreverse 之间进行乒乓。

我们不需要有一个大的动画来增长和缩小,而是将我们的动画设置为增长,然后在下一次迭代时将其逆转,使其缩小。

缩短一半的时间

最初,我们的 "呼吸 "动画持续了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>

如果我们把这个元素的不透明度随时间变化的图形化,它将看起来像这样。

image.png

为什么该元素会跳回完全可见?嗯,fromto 块中的声明只在动画运行时适用。

在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 让我们把动画中的最终值往前坚持。

image.png

"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>

在最初的半秒钟里,这个元素是完全可见的。

image.png

fromto 块中的CSS只在动画运行时被应用。令人沮丧的是,animation-delay 的时间并不算数。因此,在最初的半秒里,from 块中的CSS就好像不存在一样。

animation-fill-mode 有另一个值可以帮助我们: 。这将在backwards时间上向后应用第一个块的CSS。

image.png

"向前 "和 "向后 "是令人困惑的值,但这里有一个比喻可能会有帮助:想象一下,如果我们从页面加载的那一刻起就记录了用户的会话。我们可以在视频中向前和向后擦写。我们可以在动画开始之前向后刷,也可以在动画结束之后向前刷。

<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 ,它在两个方向上都能持续。

image.png

就个人而言,我希望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。