2024 年 1 月 20 日

CSS 动画

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,以及同时对多个属性进行动画处理。

例如,此按钮同时对 colorfont-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 中,我们编写一个要进行动画处理的属性列表,例如:leftmargin-leftheightcolor。或者我们可以写 all,这意味着“对所有属性进行动画处理”。

请注意,有些属性无法进行动画处理。但是,大多数常用的属性都是可动画的

transition-duration

transition-duration 中,我们可以指定动画应该持续多长时间。时间应该使用 CSS 时间格式:以秒 s 或毫秒 ms 为单位。

transition-delay

transition-delay 中,我们可以指定动画之前的延迟。例如,如果 transition-delay1stransition-duration2s,则动画在属性更改后 1 秒开始,总持续时间为 2 秒。

负值也是可能的。然后动画会立即显示,但动画的起点将在给定值(时间)之后。例如,如果 transition-delay-1stransition-duration2s,则动画从中途开始,总持续时间为 1 秒。

这里动画使用 CSS translate 属性将数字从 0 转换为 9

结果
script.js
style.css
index.html
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

这里,如果您点击数字 - 它将从当前秒开始动画

结果
script.js
style.css
index.html
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 个控制点,满足以下条件

  1. 第一个控制点:(0,0)
  2. 最后一个控制点:(1,1)
  3. 对于中间点,x 的值必须在 0..1 区间内,y 可以是任何值。

CSS 中贝塞尔曲线的语法:cubic-bezier(x2, y2, x3, y3)。这里我们只需要指定第二和第三个控制点,因为第一个固定为 (0,0),第四个为 (1,1)

定时函数描述了动画过程的速度。

  • x 轴表示时间:0 – 开始,1transition-duration 的结束。
  • y 轴指定了过程的完成度:0 – 属性的初始值,1 – 最终值。

最简单的变体是动画以相同的线性速度均匀进行。这可以通过曲线 cubic-bezier(0, 0, 1, 1) 指定。

以下是该曲线的形状

…正如我们所见,它只是一条直线。随着时间 (x) 的推移,动画的完成度 (y) 稳定地从 0 变为 1

以下示例中的火车以恒定速度从左向右移动(点击它)

结果
style.css
index.html
.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)

图形

正如我们所见,过程开始很快:曲线迅速上升,然后越来越慢。

以下是定时函数的实际应用(点击火车)

结果
style.css
index.html
.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 */
}

有几个内置曲线:lineareaseease-inease-outease-in-out

linearcubic-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
结果
style.css
index.html
.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 值设置为 -9999,那么火车将跳出范围更多。

但是,我们如何为特定任务创建贝塞尔曲线呢?有很多工具。

  • 例如,我们可以在 https://cubic-bezier.com 网站上进行操作。
  • 浏览器开发者工具也对 CSS 中的贝塞尔曲线提供了特殊支持
    1. 使用 F12(Mac:Cmd+Opt+I)打开开发者工具。
    2. 选择 Elements 选项卡,然后注意右侧的 Styles 子面板。
    3. 包含单词 cubic-bezier 的 CSS 属性将在该单词之前有一个图标。
    4. 点击此图标以编辑曲线。

步骤

计时函数 steps(number of steps[, start/end]) 允许将过渡拆分为多个步骤。

让我们用数字来看一个例子。

这里有一组数字,没有动画,只是一个源

结果
style.css
index.html
#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 秒。

第二个参数是两个词中的一个:startend

start 表示在动画开始时需要立即执行第一步。

实际操作

结果
style.css
index.html
#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) 的过程将如下所示

  • 0s0(在第一秒内没有任何变化)
  • 1s-10%(在第一秒结束时进行第一次更改)
  • 2s-20%
  • 9s-90%

以下是 steps(9, end) 的实际操作(注意第一个数字更改之前的暂停)

结果
style.css
index.html
#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 属性都可以进行动画,因为它们大多数都是数值。例如,widthcolorfont-size 都是数字。当你对它们进行动画时,浏览器会逐帧逐渐改变这些数字,从而产生平滑的效果。

然而,并非所有动画都能像你希望的那样平滑,因为不同的 CSS 属性的更改成本不同。

更技术性的细节是,当样式发生变化时,浏览器会经过 3 个步骤来呈现新的外观。

  1. 布局:重新计算每个元素的几何形状和位置,然后
  2. 绘制:重新计算每个元素在它们的位置上的外观,包括背景、颜色,
  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 动画来涵盖更复杂的情况。

任务

重要性:5

显示动画,如以下图片所示(点击飞机)

  • 图片在点击时从 40x24px 增长到 400x240px(放大 10 倍)。
  • 动画持续 3 秒。
  • 最后输出:“完成!”。
  • 在动画过程中,可能会有更多点击飞机。它们不应该“破坏”任何东西。

打开任务的沙箱。

CSS 动画 widthheight

/* original class */

#flyjet {
  transition: all 3s;
}

/* JS adds .growing */
#flyjet.growing {
  width: 400px;
  height: 240px;
}

请注意,transitionend 会触发两次 - 每个属性一次。因此,如果我们不执行额外的检查,则消息会显示两次。

在沙箱中打开解决方案。

重要性:5

修改之前任务 动画飞机 (CSS) 的解决方案,使飞机比其原始大小 400x240px 更大(跳出),然后返回到该大小。

它应该看起来像这样(点击飞机)

将之前任务的解决方案作为源代码。

我们需要为该动画选择合适的贝塞尔曲线。它应该在某个地方有 y>1,以便飞机“跳出”。

例如,我们可以将两个控制点都设置为 y>1,例如:cubic-bezier(0.25, 1.5, 0.75, 1.5)

图形

在沙箱中打开解决方案。

重要性:5

创建一个函数 showCircle(cx, cy, radius) 来显示一个动画增长的圆圈。

  • cx,cy 是圆心相对于窗口的坐标,
  • radius 是圆的半径。

点击下面的按钮查看它应该是什么样子

源文档有一个带有正确样式的圆圈示例,因此任务就是正确地进行动画。

打开任务的沙箱。

在任务 动画圆圈 中,显示了一个动画增长的圆圈。

现在假设我们需要的不仅仅是一个圆圈,而是在圆圈内显示一条消息。消息应该在动画完成后(圆圈完全长大)出现,否则看起来会很丑。

在任务的解决方案中,函数 showCircle(cx, cy, radius) 绘制了圆圈,但没有提供跟踪它何时准备好的方法。

添加一个回调参数:showCircle(cx, cy, radius, callback),在动画完成后调用。callback 应该接收圆圈 <div> 作为参数。

这是一个示例

showCircle(150, 150, 100, div => {
  div.classList.add('message-ball');
  div.append("Hello, world!");
});

演示

将任务 动画圆圈 的解决方案作为基础。

教程地图

评论

评论前请阅读…
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用<code>标签,对于多行代码,请使用<pre>标签,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)