专家级 CSS:CPU 黑客
“CPU Hack”意味着解锁持续处理数据和重新评估状态的能力。
例如,如果循环变量没有自动initial
在 CSS 中变为无效( )状态,这将不断增加此处的值--frame-count
:
body {
--input-frame: var(--frame-count, 0);
--frame-count: calc(var(--input-frame) + 1);
}
剧透警告:您实际上可以在 CSS 中执行此操作,而无需触及 JS,我将向您展示如何操作!
5 个可观察对象
首先,让我们对高级 CSS 动画的使用进行一些观察,以便最终的演示不会完全出乎意料。
与“ 5 个可观察量”无直接关系
1. 动画状态几乎占据主导地位
动画状态设置的属性分配胜过所有选择器状态属性分配。
hotpink
在这个例子中,body 背景始终是:body { animation: example 1s infinite; --color: blue; background: var(--color); } body:hover { --color: green; } body:has(div:hover) { --color: red; } @keyframes example { 0%, 100% { --color: hotpink; } }
这(部分)解释了为什么动画状态不允许修改控制动画的属性。例如,你不能[1]对 的值进行动画处理animation-play-state
。
(否则,一旦启动,自设置动画只能通过 JS 删除其所在元素来停止,因为动画可以设置自己的animation
值并且保持活动状态,无论其他选择器状态试图停止它。[2])
暂停的动画也不例外;暂停时的值无论是多少,仍然胜过其他状态。
[1] 从技术上讲,有一个不相关的黑客技术可以实现这一点,处理继承和无效的计算状态,但该黑客技术的动画帧计时对于 CPU 滴答来说并不可靠。
[2] 这就是《奥创》的问题所在。
2. 关键帧中的属性赋值可以使用 var()
body {
animation: example 1s infinite;
--color: blue;
}
body:hover {
--color: green;
}
body:has(div:hover) {
--color: red;
}
@keyframes example {
0%, 100% { background: var(--color); }
}
在此示例中,背景颜色blue
为默认颜色(green
悬停时,或red
悬停在 div 内时)。颜色会随着用户交互而变化。
3. --var 关键帧结果的赋值被缓存
我们可以通过在背景颜色分配中添加一些间接性来测试这一点:
body {
animation: example 1s infinite;
--color: blue;
background: var(--bg);
}
body:hover {
--color: green;
}
body:has(div:hover) {
--color: red;
}
@keyframes example {
0%, 100% { --bg: var(--color); }
}
不管怎样,背景总是blue
因为它首先被评估为blue
并且更改为--color
不会被重新计算。
即使动画paused
,当背景状态改变时,缓存的值也不会改变。
(暂停的动画使用缓存的值)
4. 更改动画属性会破坏缓存
通过在animation-duration
用户悬停时进行改变,动画缓存会被重新计算。
body {
animation: example 1s infinite;
--color: blue;
background: var(--bg);
}
body:hover {
--color: green;
animation-duration: 2s;
}
body:has(div:hover) {
--color: red;
animation-duration: 3s;
}
@keyframes example {
0%, 100% { --bg: var(--color); }
}
这里的最终结果与上面的#2完全相同;如果悬停,或者悬停在 div 内,则背景颜色是blue
默认颜色。green
red
注意:Safari 有一个错误,当动画属性发生变化时它不会重新计算缓存,因此我们进入了仅限 Chrome 的领域(Firefox 还不能使用动画 --vars)
如果我们将“更改”animation-duration
为1s
,从技术上讲它不会改变,并且缓存不会重新计算。
如果两个 :hover 状态使用与默认状态不同的相同值,您就会开始看到有趣的行为。
body {
animation-duration: 1s;
}
body:hover {
animation-duration: 2s;
}
body:has(div:hover) {
animation-duration: 2s;
}
让我们现场展示一下:
根据鼠标进入屏幕的位置(从顶部还是底部),您将获得不同的颜色,这些颜色会“锁定”到其中一种颜色,直到您将鼠标移开为止。
5. 两个动画
如果我们不通过伪选择器状态来改变值--color
,而是制作另一个动画来改变它,会怎么样?
我们的example
动画仍然--bg
基于设置--color
,因此我们可以预期它仍然具有相同的缓存行为。
改变动画的动画属性example
也应该导致它重新计算其缓存。
因此,最后,example
动画应该接受来自另一个动画的当前--color
值并将其与其状态一起缓存。
它看起来是这样的:
body {
animation: color 3s step-end infinite,
example 1s infinite;
background: var(--bg);
}
body:hover {
animation-play-state: running, paused;
}
div::after {
content: "color preview";
background: var(--color);
}
@keyframes color {
0%, 33% { --color: blue; }
33%, 67% { --color: green; }
67%, 100% { --color: red; }
}
@keyframes example {
0%, 100% { --bg: var(--color); }
}
注意:即使我们
example
在 :hover 上暂停动画,这仍然会改变默认running
状态,因此它会在相同的 CSS 绘制框架中重新计算并暂停。
感觉是这样的:
bg 会锁定到您进入时的状态,然后重新计算并重新锁定到您离开时的状态。
坚持!太棒了!
CPU 攻击开始
先前的信息暗示了一些非常有趣的事情;从动画中获取缓存值不会重新计算它,因此如果缓存值的源被移除一步,它不应该导致无效的循环状态。
双重捕获,一次计算,管理时间......应该是可能的。
我们让example
动画有条件地从正常选择器状态或另一个动画中捕获值。
让我们想象一下它捕获的是一个数字而不是一种颜色,就像--frame-count
本文开头那样。
我们将把它从 重命名example
为capture
。
body {
animation: capture 1s infinite;
--input-frame: 0;
--frame-count: calc(var(--input-frame) + 1);
}
@keyframes capture {
0%, 100% { --frame-captured: var(--frame-count); }
}
--input-frame
如果我们能够将其设置为该值那不是很好吗--frame-captured
?
我们知道直接执行此操作会循环,因为所有这 3 个分配都存在于同一框架中:
--input-frame
= --frame-captured
--frame-count
= --input-frame
+ 1 --frame-captured
=--frame-count
如果我们捕获捕获的值并确保两个捕获不同时运行,则捕获-捕获可以将该值提升回--input-frame
...
我们来试试吧。我们将捕获的调用称为“capture” hoist
。
此外,由于我们不希望它们同时运行(因为那肯定会是循环的),所以paused
为了安全起见,让我们先启动它们。
body {
animation: hoist 1ms infinite,
capture 1ms infinite;
animation-play-state: paused, paused;
--input-frame: var(--frame-hoist, 0);
--frame-count: calc(var(--input-frame) + 1);
}
body::after {
counter-reset: frame var(--frame-count);
content: "--frame-count: " counter(frame);
}
@keyframes hoist {
0%, 100% { --frame-hoist: var(--frame-captured, 0); }
}
@keyframes capture {
0%, 100% { --frame-captured: var(--frame-count); }
}
现在,为了测试这一点,我们还需要设置一些 DOM,以便我们能够按特定顺序悬停,从而animation-play-state
以正确的顺序触发。元素之间没有间隙,我们会给它们添加类名phase-0
等等。
第一阶段肯定是捕获原始输出。所以我们先hoist
暂停,让我们的老朋友capture
先运行:
body:has(.phase-0:hover) {
animation-play-state: paused, running;
}
我们可以停止悬停该元素以暂停两者,这将捕获--frame-count
,或者我们可以继续设置另一个元素来明确执行此操作:
body:has(.phase-1:hover) {
animation-play-state: paused, paused;
}
最后呢?关键时刻,测试一下我们是否可以在暂停hoist
时运行capture
,这样我们就能有足够的空间来避免循环依赖,并将输出放回到顶部作为输入……这应该能给我们第一个2
body:has(.phase-2:hover) {
animation-play-state: running, paused;
}
这是实时的:将光标从上到下悬停以完成循环:
CPU 黑客
尤里卡!
我们可以让用户整天用光标抚摸 dom,或者我们可以在需要自动触发 :hover 时将 dom 移动到光标下方
让我们弄清楚吧!
我们需要悬停.phase-0
以自动“转到” .phase-1
,然后悬停将“转到” .phase-2
......
然后悬停.phase-2
需要返回到某个paused, paused
状态以避免任何单帧同时对两个动画进行计算。
请记住:播放或暂停动画会导致其在该帧上重新计算,因此从
running, paused
直线变为直线paused, running
实际上是在一帧中同时运行两者。
所以我们需要“跳转到”一个状态,然后它会“跳转到” .phase-0
。由于.phase-1
是paused, paused
且“跳转到”2,我们将复制它,并使新的状态.phase-3
也暂停,但改为“跳转到”0。
让我们将这个 CSS 添加到我们已有的内容中:
body:has(.phase-3:hover) {
animation-play-state: paused, paused;
}
我们将在 HTML 中使用它:
<ol class="cpu">
<li class="phase-0"></li>
<li class="phase-1"></li>
<li class="phase-2"></li>
<li class="phase-3"></li>
</ol>
如果感兴趣的话,这里是每个阶段的回顾
-
.phase-0
(hoist
暂停,capture
正在运行)- 提升值已冻结
- 分配捕获=输出值
- 转到
.phase-1
-
.phase-1
(hoist
暂停,capture
暂停)- 提升值已冻结
- 分配捕获值 = 输出值
- 冻结捕获(在此 css 绘制框架的末尾)
- 转到
.phase-2
-
.phase-2
(hoist
正在运行,capture
暂停)- 捕获值被冻结
- 分配提升值 = 捕获值
- 转到
.phase-3
-
.phase-3
(hoist
暂停,capture
暂停)- 捕获值被冻结
- 分配提升值 = 捕获值
- 冻结提升(在此 css 绘制框架的末尾)
- 转到
.phase-0
接下来,我们将设计这个.cpu
元素的样式,以便当它的宽度变为时,它的每个子元素都占据其整个区域100%
,并按照 dom 顺序在 z 方向上堆叠在一起。
.cpu { position: relative; list-style: none; }
.cpu > * {
position: absolute;
inset: 0px;
width: 0px;
}
.cpu > .phase-0 { width: 100%; }
.cpu > .phase-0:hover + .phase-1 { width: 100%; }
.cpu > .phase-1:hover + .phase-2 { width: 100%; }
.cpu > .phase-2:hover + .phase-3 { width: 100%; }
这应该是最终成品了;每个阶段都会触发下一个阶段,并且每个阶段只会触发一个 CSS Paint Frame。让我们现场看看吧!
注意:我们还需要注册输出变量 (
--frame-count
),否则它会在 100 时突然停止工作,因为calc()
每次迭代都会在技术上嵌套。将其强制转换为整数可以避免这种情况,并且效率更高。上面的演示中包含@property
了代码。
另外,从技术上讲,您可以进行一项小的清理:
直接删除
--input-frame
var,--frame-hoist
这样更干净。
猫头鹰的其余部分
那么,CSS 中有一个 CPU。你能用它做什么?
100% CSS 康威生命游戏模拟器 - 无限世代,42x42
100% CSS Breakout,在这里播放:
完结!
如果你觉得这篇文章有用、有趣,或者很有意思,那我闲暇时也会做!所以,请考虑在这里、CodePen和X 上关注我!
👽💜
// Jane Ori
PS:最近被裁员了,正在找工作!
https://linkedin.com/in/JaneOri
拥有超过 13 年的全栈(主要是 JS)工程工作和咨询经验,为合适的机会做好准备!
鏂囩珷鏉ユ簮锛�https://dev.to/janeori/expert-css-the-cpu-hack-4ddj