调试 CSS 关键帧动画

Avatar of Sarah Drasner
Sarah Drasner

DigitalOcean 提供适用于您旅程每个阶段的云产品。立即开始使用 $200 免费积分!

创建 CSS 动画可能需要学习语法,但要掌握美观直观的动画则需要更多的细微差别。由于动画会吸引很多注意力,因此对代码进行细化以确保时间安排正确并在出现问题时进行调试非常重要。在解决这个问题之后,我想收集一些可以帮助完成此过程的工具。

使用负延迟值

假设您有多个动画同时运行,并且您希望它们稍微错开一点。您可以使用 animation-delay,但您可能不希望用户访问页面时,有些内容不移动,需要等待延迟。

您可以将 animation-delay 设置为负数,它会将播放头向后移动,以便所有动画在用户出现时运行。当动画共享相同的关键帧值,并且仅通过延迟来区分运动时,这尤其有用。

您可以将此概念用于调试。设置 animation-play-state: paused;,然后将延迟调整为不同的负时间。您将在补间动画的不同暂停状态下看到动画。

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

示例

查看 CodePen 上 CSS-Tricks 的 LVMMGZ 笔记(@css-tricks)。

在一个有趣的演示中,我们看到两个机器人以非常轻微不同的时间悬停,以使感觉更加自然。我们在声明悬停动画时为紫色机器人设置了负延迟,以便在用户首次看到页面时他正在移动。

.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);
  }
}

查看 CodePen 上 Sarah Drasner 的 Robotter Love 笔记(@sdras)。

多个变换值的弊端

为了获得最佳性能,您还应该使用 transform 来移动和更改内容,并避免使用 margin 或 top/left 等属性造成的重绘成本。Paul Lewis 创建了一个名为 CSS Triggers 的出色资源,它以易于查看的表格形式分解了这些成本。这里的陷阱是,如果您尝试使用多个变换来移动内容,则会出现一些问题。

一个主要问题是顺序。变换不会像预期的那样同时应用,而是在操作顺序中应用。第一个操作是在最右边完成的,然后向内完成。例如,在下面的代码中,缩放将首先应用,然后是平移,最后是旋转。

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

在大多数情况下,这不是理想的。您更有可能希望所有内容同时发生。此外,当您开始将变换拆分为多个关键帧时,有些值同时应用,有些值不应用,例如

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

这将导致一些令人惊讶且不理想的结果。不幸的是,答案通常是使用多个嵌套的 <div>,对每个 <div> 应用单一平移,以避免冲突。

查看 CodePen 上 Sarah Drasner 的 Show Order of Operations Transform Fail 笔记(@sdras)。

有一些替代方法,例如使用矩阵变换(手动编码不直观)或使用 JavaScript 动画 API,例如 GreenSock,其中没有针对多个变换插值的顺序序列。

多个 div 实现也可以帮助解决 SVG 中的麻烦错误。在 Safari 中,您无法在动画中同时声明不透明度和变换——其中一个将失败。您可以在本文的第一个演示中看到解决方法的实际操作。

在 2015 年 8 月初,独立变换声明 已迁移到 Chrome Canary。这意味着我们不必再担心顺序问题。您可以分别声明 rotatetranslatescale

DevTools 定时助手

ChromeFirefox 现在都提供了一些专门用于帮助处理动画的工具。它们提供一个用于控制速度的滑块、一个暂停按钮以及用于处理缓动值的 UI。将动画速度减慢,并在特定停止点查看动画对于调试 CSS 动画非常有用。

它们都使用 Lea Verou 的 cubic-bezier.com 可视化和 GUI。这非常有用,因为您不再需要在 cubic-bezier.com、文本编辑器和校对之间来回切换。

这些工具使我们能够以更直观的方式微调动画。以下是对这两种工具提供的 UI 的说明

