w3ctech

调试 CSS Keyframe 动画

此文为译文,原文地址:https://css-tricks.com/debugging-css-keyframe-animations/

学会语法就可以制作 CSS 动画,但是要想做出动感、美观的动画,仅会语法是不够的。动画直接关系到用户体验,因此我们需要改进代码,从而找到正确的触发时机并掌握调试动画的方法。经过一番研究之后,我总结了一些有用的工具和方法。

使用负的延迟值

如果你需要同时执行多个动画并错开它们的开始时间,可以使用animation-delay。但是这会导致用户打开网页时有些元素需要静止一段时间才会开始移动。

此时可以给animation-delay设置一个负数,这样会将播放头向前移动,因此用户打开网页的时候所有动画都会播放。使用这种方式可以通过共享一套 keyframes 来实现不同的动画。

这个技巧也可以用来调试。设置animation-play-state: paused;然后把延迟设置成不同的负数,就可以看到动画在不同帧的状态。

.thing {
  animation: move 2s linear infinite alternate;
  animation-play-state: paused;
  animation-delay: -1s;
}

示例:

http://codepen.io/css-tricks/embed/LVMMGZ

在下面这个有趣的例子中,可以看到两个机器人的动作错开了一点时间,这样看起来会更自然。我们给粉色的机器人设置一个负的延迟,这样用户打开网页的时候他就已经处在移动状态了。

.teal {
 animation: hover 2s ease-in-out infinite both;
}

.purple {
 animation: hover 2s -0.5s ease-in-out infinite both;
}

@keyframes hover {
  50% {
    transform: translateY(-4px);
  }
}

示例:http://codepen.io/sdras/embed/qdLJLJ

多 transform 之殇

为了充分提高性能,你需要尽可能多地使用transform来移动和改变元素,这样就会减少修改margintop/left之类属性带来的重绘损耗。Paul Lewis维护的CSS Triggers非常棒,可以直观地看出这些属性对应的损耗。然而,如果你使用多个 transform 来移动元素,就可能带来一系列问题。

第一个问题是顺序。Transform 并不像你想的那样同步发生,而是按照一定顺序。最右边的操作最先执行,然后往左依次执行。举例来说,下面的代码中scale首先执行,然后是translate,最后是rotate

@keyframes foo {
 to {
   /*         3rd           2nd              1st      */
   transform: rotate(90deg) translateX(30px) scale(1.5);
 }
}

大多数情况下这不是我们想要的,通常我们希望这些操作同时发生。此外,如果你把 transform 分割成多个 keyframe,事情会变得更加复杂,有些操作同步,有些操作不同步。比如下面这个例子:

@keyframes foo {
  30% {
    transform: rotateY(360deg);
  }
  65% {
    transform: translateY(-30px) rotateY(-360deg) scale(1.5);
  }
  90% {
    transform: translateY(10px) scale(0.75);
  }
}

这段代码会产生非常糟糕的效果。不幸的是,解决方法并不优雅,通常你必须嵌套多个<div>,每个应用一个变化,这样就不会产生冲突。

http://codepen.io/sdras/embed/bdOvJL

还有一些解决方法,比如使用矩阵变换(通常无法手写)或者使用 JavaScript 的动画 API(比如GreenSock),这样就可以同步执行多个变换操作。

使用多个 div 还可以解决 SVG 的一些 bug。在 Safari 中,不能同时声明 opacity 和 transform 动画——其中一个会失败。你可以查看本文的第一个例子。

在 2015 年 8 月初,Chrome Canary 支持独立 transform 声明。这意味着我们不再需要关心执行顺序,你可以分别声明rotatetranslatescale

调试工具

ChromeFirefox都提供了调试动画的工具。这些工具提供了控制速度的滑块、暂停按钮以及 easing 值对应的 UI。对于调试 CSS 动画来说,减速并检查特殊位置的动画效果真的非常有用。

这些工具都使用了 Lea Verou 的cubic-bezier.com可视化工具和 GUI。这样你就可以直接进行调试,不用每次都跑到网站上输入文本。

这些工具可以帮助我们细粒度地调整动画,下面是两个工具的 UI:

Chrome 和 Firefox 都可以控制时间(加速或者减速),也可以手动执行动画。Chrome 正在开发许多高级的时间线工具,可以用它们同时调试多个元素。这是件好事,每次只能调试一个元素确实是个很大的限制。

我遇到的一个问题是,如果元素的动画时间很短,那就很难及时获取这个元素。我的解决方案是设置animation-iteration-count: infinite;,这样就不需要和时间赛跑。

此外,我发现减速、重播以及调整时间非常有用,你可以在很低的层面观察动画到底是如何执行的。如果你在低速下调整好动画,恢复到正常速度之后动画会变得非常棒。

使用 JavaScript 调试 CSS 动画事件

