- CSSの数学関数サポートが増えたことで、keyframes の代わりに 時間 tick 変数
--t でアニメーション状態を計算するアプローチが再び実用的になった
@property --t と長さ 86400000ms のルートアニメーションを組み合わせると、ミリ秒単位で時間が増加し、それを rotate、translate などのプロパティ計算に結び付けられる
step() を使えば 8fps、24fps、60fps のように フレームレート を制限でき、ブラウザ最適化のおかげで 60fps を超える更新頻度も通常は問題にならない
min(), mod(), sin(), pow(), round() で時間値を制限・反復・振動・イージング処理し、カスタム easing と位置移動を keyframes なしで表現できる
css-doodle は @t, @T, @TS のような短縮表現を提供し、数式の多いデモをより短く書いて素早く試せる
時間 tick を CSS 変数にする
- 過去のアプローチは、CSS で複雑な 数学計算 を行うのが難しく、適用範囲が限られていた
- 現在の CSS は
mod(), round(), 三角関数など必要な数学関数をサポートしており、時間ベースのアニメーションを再び試せるようになっている
- デモの確認には Experimental feature flags の有効化が必要な場合がある
基本的な仕組み
- JavaScript のように CSS が直接タイマーを開始することはできないが、CSS Houdini API でカスタム変数
--t を定義してミリ秒単位の時間を追跡できる
- 基本例では
@property --t を <integer> として宣言し、@keyframes tick で 0 から 86400000 まで変化させ、:root に 86400000ms linear infinite アニメーションを適用する
--t は 1 ミリ秒ごとに 1 ずつ増加し、1 秒で 1000 になる
counter() を使えば現在の --t の値を画面に表示できる
rotate: calc(var(--t) * .001turn) のように --t ベースの計算をプロパティに結び付けると、時間の経過とともに値が変わり、アニメーション になる
フレームレート制御
- 滑らかなアニメーションには 60fps の更新頻度で十分である
- ブラウザのレンダリング最適化があるため、更新頻度が 60fps を超えても通常は問題にならない
- 必要なら
animation-timing-function に step() を使ってフレームレートを直接制限できる
- 8fps:
step(calc(86400000/(1000/8)))
- 24fps:
step(calc(86400000/(1000/24)))
- 60fps:
step(calc(86400000/(1000/60)))
増え続ける時間値をアニメーション値に変換する
--t の値は一方向に増え続ける
angle 値は 360deg を超えても問題ないが、すべての CSS プロパティが値を 循環的 に処理するわけではない
translate: calc(var(--t) * .001px) はボックスを右へ動かし続ける
-
min() で停止する
- 特定の offset に到達した瞬間に停止させるには min() を使える
translate: min(270px, calc(var(--t) * .5px)) は移動値を 270px に制限する
- 継続時間を正確に制御したいなら
--t 自体を制限できる
translate: calc(min(3000, var(--t)) * (270px / 3000))
- この計算は 3 秒で 270px 移動させる
-
mod() で繰り返す
- ボックスが右に移動した後、最初からやり直すには mod() を使う
translate: calc(mod(var(--t)/4, 270) * 1px) で offset を繰り返せる
-
sin() で往復移動を作る
- 往復移動は sin() で表現できる
translate: calc(sin(mod(var(--t)/135, 270)) * 135px) のように時間値を三角関数に結び付ける
カスタム easing 関数
- CSS の数学関数と
--t 変数を使うことで、cubic-bezier() では実現しにくい カスタム easing を作れる
- まず
--t を 0 と 1 の間の値に制限する
--t01: calc(min(1000, var(--t)) / 1000) は 1 秒かけて 0 から 1 へ変化する
-
ease-out-cubic
ease-out-cubic は 1 - pow(1 - t, 3) という計算式で構成される
--ease-out-cubic の値を作り、それを translate: calc(var(--ease-out-cubic) * 270px) に結び付ける
-
ease-out-elastic
ease-out-elastic は pow(2, -10t) * sin((10t - .75) * 2/3 * PI) + 1 という計算式で構成される
- 同じ方法で
--ease-out-elastic の値を作り、translate に掛けて 270px の移動に適用する
css-doodle で数式を短くする
- 数式が複雑になるほど
var() と calc() によってコードが読みにくくなる
- css-doodle の最新バージョンは
--t を表す @t 関数 を追加しており、引数内で簡単な数式を直接受け取れる
rotate の表現は次のように短くなる
- 従来:
rotate: calc(mod(var(--t) / 1000, 10) * 5deg)
css-doodle: rotate: @t(/1000, %10, *5deg)
- keyframes を書かなくても、短いコードで数学ベースのアニメーションを作れる
- パラメータを変えれば新しい結果を素早く試せる
-
@T と @TS
@t のほかに大文字の @T は、その日の開始時点からの time ticks を表す
- @TS は
@t(/1000) の shorthand で、時間を秒単位で追跡する
- 時計の例は CodePen で見られる
- 秒針:
rotate: @TS(*6, %360deg)
- 分針:
rotate: @TS(/60, *6, %360deg)
- 時針:
rotate: @TS(/60, /12, *6, %360deg)
-
round() で秒針のジャンプを作る
- 秒針をジャンプするように動かしたいなら round() を使う
round() の第 3 引数は丸め間隔を指定する
- 時計では 1 ステップが
360 / 60 = 6deg なので、rotate: round(down, @TS(*6, %360deg), 6deg) を使う
デモと適用ポイント
- 色と位置を一緒にアニメーションする例は CodePen で見られる
- テキスト色アニメーションの例は CodePen にある
- keyframes のほうが直感的に見える場合もあるが、数学計算と入力変数が多いデモシーンでは、時間を変数として使う方式 のほうが多様な結果を作りやすい
- 2024 年 5 月 17 日の更新で、テキスト色アニメーションのデモが追加された
1件のコメント
Hacker Newsのコメント
JSで進行度を制御したいなら、負の
animation-delay値を使うのもおすすめたとえば
animation-delay: -1500msはアニメーションを即座に開始しつつ、1.5秒地点までスキップするこの値をJSで制御すれば、CSSアニメーションをJSから実質的にスクラブでき、すべてのアニメーションをゲームエンジン風の compute-update-render ティックループに合わせられる
https://developer.mozilla.org/en-US/docs/Web/CSS/animation-d...
単純なイージング関数や1〜2チャンネルの基本的なキーフレームより少しでも複雑になると、このアプローチの限界にすぐ突き当たる
ここ数年 Theatre.js を使っていて、かなり満足している
キーフレームとベジェ曲線をタイムライン上で編集するスタジオUIと、そのキーフレームをタイムライン基準で補間するランタイムに分かれたライブラリ
複数のアニメーションを同期させて動かしたいなら試す価値がある
https://www.theatrejs.com/
とても印象的だが、どこか何十年も前に Flash でできたことにようやく近づいた感じもする
Flash の復活を望んでいるわけではないが、CSSアニメーションを作るための、もっとユーザーフレンドリーなツールがあればと思う
CSS は今や
mod(),round(), 三角関数のような数学関数のサポートが十分に整っているCSS は JavaScript のようにタイマーを開始することはできないが、最近では CSS Houdini API でミリ秒単位の時間を追跡するカスタムプロパティを定義できる
とはいえ、JavaScript があるのになぜこれらすべてが必要なのだろう?
描画レイヤーは描画プリミティブに集中すべきではないのか? なぜそこにさらに上位レイヤーの機能を詰め込むのだろう?
アニメーションのために JS で三角関数を回すなら、おそらく毎ミリ秒 UI を更新しようとして DOM も操作しているはずで、そのコストは非常に大きい
描画は HTML で構成されるか、JS で更新される DOM が主導する
CSS は DOM に対して何をするかというパラメータをペイントエンジンに渡し、デフォルト値を上書きする形で載っているにすぎない
DOM ブロックを追加することもできるし、複雑なアルゴリズムで配置動作を変えることもできるし、マージンですら実はそれほど単純ではない
優れた解決策だとは思わないが、DOM レンダリング中に色を変えるために HTML コードを更新しなくて済むようにしようとして始まったように見える
おそらく HTML コードの更新がまったく楽しくなかったからだろうが、そもそも間違った問題を解いていたのだと思う
当時が「ウェブマスター」の時代だったという事情もある
fetch ロジックの中に、ローダー DOM 要素の幾何学的な進行ロジック、たとえば回転角度の計算やインラインスタイルの適用などが混ざっていなければ読みやすい
今どきはチョコレートとピーナッツバターを別々にしておく流儀は人気がないのは承知しているが、こうした慣習はコードがどこにあるべきか、どこへ置くべきかを推測しやすくする境界線を引いてくれる
冗談はさておき、CSS は宣言的であり、JS と CSS で解ける問題が重なるのは自然なことだと思う
最近 CSS に追加されている新機能には本当に期待しているし、解釈されるコードが減るのは良いことだ
記事で使われていたカスタム CSS
@propertyは素晴らしく、ブラウザ対応率も 88% ある [1]ただし、フォールバックの
initial-valueを設定する際はブラウザごとの挙動差に注意が必要Chrome は CSS 変数が未定義、または不正な値に設定されている場合に
initial-valueを使うFirefox は変数が未定義のときにだけ
initial-valueを使うほとんどのプロジェクトでは問題にならないだろうが、最近のプロジェクトではブラウザ実装差を吸収するため、Firefox でデフォルト値を JavaScript から設定する必要があった
[1] https://caniuse.com/?search=%40property
about:configに行き、layout.css.properties-and-values.enabledを有効にすればよい:root{}に定義されたプロパティ値を取得:getComputedStyle(document.documentElement).getPropertyValue("--some-custom-css-var");プロパティ値を設定:
document.documentElement.style.setProperty("--some-custom-css-var", "some value");まだ CSS
@propertyサポートが有効化されていないブラウザでは、ポリフィルのように使える最新の Android Chrome では興味深いことに、Experimental feature flags なしでも
mod()以前のデモは動くが、mod()とsin()は何も起こらないしばらくこれを欲しかった理由は、box-breathing のようなものを作りたかったから
吸う・止める・吐く・止めるの各段階を、自分の身体や狙った効果に合わせてすべて調整できるようにしたかった
このページのようなやり方がなければ、JS なしで、あるいはばかげた複雑さなしで実装する方法が見つからなかった
まったく、スタイルシートだなんて! このページのデモは本当に独創的だ
記事は本当によく書けている
Chrome はまだ CSS
mod()サポートを正式リリースしていないので、Chrome のプレビューリリースを使わない限り、このページの例の大半はアニメーションしないティック制御が不要で、すべてのブラウザで滑らかな CSS アニメーションだけが欲しいなら、FLIP テクニックが有用
https://medium.com/outsystems-experts/flip-your-60-fps-anima...
ページ下部のアニメーションが非常に印象的
個々の要素というより、1本の映像のように見える
https://RTCode.io は、リロードなし・状態リセットなしで動くリアルタイムWebプレイグラウンドなので、CSS アニメーションやトランジションを扱う人に大いに役立つかもしれない
デモは https://RTCode.io にある
Pen をインポートするには、URL のホスト名を
xcodepen.ioに変えればよい