Chrome 和 Firefox 都允许您控制时间安排(加快或减慢),并手动浏览动画。更高级的时间线工具 正在 Chrome 中推出,可以一次查看多个元素。这将非常有用,因为一次只能处理一个元素的动画是一个很大的限制因素。

我遇到过的一个问题是,如果动画持续时间短且速度快,则无法快速抓取元素。在这种情况下,我通常会将其设置为 animation-iteration-count: infinite;,以便我可以继续操作,而无需与时间作斗争。

我还发现将动画速度减慢,然后使用这些工具在浏览器中重新播放和调整时间安排非常有用。它允许您从根本上拆分每个动作,并查看所有内容的交互方式以及运动进度。如果您以该速度进行细化,那么当您调整它以更快地播放时,它将看起来更加精通。

使用 JavaScript 调试 CSS 动画事件

如果您想确定每个动画的确切触发位置和时间,可以使用一些 JavaScript 来检测并提醒您每个事件何时发生,方法是挂钩到 animationstartanimationiterationanimationend

以下是该演示

查看 CodePen 上 Sarah Drasner 的 Showing how to Debug Animation Play States in JavaScript 笔记(@sdras)。

保持关键帧简洁

我经常看到人们在 0% 关键帧和 100% 关键帧中声明相同的属性和值。这是不必要的,会导致代码膨胀。默认情况下,浏览器将采用属性值作为初始值和结束值。

这意味着这有点过分

.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() 缓动函数。经过微调的缓动函数与公司的调色板类似。您的运动具有自己的特定品牌和“声音”。如果您在整个网站上使用它(应该这样,以保持一致性),最简单的方法是将一个或两个缓动函数存储在变量中,就像我们对调色板所做的那样。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 关键帧进行动画制作时,我们希望尽可能多地从 GPU 获取帮助。这意味着,如果您要对多个对象进行动画制作,您需要一种简单的方法来准备 DOM 以进行即将到来的运动并对元素进行分层。您可以使用标准声明块来硬件加速本机 DOM 元素(不是 SVG)。由于我们在所有要进行动画制作的元素上都重复使用它,因此使用 mixin 或扩展来保存一些输入并添加它是有意义的

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

.foo {
  @include accelerate(transform);
}

小心。一次卸载太多元素会导致相反的效果,性能下降。大多数动画应该没问题,但如果您使用 haml 之类的工具生成大量 DOM 元素,请注意这一点。

循环以提高性能

Smashing Magazine 最近发布了一篇很棒的文章,展示了很棒的项目Species in Pieces 背后的工作。在特定的一节中,作者详细介绍了如何一次性动画所有片段会导致性能问题。他指出

想象一下,您同时移动 30 个物体;您对浏览器的要求很高,因此这会导致问题是合理的。如果您每个物体都具有 0.199 秒的速度和 0.2 秒的延迟,则可以通过一次只移动一个物体来解决问题。总移动量相同并不重要:如果动画作为链式进行,则性能会立即提高 30 倍。

您可以利用 Sass 或其他预/后处理器 for 循环来实现此类功能。以下是我编写的用于循环遍历 nth-child 的一个非常简单的循环

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

不仅如此,您还可以使用它们来交错颜色之类的视觉效果。(点击重新播放以重新运行动画。)

查看代码笔 SASS for loops in CSS Animations,作者 Sarah Drasner (@sdras) 在 CodePen 上发布。

按顺序调整多个动画

当您创建较长的动画时,对多个动画或事件进行排序的方法通常是使用渐进延迟将它们链接在一起。例如

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,因为它具有非常强大的时间线功能,但是大多数 JS 动画都允许您轻松堆叠动画,而无需任何重新计算,这绝对是一个工作流程优势。

使动画效果良好不仅是构建动画本身,通常,是编辑、细化和调试才能将项目从简单的移动转变为精心编排和高效的片段。希望这些技巧能为您提供更多工具,并能为您工作流程中的部分难点提供帮助。