Эксперт CSS: Взлом процессора
«Взлом ЦП» подразумевает разблокирование возможности непрерывной обработки данных и переоценки состояния.
Например, если бы циклические переменные автоматически не переходили в недопустимое (initial
) состояние в CSS, это будет постоянно увеличивать значение --frame-count
здесь:
body {
--input-frame: var(--frame-count, 0);
--frame-count: calc(var(--input-frame) + 1);
}
Спойлер: вы действительно можете сделать это с помощью CSS, даже не прикасаясь к JS, я покажу вам, как!
5 Наблюдений
Во-первых, давайте проведем несколько наблюдений за использованием расширенной CSS-анимации, чтобы окончательная демонстрация не была совершенно неожиданной.
1. Правила состояния анимации для всех (почти)
Назначения свойств, заданные состоянием анимации, превосходят все назначения свойств состояния селектора.
В этом примере фон тела всегда hotpink
:
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; }
}
Именно поэтому (частично) состояние анимации не позволяет изменять свойства, управляющие анимацией. Например, вы не можете анимировать значение animation-play-state
.
В противном случае после запуска самонастраивающуюся анимацию можно было бы остановить только путем удаления JS элемента, в котором она находится, поскольку анимация могла бы установить свое собственное значение animation
и остаться в живых независимо от того, какие другие состояния селектора пытались ее остановить.
Приостановленная анимация не является исключением; какое бы значение ни было во время паузы, оно все равно превосходит другие состояния.
2. При назначении свойств в Keyframe можно использовать 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 Назначения результатов Keyframe кэшируются
Мы можем проверить это, добавив небольшую косвенность к назначению цвета фона:
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
при :hover
(наведения) пользователя, кэш анимации пересчитывается.
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 выше; цвет фона по умолчанию blue
, green
при наведении или red
при наведении курсора на элемент div
.
Примечание. В Safari есть ошибка, из-за которой он НЕ пересчитывает кэш при изменении свойства анимации, поэтому мы вошли на территорию только Chrome (Firefox пока не может анимировать --vars
)
Если бы мы «изменили» animation-duration
на 1с
, технически она не изменилась бы, и кеш не будет пересчитываться.
Вы начнете получать интересное поведение, если оба состояния :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
анимации при наведении курсора мыши, это по-прежнему представляет собой изменение состоянияrunning
по умолчанию, поэтому оно повторно вычисляет и приостанавливает работу в том же кадре рисования CSS.
и вот на что это похоже:
Фон фиксируется на том, что было, когда вы вошли, затем пересчитывается и снова фиксируется на том, что было, когда вы уходите.
Взлом процессора начинается
Предыдущая информация подразумевает нечто чрезвычайно интересное; получение кэшированного значения из анимации не приводит к его повторному вычислению, поэтому оно не должно вызывать недопустимое циклическое состояние, если источник кэшированного значения удален на один шаг.
Двойной захват, один раз вычисление, управление временем... Должно быть возможно.
У нас есть 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
?
Мы знаем, что выполнение этого напрямую будет циклическим, поскольку все три назначения существуют в одном кадре:
--input-frame
= --frame-captured
--frame-count
= --input-frame
+ 1--frame-captured
= --frame-count
Если мы захватим захваченное значение и убедимся, что оба захвата не выполняются одновременно, захват-захват может поднять это значение обратно в --input-frame
...
Давайте попробуем. Мы вызовем захваченный захватный 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;
}
Вот это в реальном времени: наведите курсор сверху вниз, чтобы завершить цикл:
Взлом процессора
Мы могли бы заставить пользователя гладить dom своим курсором весь день или мы могли бы переместить dom под курсор в тот момент, когда это необходимо, чтобы автоматически вызвать :hover
Давайте разберемся в этом!
Нам понадобится навести курсор на .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
работает)
- Hoist значение заморожено
- Назначить Captured = Выходное значение
- Перейти к
.phase-1
.phase-1
(hoist
приостановлен,capture
приостановлен)
- Hoist значение заморожено
- Назначить Captured = Выходное значение
- Зафиксировано Captured (в конце этого кадра рисования CSS)
- Перейти к
.phase-2
.phase-2
(hoist
работает,capture
приостановлен)
- Captured значение заморожено
- Назначить Hoist = Captured значение
- Перейти к
.phase-3
.phase-2
(hoist
приостановлен,capture
приостановлен)
- Captured значение заморожено
- Назначить Hoist = Captured значение
- Зафиксировано Hoist (в конце этого кадра рисования CSS)
- Перейти к
.phase-0
Далее мы стилизуем элемент .cpu
, чтобы каждый из его дочерних элементов занимал всю его площадь, когда их ширина становится 100%
, располагаясь друг над другом по оси Z в порядке dom.
.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. Давайте посмотрим это вживую!
Примечание: нам также необходимо зарегистрировать выходную переменную (--frame-count
), иначе она внезапно перестанет работать при значении100
из-за того, что функцияcalc()
технически становится вложенной на каждой итерации. Приведение его к целому числу предотвращает это и является гораздо более эффективным. В приведенной выше демонстрации включен код@property
.
Кроме того, технически вы можете сделать одну небольшую очистку:
Отбросьте переменную--input-frame
, просто--frame-hoist
напрямую, так будет чище.
Остальная часть вопроса
Итак, у вас есть процессор в CSS. Что вы можете сделать с этим?
100% CSS Compute Integer --width and --height of the Screen
100% CSS Image-Mouse-Coordinate Zoom on Hover
100% CSS Conway's Game of Life Simulator - Infinite Generations, 42x42
100% CSS Breakout, играйте здесь: