黑客攻击 CSS 动画状态和播放时间

Avatar of Lu Wang
Lu Wang

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 价值 200 美元的免费积分!

纯 CSS 狼人 是我几周前完成的一个小项目。 它是一个使用 CSS 3D 变换和动画的实验。

FPS 演示 和另一个 Wolfenstein CodePen 的启发,我决定构建自己的版本。 它松散地基于最初的 Wolfenstein 3D 游戏的第 1 集 - 第 9 层。

编辑:此游戏故意需要一些快速反应才能避免游戏结束屏幕。

这是一个通关视频

简而言之,我的项目不过是一个精心编排的 CSS 长动画。 另外还有一些 复选框技巧 实例。

:checked ~ div { animation-name: spin; }

环境由 3D 网格面组成,动画主要是简单的 3D 平移和旋转。 没什么真正花哨的。

然而,有两个问题特别棘手,难以解决

  • 每当玩家点击敌人时,播放“武器射击”动画。
  • 当快速移动的 Boss 受到最后一击时,进入戏剧性的慢动作。

从技术层面来说,这意味着

  • 当下一个复选框被选中时,重新播放动画。
  • 当选中复选框时,减慢动画速度。

事实上,我的项目中这两个问题都没有得到真正的解决! 我最终要么使用变通方法,要么干脆放弃。

另一方面,经过一番挖掘,我最终找到了这两个问题的关键:改变正在运行的 CSS 动画的属性。 在本文中,我们将进一步探讨这个主题

  • 很多交互式示例。
  • 剖析:每个示例是如何工作的(或不工作的)?
  • 幕后:浏览器如何处理动画状态?

让我 “扔砖头”

问题 1:重新播放动画

第一个例子:“只是一个复选框”

我的第一直觉是“再加一个复选框”,但行不通

每个复选框都可以单独工作,但不能同时工作。 如果一个复选框已经被选中,另一个就失效了。

这是它如何工作(或“不工作”)的

  1. <div>animation-name 默认情况下是 none
  2. 用户点击一个复选框,animation-name 变成 spin,动画从头开始。
  3. 一段时间后,用户点击另一个复选框。 一个新的 CSS 规则生效,但 animation-name 仍然是 spin,这意味着没有添加或删除动画。 动画简单地继续播放,就像什么都没发生一样。

第二个例子:“克隆动画”

一种有效的方法是克隆动画

#spin1:checked ~ div { animation-name: spin1; }
#spin2:checked ~ div { animation-name: spin2; }

这是它如何工作的

  1. animation-name 最初是 none
  2. 用户点击“旋转!”,animation-name 变成 spin1。 动画 spin1 从头开始,因为它刚刚被添加。
  3. 用户点击“再次旋转!”,animation-name 变成 spin2。 动画 spin2 从头开始,因为它刚刚被添加。

请注意,在步骤 #3 中,由于 CSS 规则的顺序,spin1 被删除。 如果“再次旋转!”首先被选中,它将不起作用。

第三个例子:“追加相同的动画”

另一种有效的方法是“追加相同的动画”

#spin1:checked ~ div { animation-name: spin; }
#spin2:checked ~ div { animation-name: spin, spin; }

这与前面的例子类似。 你实际上可以这样理解这个行为

#spin1:checked ~ div { animation-name: spin1; }
#spin2:checked ~ div { animation-name: spin2, spin1; }

请注意,当选中“再次旋转!”时,旧的正在运行的动画成为新列表中的第二个动画,这可能不直观。 一个直接的结果是:如果 animation-fill-modeforwards,这个技巧将不起作用。 这里有一个演示

如果你想知道为什么会出现这种情况,以下是一些线索

  • animation-fill-mode 默认情况下是 none,这意味着“如果动画没有播放,则没有任何效果”。
  • animation-fill-mode: forwards; 意味着“动画播放完毕后,必须永远停留在最后一个关键帧上”。
  • spin1 的决定始终覆盖 spin2 的决定,因为 spin1 出现在列表中靠后。
  • 假设用户点击“旋转!”,等待一个完整的旋转,然后点击“再次旋转!”。 此时。 spin1 已经完成,而 spin2 刚刚开始。

讨论

经验法则:你不能“重新开始”现有的 CSS 动画。 相反,你想添加并播放一个新的动画。 这可能被 W3C 规范 确认

一旦动画开始,它将持续到结束或动画名称被删除为止。

现在比较最后两个例子,我认为在实践中,“克隆动画”通常效果更好,尤其是在可以使用 CSS 预处理器的情况下。

问题 2:慢动作

有人可能会认为,减慢动画速度只是设置更长的 animation-duration 的问题

div { animation-duration: 0.5s; }
#slowmo:checked ~ div { animation-duration: 1.5s; }

的确,这行得通

…… 还是不行?

稍微调整一下,应该更容易看到问题。

是的,动画速度减慢了。 而且,看起来不好。 只要你切换复选框,那只狗(几乎)总是会“跳”。 此外,那只狗似乎跳到随机位置,而不是初始位置。 为什么会这样?

