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

160 阅读9分钟

CSS-only Wolfenstein是我几周前做的一个小项目。它是一个关于CSS 3D变换和动画的实验。

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

CodePen嵌入回退

编辑:这个游戏故意需要一些快速反应,以避免游戏结束的画面。

这里有一个玩法的视频。

简而言之,我的项目只不过是一个精心编写的长CSS动画。再加上几个复选框黑客的实例。

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

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

然而,有两个问题是特别难解决的:

  • 每当玩家点击一个敌人,就播放 "武器发射 "动画。
  • 当快速移动的老板得到最后一击时,进入一个戏剧性的慢动作。

在技术层面上,这意味着:

  • 当下一个复选框被选中时重放一个动画。
  • 当一个复选框被选中时,放慢一个动画。

事实上,在我的项目中,这两个问题都没有得到适当的解决!我最后要么使用了变通的方法。我要么最终使用变通方法,要么就放弃了。

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

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

让我来"抛砖引玉"。

问题1:重放动画

第一个例子:"只是另一个复选框"

我的第一直觉是 "只是再加一个复选框",这并不可行。

CodePen嵌入回退

每个复选框都单独工作,但不是两个一起工作。如果一个复选框已经被选中,另一个就不再工作了。

下面是它如何工作(或 "不工作"):

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

CodePen Embed Fallback

下面是它的工作原理:

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

CodePen嵌入回退

这与前面的例子类似,你实际上可以理解这种方式的行为。

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

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

CodePen嵌入回退

如果你想知道为什么会出现这种情况,这里有一些线索:

  • 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; }

的确,这很有效。

CodePen嵌入回退

...或者是吗?

通过一些调整,应该更容易看到这个问题。

CodePen Embed Fallback

是的,动画被放慢了。而且,它看起来并不好看。当你拨动复选框时,狗(几乎)总是 "跳 "起来。此外,狗似乎跳到一个随机的位置,而不是最初的位置。怎么会这样?

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

CodePen嵌入Fallback

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

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

当动画运行时,对动画属性值的改变就像动画开始时就有这些值一样适用。

这遵循了无状态设计,这使得浏览器可以很容易地确定动画的值。实际的计算方法在这里这里都有描述。

另一种尝试

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

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

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

这样就可以了。

CodePen 嵌入回退

...或者是吗?

当你点击 "Slowmo!"时,它确实慢下来了。但如果你等待一整圈,你会看到一个 "跳跃"。事实上,它总是跳到点击 "Slowmo!"时的位置。

原因是我们没有定义一个from 关键帧--我们也不应该这样做。当用户点击 "Slowmo!"时,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);
  }
}

CodePen嵌入回退

注意:在这个例子中使用了@property ,这不是所有的浏览器都支持的

"完美 "的解决方案

前面的解决方案有一个注意事项:"退出慢动作 "的效果并不好。

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

CodePen Embed Fallback

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

这个解决方案的工作原理就像 "换挡 "的 "齿轮":

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

虽然我非常喜欢这个结果,但有一个问题:这是对spin 动画的一个很好的利用,这对其他类型的动画一般都不适用。

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

对于一般的动画,用一点JavaScript就可以实现慢动作的功能。

CodePen Embed Fallback

一个快速的解释:

  • 一个自定义的属性被用来跟踪动画的进展。
  • 当复选框被切换时,动画会被 "重新启动"。
  • JS代码计算出正确的animation-delay ,以确保无缝过渡。如果你不熟悉animation-delay 的负值,我推荐这篇文章

你可以把这个解决方案看作是 "重启动画 "和 "换挡 "方法的混合。

在这里,正确跟踪动画的进展是很重要的。如果没有@property ,也可以采取变通的办法。作为一个例子,这个版本使用z-index 来跟踪进度。

CodePen嵌入回退

题外话:最初,我也试图创建一个纯CSS的版本,但没有成功。虽然不是100%确定,但我认为这是因为animation-delay不可动画的

这里是一个使用最小JavaScript的版本。只有 "进入慢动作 "可以使用。

CodePen 嵌入回退

如果你能创建一个只用CSS的版本,请让我知道。

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

最后,我想分享一个解决方案,它适用于(几乎)任何动画,甚至有多个复杂的@keyframes

CodePen 嵌入回退

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

比如说:

  • animation-timing-function is not .linear
  • animation-direction 不是 。normal
  • animation-name 中的多个值,有不同的animation-duration's 和animation-delay's。

这里也描述了网络动画API的这个方法。

鸣谢

我是在遇到只用CSS的项目后开始走上这条路的。有些是精致的艺术品,有些是复杂的装置。我最喜欢的是那些涉及三维物体的项目,例如这个弹跳球和这个包装立方体

一开始,我不知道这些是怎么做出来的。后来我阅读了安娜-图德的这个不错的教程,并从中学到了很多。

结果发现,用CSS制作3D物体并为其制作动画,与用Blender制作并无太大区别,只是味道有些不同。

总结

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

我希望你觉得这篇文章很有趣。请让我知道你的想法!