专家级 CSS:CPU 黑客

2025-06-09

专家级 CSS:CPU 黑客

“CPU Hack”意味着解锁持续处理数据和重新评估状态的能力。

例如,如果循环变量没有自动initial在 CSS 中变为无效( )状态,这将不断增加此处的值--frame-count

body {
  --input-frame: var(--frame-count, 0);
  --frame-count: calc(var(--input-frame) + 1);
}
Enter fullscreen mode Exit fullscreen mode

剧透警告:您实际上可以在 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); }
}
Enter fullscreen mode Exit fullscreen mode

在此示例中,背景颜色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); }
}
Enter fullscreen mode Exit fullscreen mode

不管怎样,背景总是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); }
}
Enter fullscreen mode Exit fullscreen mode

这里的最终结果与上面的#2完全相同;如果悬停,或者悬停在 div 内,则背景颜色是blue默认颜色。greenred

注意:Safari 有一个错误,当动画属性发生变化时它不会重新计算缓存,因此我们进入了仅限 Chrome 的领域(Firefox 还不能使用动画 --vars)

如果我们将“更改”animation-duration1s,从技术上讲它不会改变,并且缓存不会重新计算。

如果两个 :hover 状态使用与默认状态不同的相同值,您就会开始看到有趣的行为。

body {
  animation-duration: 1s;
}
body:hover {
  animation-duration: 2s;
}
body:has(div:hover) {
  animation-duration: 2s;
}
Enter fullscreen mode Exit fullscreen mode

让我们现场展示一下:

根据鼠标进入屏幕的位置(从顶部还是底部),您将获得不同的颜色,这些颜色会“锁定”到其中一种颜色,直到您将鼠标移开为止。


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); }
}
Enter fullscreen mode Exit fullscreen mode

注意:即使我们example在 :hover 上暂停动画,这仍然会改变默认running状态,因此它会在相同的 CSS 绘制框架中重新计算并暂停。

感觉是这样的:

bg 会锁定到您进入时的状态,然后重新计算并重新锁定到您离开时的状态。

坚持!太棒了!


CPU 攻击开始

先前的信息暗示了一些非常有趣的事情;从动画中获取缓存值不会重新计算它,因此如果缓存值的源被移除一步,它不应该导致无效的循环状态。

双重捕获,一次计算,管理时间......应该是可能的。

我们让example动画有条件地从正常选择器状态或另一个动画中捕获值。

让我们想象一下它捕获的是一个数字而不是一种颜色,就像--frame-count本文开头那样。

我们将把它从 重命名examplecapture

body {
  animation: capture 1s infinite;

  --input-frame: 0;
  --frame-count: calc(var(--input-frame) + 1);
}
@keyframes capture {
  0%, 100% { --frame-captured: var(--frame-count); }
}
Enter fullscreen mode Exit fullscreen mode

--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); }
}
Enter fullscreen mode Exit fullscreen mode

现在,为了测试这一点,我们还需要设置一些 DOM,以便我们能够按特定顺序悬停,从而animation-play-state以正确的顺序触发。元素之间没有间隙,我们会给它们添加类名phase-0等等。

第一阶段肯定是捕获原始输出。所以我们先hoist暂停,让我们的老朋友capture先运行:

body:has(.phase-0:hover) {
  animation-play-state: paused, running;
}
Enter fullscreen mode Exit fullscreen mode

我们可以停止悬停该元素以暂停两者,这将捕获--frame-count,或者我们可以继续设置另一个元素来明确执行此操作:

body:has(.phase-1:hover) {
  animation-play-state: paused, paused;
}
Enter fullscreen mode Exit fullscreen mode

最后呢?关键时刻,测试一下我们是否可以在暂停hoist时运行capture,这样我们就能有足够的空间来避免循环依赖,并将输出放回到顶部作为输入……这应该能给我们第一个2

body:has(.phase-2:hover) {
  animation-play-state: running, paused;
}
Enter fullscreen mode Exit fullscreen mode

这是实时的:将光标从上到下悬停以完成循环:

CPU 黑客

尤里卡!

我们可以让用户整天用光标抚摸 dom,或者我们可以在需要自动触发 :hover 时将 dom 移动到光标下方

让我们弄清楚吧!

我们需要悬停.phase-0以自动“转到” .phase-1,然后悬停将“转到” .phase-2......

然后悬停.phase-2需要返回到某个paused, paused状态以避免任何单帧同时对两个动画进行计算。

请记住:播放或暂停动画会导致其在该帧上重新计算,因此从running, paused直线变为直线paused, running实际上是在一帧中同时运行两者。

所以我们需要“跳转到”一个状态,然后它会“跳转到” .phase-0。由于.phase-1paused, paused且“跳转到”2,我们将复制它,并使新的状态.phase-3也暂停,但改为“跳转到”0。

让我们将这个 CSS 添加到我们已有的内容中:

body:has(.phase-3:hover) {
  animation-play-state: paused, paused;
}
Enter fullscreen mode Exit fullscreen mode

我们将在 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>
Enter fullscreen mode Exit fullscreen mode

如果感兴趣的话,这里是每个阶段的回顾
  • .phase-0hoist暂停,capture正在运行)

    1. 提升值已冻结
    2. 分配捕获=输出值
    3. 转到.phase-1
  • .phase-1hoist暂停,capture暂停)

    1. 提升值已冻结
    2. 分配捕获值 = 输出值
    3. 冻结捕获(在此 css 绘制框架的末尾)
    4. 转到.phase-2
  • .phase-2hoist正在运行,capture暂停)

    1. 捕获值被冻结
    2. 分配提升值 = 捕获值
    3. 转到.phase-3
  • .phase-3hoist暂停,capture暂停)

    1. 捕获值被冻结
    2. 分配提升值 = 捕获值
    3. 冻结提升(在此 css 绘制框架的末尾)
    4. 转到.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%; }
Enter fullscreen mode Exit fullscreen mode

这应该是最终成品了;每个阶段都会触发下一个阶段,并且每个阶段只会触发一个 CSS Paint Frame。让我们现场看看吧!

注意:我们还需要注册输出变量 ( --frame-count),否则它会在 100 时突然停止工作,因为calc()每次迭代都会在技术上嵌套。将其强制转换为整数可以避免这种情况,并且效率更高。上面的演示中包含@property了代码。

另外,从技术上讲,您可以进行一项小的清理:

直接删除--input-framevar,--frame-hoist这样更干净。


猫头鹰的其余部分

那么,CSS 中有一个 CPU。你能用它做什么?

100% CSS 计算整数 --屏幕宽度和 --屏幕高度

100% CSS 图像鼠标坐标悬停时缩放

100% CSS 康威生命游戏模拟器 - 无限世代,42x42

100% CSS Breakout,在这里播放:


完结!

如果你觉得这篇文章有用、有趣,或者很有意思,那我闲暇时也会做!所以,请考虑在这里、CodePenX 上关注我!

👽💜
// Jane Ori


PS:最近被裁员了,正在找工作!

https://linkedin.com/in/JaneOri

拥有超过 13 年的全栈(主要是 JS)工程工作和咨询经验,为合适的机会做好准备!

鏂囩珷鏉ユ簮锛�https://dev.to/janeori/expert-css-the-cpu-hack-4ddj
PREV
在 Ubuntu 上安装 JetBrains ToolBox 系统要求 安装 Jetbrains Toolbox 配置 JetBrains Toolbox 和安装应用程序 如何卸载 Jetbrains Toolbox
NEXT
CSS 类型转换为数字:tan(atan2()) 标量