CSS-only Wolfenstein是我几周前做的一个小项目。它是一个关于CSS 3D变换和动画的实验。
受FPS演示和另一个Wolfenstein CodePen的启发,我决定建立我自己的版本。它松散地基于原始Wolfenstein 3D游戏的第1集-第9层。
CodePen嵌入回退
编辑:这个游戏故意需要一些快速反应,以避免游戏结束的画面。
这里有一个玩法的视频。
简而言之,我的项目只不过是一个精心编写的长CSS动画。再加上几个复选框黑客的实例。
:checked ~ div { animation-name: spin; }
环境由3D网格面组成,动画主要是普通的3D平移和旋转。没有什么真正的花哨。
然而,有两个问题是特别难解决的:
- 每当玩家点击一个敌人,就播放 "武器发射 "动画。
- 当快速移动的老板得到最后一击时,进入一个戏剧性的慢动作。
在技术层面上,这意味着:
- 当下一个复选框被选中时重放一个动画。
- 当一个复选框被选中时,放慢一个动画。
事实上,在我的项目中,这两个问题都没有得到适当的解决!我最后要么使用了变通的方法。我要么最终使用变通方法,要么就放弃了。
另一方面,经过一番挖掘,最终我找到了解决这两个问题的关键:改变运行中的CSS动画的属性。在这篇文章中,我们将进一步探讨这个话题:
- 大量的交互式例子
- 剖析:每个例子是如何工作(或不工作)的?
- 幕后:浏览器是如何处理动画状态的?
让我来"抛砖引玉"。
问题1:重放动画
第一个例子:"只是另一个复选框"
我的第一直觉是 "只是再加一个复选框",这并不可行。
CodePen嵌入回退
每个复选框都单独工作,但不是两个一起工作。如果一个复选框已经被选中,另一个就不再工作了。
下面是它如何工作(或 "不工作"):
<div>的animation-name,默认情况下是none。- 用户点击一个复选框,
animation-name变成spin,动画从头开始。 - 过了一会儿,用户又点击了另一个复选框。一个新的CSS规则生效了,但
animation-name仍然是spin,这意味着没有动画被添加或删除。动画只是继续播放,好像什么都没有发生。
第二个例子:"克隆动画"
一种可行的方法是克隆动画:
#spin1:checked ~ div { animation-name: spin1; }
#spin2:checked ~ div { animation-name: spin2; }
CodePen Embed Fallback
下面是它的工作原理:
animation-name是 最初。none- 用户点击 "旋转!",
animation-name变成spin1。动画spin1从头开始,因为它是刚刚添加的。 - 用户点击 "再次旋转!",
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-mode 是forwards ,这一招就不起作用了。 这里有一个演示。
CodePen嵌入回退
如果你想知道为什么会出现这种情况,这里有一些线索:
animation-fill-mode是 ,默认情况下,这意味着 "如果不播放动画,则完全没有效果"。noneanimation-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-functionis not .linearanimation-direction不是 。normalanimation-name中的多个值,有不同的animation-duration's 和animation-delay's。
鸣谢
我是在遇到只用CSS的项目后开始走上这条路的。有些是精致的艺术品,有些是复杂的装置。我最喜欢的是那些涉及三维物体的项目,例如这个弹跳球和这个包装立方体。
一开始,我不知道这些是怎么做出来的。后来我阅读了安娜-图德的这个不错的教程,并从中学到了很多。
结果发现,用CSS制作3D物体并为其制作动画,与用Blender制作并无太大区别,只是味道有些不同。
总结
在这篇文章中,我们研究了animate-* 属性被改变时的CSS动画行为。我们特别研究了 "重放动画 "和 "慢放动画 "的解决方案。
我希望你觉得这篇文章很有趣。请让我知道你的想法!