CSSはこんなにも進化していたのか! CSSのかなり高度なテクニック -Expert CSS: The CPU Hack
Post on:2023年9月28日
当ブログの記事でも、JavaScriptで実装していたのがCSSで実装できるようになりました、と今までにいくつか紹介してきましたが、今回紹介するのはそれらとはかなり異なるCSSのかなり高度なテクニックです。
CSSでCPUのようにデータを継続的に解析し、状態を再評価します。簡単に言うと、スクリーンの高さや幅を取得したり、マウスの座標を取得したり、さらにはブロック崩しなどのゲームもCSSのみで実装できます。
もう私が知っているCSSをはるかに超えたCSSです。
Expert CSS: The CPU Hack
br Jane Ori (@Jane0ri)
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
CSSのエキスパート: The CPU Hack
「The CPU Hack」とは、継続的にデータを解析し、状態を再評価する機能を解除することを意味します。
たとえば、CSSで循環変数が初期状態(initial
)に自動的にならなかった場合、--frame-count
の値は継続的に増加されることになります。
1 2 3 4 |
body { --input-frame: var(--frame-count, 0); --frame-count: calc(var(--input-frame) + 1); } |
ネタバレ注意: JavaScriptを使用せずに、CSSでこういったことができます! そのやり方を解説します。
5つの観測可能性
最終的なデモがまったくの予想外にならないように、まずは高度なアニメーションの使用方法についていくつかの観測事項を紹介しておきたいと思います。
1. アニメーションの状態が(ほぼ)すべてを支配する
アニメーションの状態によって設定されるプロパティの割り当ては、すべてのセレクタの状態プロパティの割り当てに優先されます。
下記のCSSでは、body
の背景は常にhotpink
です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
の値をアニメーション化することはできません。
※技術的には、継承と無効な計算状態を処理することでこれを可能にするハックがありますが、そのハックのアニメーションのフレームのタイミングは、CPUの刻みに対して信頼できません。
(そうでなければ、アニメーションは独自のanimation
値を設定し、他のセレクタ状態がそれを停止しようとしても生き続けることができるため、一旦開始された自己設定アニメーションはそのアニメーションが存在する要素をJavaScriptで削除することによってのみ停止できます。
※これがウルトロンの失敗です。
一時停止されたアニメーションも例外ではありません。一時停止中の値が何であっても、他の状態より優先されます。
2. キーフレーム内のプロパティの割り当てにはvar()を使用できる
1 2 3 4 5 6 7 8 9 10 11 12 13 |
body { animation: example 1s infinite; --color: blue; } body:hover { --color: green; } body:has(div:hover) { --color: red; } @keyframes example { 0%, 100% { background: var(--color); } } |
このCSSでは、背景色のデフォルトはblue
で、ホバーするとgreen
になり、div
内でホバーするとred
になります。ユーザーの操作に応じて色が変化します。
3. キーフレームの結果に対する--varの割り当てがキャッシュされる
前述の背景色の割り当てにちょっとした間接的な方法を追加することで、この現象を確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
として評価され、--color
への変更は再計算されないため、常にblue
です。
@keyframes
で設定したアニメーションがpaused
で一時停止されても、背景の状態が変化してもキャッシュされた値は変化しません。
つまり、一時停止されたアニメーションは、キャッシュされた値を使用します。
4. アニメーションのプロパティを変更するとキャッシュが破壊される
ユーザーがホバーしている間にanimation-duration
を変更すると、アニメーションのキャッシュが再計算されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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); } } |
このCSSの最終結果は、2とまったく同じです。背景色のデフォルトはblue
で、ホバー時にgreen
、div
のホバー時にred
です。
注: Safariではアニメーションのプロパティが変更されたときにキャッシュが再計算されないというバグがあるため、Chromeのみの領域です(Firefoxはまだ--var
をアニメーション化できません)。
animation-duration
を1s
に変更しても、技術的には変更されず、キャッシュは再計算されません。
2つの:hover
状態にデフォルト状態とは異なる同じ値(2s
)を設定した場合、興味深い動作になります。
1 2 3 4 5 6 7 8 9 |
body { animation-duration: 1s; } body:hover { animation-duration: 2s; } body:has(div:hover) { animation-duration: 2s; } |
実際の動作は、デモページをご覧ください。
デフォルトの背景色はblue
でマウスがどこから入ったかによって、ホバー時に異なる色(上からだとred
、下からだとgreen
)が表示されます。
5. 2つのアニメーション
--color
の値を変更する疑似セレクタ状態の代わりに、それを変更する別のアニメーションを作成したらどうなるでしょうか?
下記のデモのアニメーションでは、--color
に基づいて--bg
が設定されているため、同じキャッシュの動作が期待できます。example
のアニメーションのプロパティを変更すると、やはりキャッシュが再計算されるはずです。
そこで最後に、アニメーションは現在の--color
値をexample
のアニメーションから取得し、その状態と一緒にキャッシュするようにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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
で一時停止しますが、これはデフォルトの実行状態からの変更であるため、同じCSSペイントフレームで再計算して一時停止します。
実際の動作は、デモをご覧ください。
背景色はホバーした時にロックされ、その後再計算され、ホバーから外れた時に再ロックされます。
そうです、これが持続性です!
The CPU Hackの始まり
ここまでの結果は、非常に興味深いことを示唆しています。
アニメーションからキャッシュされた値を取得しても再計算はされないため、キャッシュされた値のソースが1ステップ削除されても、無効な周期状態が発生することはありません。
2回のキャプチャ、1回の計算、あとはタイミングの管理で可能なはずです。
example
のアニメーションは、通常のセレクタの状態か別のアニメーションから条件付きで値をキャプチャします。
この記事の最初にあった--frame-count
のように、色の代わりに数値をキャプチャしていると想像してみましょう。
そして、名前をexample
からcapture
に変更します。
1 2 3 4 5 6 7 8 9 |
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
に戻すことができます。
では、やってみましょう。
captured
されたキャプチャをhoist
と呼ぶことにします。
また、この2つを同時に実行することは望ましくないので(間違いなく周期的になるため)、安全のためpaused
で一時停止して開始します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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を用意したいと思います。要素間には隙間がないようにし、それらの要素にphase-*
のクラスを与えます。
最初の.phase-0
では、確実に元の出力をキャプチャしています。したがって、hoist
を一時停止したままにしておき、古くからの友人であるcapture
を最初に実行させます。
1 2 3 |
body:has(.phase-0:hover) { animation-play-state: paused, running; } |
その要素のホバーを停止して両方を一時停止すると、--frame-count
がキャプチャされます。または、別の要素を設定して明示的にそれをおこなうこともできます。
1 2 3 |
body:has(.phase-1:hover) { animation-play-state: paused, paused; } |
そして最後に、キャプチャが一時停止している間にhoist
を実行できるかどうかテストします。これにより、循環依存性を回避し、入力として先頭の出力するプロップを回避するのに十分な余地が得られるはずです。つまり、前述の2が得られるはずです。
1 2 3 |
body:has(.phase-2:hover) { animation-play-state: running, paused; } |
実際の動作をご覧ください。
マウスのカーソルを上から下に動かすとループが完成し、数字が増加します。
The CPU Hack
エウレカ!
※アルキメデスが発見・発明したときに叫んだと言われている言葉。
ユーザーに一日中カーソルでDOMを撫でもらうことも、必要な瞬間だけカーソルの下にDOMを移動してもらい:hover
をトリガーにすることもできます。
.phase-0
をホバーすると自動的に.phase-1
に移動します。そして、.phase-2
をホバーすると一時停止状態に戻り、1つのフレームで両方のアニメーションの計算が同時に実行されるのを避ける必要があります。
覚えておいてほしいことは、アニメーションを再生または一時停止すると、そのフレームで再計算が行われるため、実行中のpaused, paused
への変更は実際には1つのフレームで両方を同時に実行することになります。
したがって、.phase-0
にgotoする状態にgotoする必要があります。.phase-1
はpaused, paused
と2番目のgotoなので、これを複製して、新しい.phase-3
も両方を一時停止しますが、代わりに0番目のgotoを実行します。
このCSSを先ほどのCSSに追加してみましょう。
1 2 3 |
body:has(.phase-3:hover) { animation-play-state: paused, paused; } |
HTMLは下記の通りです。
1 2 3 4 5 6 |
<ol class="cpu"> <li class="phase-0"></li> <li class="phase-1"></li> <li class="phase-2"></li> <li class="phase-3"></li> </ol> |
次に、この.cpu
要素の子要素の幅が100%になったときに、子要素が領域全体を占めるようにスタイルを設定します。
1 2 3 4 5 6 7 8 9 10 |
.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%; } |
これで最後のピースが完成です。
各.phase-*
は次の.phase-*
をトリガーにし、それぞれ1つのCSSペイントフレームに対してのみ発生します。実際の動作はデモでご覧ください!
注: 出力変数(--frame-count
)を設定する必要がありました。そうしないとcalc()
が繰り返される度に入れ子になるため、100
で動作しなくなります。整数(integer
)にキャストすることでこれを防ぐことができ、より効率的です。上記のデモでは@property
にコードが含まれています。
また、記述的なことですが、次のように小さなクリーンアップを実行できます。
--input-frame
変数を削除し、直接--frame-hoist
にしたほうがよりクリーンになります。
終わりに
これで、CSSにCPUがあることが分かりました。では、これを使って何ができるでしょう?
下記は、100% CSSで実装したブロック崩しです。
この記事が役に立った、楽しい、興味深いと思われたら幸いです。私のCodePenやX(旧Twitter)をフォローしてくれると嬉しいです。
sponsors