如果我们引入两个“阴影元素”,会更容易理解

两个阴影元素都运行相同的动画,但 animation-duration 不同。 而且它们不受复选框的影响。

当您切换复选框时,元素只是立即在两个阴影元素的状态之间切换。

引用 W3C 规范

动画运行期间对动画属性值的更改将应用于动画开始时的这些值。

这遵循 无状态 设计,它允许浏览器轻松确定动画值。 实际计算在 这里这里 进行描述。

另一种尝试

一个想法是暂停当前动画,然后添加一个更慢的动画,从那里接管

div {
  animation-name: spin1;
  animation-duration: 2s;
}

#slowmo:checked ~ div {
  animation-name: spin1, spin2;
  animation-duration: 2s, 5s;
  animation-play-state: paused, running;
}

所以它有效

…… 还是不行?

当您点击“慢动作!”时,它确实会减慢速度。 但是,如果你等待一个完整的圆圈,你会看到一个“跳跃”。 事实上,它总是跳到点击“慢动作!”时所处的位置。

原因是我们没有定义 from 关键帧——而且我们不应该定义。 当用户点击“慢动作!”时,spin1 在某个位置暂停,而 spin2 从完全相同的位置开始。 我们无法事先预测那个位置…… 或者可以?

一个有效的解决方案

我们可以! 通过使用自定义属性,我们可以捕获第一个动画中的角度,然后将其传递给第二个动画

div {
  transform: rotate(var(--angle1));
  animation-name: spin1;
  animation-duration: 2s;
}

#slowmo:checked ~ div {
  transform: rotate(var(--angle2));
  animation-name: spin1, spin2;
  animation-duration: 2s, 5s;
  animation-play-state: paused, running;
}

@keyframes spin1 {
  to {
    --angle1: 360deg;
  }
}

@keyframes spin2 {
  from {
    --angle2: var(--angle1);
  }
  to {
    --angle2: calc(var(--angle1) + 360deg);
  }
}

注意:本例中使用了 @property,它 并非所有浏览器都支持

“完美”的解决方案

前一个解决方案有一个缺陷:“退出慢动作”效果不好。

这里有一个更好的解决方案

在这个版本中,慢动作可以无缝地进入或退出。也不使用任何实验性功能。那么它是一个完美的解决方案吗? 是也不是。

这个解决方案类似于“换挡”

  • 档位:有两个<div>。一个是另一个的父元素。两者都有spin动画,但animation-duration不同。元素的最终状态是两个动画的累加。
  • 换档:开始时,只有一个<div>的动画正在运行。另一个是暂停的。当复选框切换时,两个动画会交换它们的状态。

虽然我非常喜欢这个结果,但它有一个问题:它是spin动画的一个巧妙的利用,对于其他类型的动画来说,它通常是行不通的。

一个实用的解决方案(使用 JS)

对于一般的动画,可以使用一些 JavaScript 来实现慢动作功能。

快速解释一下

  • 使用自定义属性来跟踪动画进度。
  • 当复选框切换时,“重新开始”动画。
  • JS 代码计算出正确的animation-delay,以确保无缝过渡。如果你不熟悉animation-delay的负值,我推荐阅读这篇文章

你可以将这个解决方案看作是“重新开始动画”和“换挡”方法的混合。

这里重要的是正确跟踪动画进度。如果@property不可用,可以采用变通方法。例如,这个版本使用z-index来跟踪进度。

旁注:最初,我也尝试过创建一个纯 CSS 版本,但没有成功。虽然不能百分百确定,但我认为这是因为animation-delay不可动画的

这是一个使用最少 JavaScript 的版本。只有“进入慢动作”有效。

如果你能够创建一个可工作的纯 CSS 版本,请告诉我!

任何动画的慢动作(使用 JS)

最后,我想分享一个适用于(几乎)任何动画的解决方案,即使是包含多个复杂@keyframes的动画。

基本上,你需要添加一个动画进度跟踪器,然后仔细计算新动画的animation-delay。但是,有时获取正确的值可能很棘手(但可行)。

例如

  • animation-timing-function不是linear
  • animation-direction不是normal
  • animation-name中有多个值,它们具有不同的animation-durationanimation-delay

这种方法在这里也进行了描述,用于Web Animations API

致谢

我在遇到纯 CSS 项目后开始走上这条路。有些是精致的艺术品,有些是复杂的装置。我最喜欢的那些涉及 3D 对象,例如这个弹跳球和这个装箱立方体

一开始,我完全不知道这些是如何制作的。后来,我从Ana Tudor 的这篇很棒的教程中学到了很多。

事实证明,使用 CSS 构建和动画化 3D 对象与使用Blender没有什么不同,只是味道略有不同。

结论

在这篇文章中,我们研究了当animate-*属性改变时 CSS 动画的行为。特别是,我们研究了“重新播放动画”和“动画慢动作”的解决方案。

希望你发现这篇文章很有趣。请告诉我你的想法!