如果你想知道每个动画触发的准确时间和位置,可以使用 JavaScript 来监听animationstartanimationiterationanimationend事件并输出信息。

看下面这个例子:

http://codepen.io/sdras/embed/PqXeMX

保证 keyframe 足够准确

我经常看到有人在 0% 和 100% keyframe中声明相同的属性和值。没必要这样做,浏览器会在动画开始和结束时自动处理属性值。

如下所示:

.element {
 animation: animation-name 2s linear infinite;
}

@keyframes animation-name {
  0% {
   transform: translateX(200px);
 }
  50% {
   transform: translateX(350px);
 }
 100% {
   transform: translateX(200px);
 }
}

可以改写成:

.element {
 transform: translateX(200px);
 animation: animation-name 2s linear infinite;
}

@keyframes animation-name {
  50% {
   transform: translateX(350px);
 }
}

让动画也 DRY

漂亮和简洁的动画通常意味着一个特殊的cubic-bezier()easing 函数。仔细调整过的 easing 函数会成为公司的一个特点。动画会传递公司的品牌和“声音”。如果你在网站中大量使用这个函数,最简单(并且最一致)的方法就是把函数保存到变量中,我们就是这么做的。SASS 或者其他预/后处理器都可以做到这一点:

$smooth: cubic-bezier(0.17, 0.67, 0.48, 1.28);

.foo { animation: animation-name 3s $smooth; }

.bar { animation: animation-name 1s $smooth; }

如果使用 CSS keyframe 编写动画,我们需要尽量利用 GPU。这意味着如果你需要操作多个对象,就需要提前准备好 DOM 并给元素分层。使用标准的 CSS 声明代码块可以让硬件加速原生 DOM 元素(SVG 不行)。由于我们需要复用代码块,把它存储到 mixin 或者 extend 中是个不错的选择:

@mixin accelerate($name) {
 will-change: $name;
 transform: translateZ(0);
 backface-visibility: hidden;
 perspective: 1000px;
}

.foo {
  @include accelerate(transform);
}

一定要小心,同时操作多个元素可能会引发副作用并严重降低性能。大多数动画都没有问题,不过如果你使用类似 haml 的技术来生成多个 DOM 元素,那就要小心了。

使用循环提高性能

Smashing Magazine 最近发布的一篇文章中详细介绍了Species in Pices项目的原理。在其中的一节里,作者解释了为什么同时操作大量对象会导致性能问题。他说:

假设你要同时移动 30 个对象;你需要浏览器做很多工作,因此会导致问题。如果你的动画速度是 0.199 秒并且每个对象都延迟 0.2 秒,那同时只会操作一个对象,从而解决问题。虽然总的动画次数不变,但是因为动画现在变成了一个连续执行的序列,性能可以提高 30 倍。

可以使用 Sass 或者其他预/后处理器的for循环来编写这样的代码。下面是我写的一个简单的例子:

@for $i from 1 through $n {
  &:nth-child(#{$i}) {
    animation: loadIn 2s #{$i*0.11}s $easeOutSine forwards;
  }
}

这不仅会错开动画,还可以错开其他的视觉效果,比如颜色改变。(点击 rerun 来重播动画。)

示例:http://codepen.io/sdras/embed/RPEMZr

调整动画顺序

编写很长的动画时,常用的办法是把它们写成一个序列并加上延迟,比如:

animation: foo 3s ease-in-out, bar 4s 3s ease-in-out, brainz 6s 7s ease-in-out;

但是假设你现在要重构代码,需要修改第一个动画的时间,这会导致后续的动画都需要修改时间。这也不是什么大事:

animation: foo 2.5s ease-in-out, bar 4s 2.5s ease-in-out, brainz 6s 6.5s ease-in-out;

不过假设我们需要添加一个新动画并再次调整时间(在实际项目中类似的改动经常发生),就会发现这种修改方式非常低效。如果你做过 3 次以上,就会明白,这真的很低效。

继续,假设动画执行到一半的时候有两个改动需要同时出发,所以你需要保证两个不同的属性一致然后……好的,你应该懂了。这就是为什么处理超过三个串联动画时我会使用 JavaScript 的原因。就我自己来说,我喜欢使用GreenSock 动画 API,因为它的时间线功能非常好用,而且不需要重新计算就能轻松重叠多个动画,这可以极大地提升效率。

改进动画远比编写难。通常来说,需要不断编辑、修改和调试才能提高项目的质量和性能。希望这些技巧可以帮助你更好地编写动画。

w3ctech微信

扫码关注w3ctech微信公众号

共收到3条回复

  • 赞, 好多文档都是E文,能有个中文文档真心不错

    回复此楼
  • 赞,这篇文章真心不错

    回复此楼
  • 这篇文章有太多的技巧了~

    回复此楼