7 ポイント 投稿者 GN⁺ 2024-05-06 | 1件のコメント | WhatsAppで共有
  • CSSの数学関数サポートが増えたことで、keyframes の代わりに 時間 tick 変数 --t でアニメーション状態を計算するアプローチが再び実用的になった
  • @property --t と長さ 86400000ms のルートアニメーションを組み合わせると、ミリ秒単位で時間が増加し、それを rotatetranslate などのプロパティ計算に結び付けられる
  • 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 tick0 から 86400000 まで変化させ、:root86400000ms linear infinite アニメーションを適用する
  • --t は 1 ミリ秒ごとに 1 ずつ増加し、1 秒で 1000 になる
  • counter() を使えば現在の --t の値を画面に表示できる
  • rotate: calc(var(--t) * .001turn) のように --t ベースの計算をプロパティに結び付けると、時間の経過とともに値が変わり、アニメーション になる

フレームレート制御

  • 滑らかなアニメーションには 60fps の更新頻度で十分である
  • ブラウザのレンダリング最適化があるため、更新頻度が 60fps を超えても通常は問題にならない
  • 必要なら animation-timing-functionstep() を使ってフレームレートを直接制限できる
    • 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 を作れる
  • まず --t01 の間の値に制限する
    • --t01: calc(min(1000, var(--t)) / 1000) は 1 秒かけて 0 から 1 へ変化する
  • ease-out-cubic

    • ease-out-cubic1 - pow(1 - t, 3) という計算式で構成される
    • --ease-out-cubic の値を作り、それを translate: calc(var(--ease-out-cubic) * 270px) に結び付ける
  • ease-out-elastic

    • ease-out-elasticpow(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件のコメント

 
GN⁺ 2024-05-06
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...

    • それなら iOS スタイルのスクロールオフセットベースのアニメーションも可能になるのでは?
  • 単純なイージング関数や1〜2チャンネルの基本的なキーフレームより少しでも複雑になると、このアプローチの限界にすぐ突き当たる
    ここ数年 Theatre.js を使っていて、かなり満足している
    キーフレームとベジェ曲線をタイムライン上で編集するスタジオUIと、そのキーフレームをタイムライン基準で補間するランタイムに分かれたライブラリ
    複数のアニメーションを同期させて動かしたいなら試す価値がある
    https://www.theatrejs.com/

  • とても印象的だが、どこか何十年も前に Flash でできたことにようやく近づいた感じもする
    Flash の復活を望んでいるわけではないが、CSSアニメーションを作るための、もっとユーザーフレンドリーなツールがあればと思う

  • CSS は今や mod(), round(), 三角関数のような数学関数のサポートが十分に整っている
    CSS は JavaScript のようにタイマーを開始することはできないが、最近では CSS Houdini API でミリ秒単位の時間を追跡するカスタムプロパティを定義できる
    とはいえ、JavaScript があるのになぜこれらすべてが必要なのだろう?
    描画レイヤーは描画プリミティブに集中すべきではないのか? なぜそこにさらに上位レイヤーの機能を詰め込むのだろう?

    • CSSアニメーション がハードウェアアクセラレーションされる点を忘れてはいけない
      アニメーションのために JS で三角関数を回すなら、おそらく毎ミリ秒 UI を更新しようとして DOM も操作しているはずで、そのコストは非常に大きい
    • CSS は描画レイヤーではない
      描画は HTML で構成されるか、JS で更新される DOM が主導する
      CSS は DOM に対して何をするかというパラメータをペイントエンジンに渡し、デフォルト値を上書きする形で載っているにすぎない
      DOM ブロックを追加することもできるし、複雑なアルゴリズムで配置動作を変えることもできるし、マージンですら実はそれほど単純ではない
      優れた解決策だとは思わないが、DOM レンダリング中に色を変えるために HTML コードを更新しなくて済むようにしようとして始まったように見える
      おそらく HTML コードの更新がまったく楽しくなかったからだろうが、そもそも間違った問題を解いていたのだと思う
      当時が「ウェブマスター」の時代だったという事情もある
    • 思い浮かぶ利点の1つは、アプリケーションロジックと表現の分離
      fetch ロジックの中に、ローダー DOM 要素の幾何学的な進行ロジック、たとえば回転角度の計算やインラインスタイルの適用などが混ざっていなければ読みやすい
      今どきはチョコレートとピーナッツバターを別々にしておく流儀は人気がないのは承知しているが、こうした慣習はコードがどこにあるべきか、どこへ置くべきかを推測しやすくする境界線を引いてくれる
    • JS は基本的にシングルスレッドなので、もちろんワーカーはあるにせよ、UI を安定したフレームレートで維持するには、作業を別の場所へオフロードできるものは何でもありがたい
    • CSS に WebSocket が入るまで待とう!
      冗談はさておき、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

    • Firefox でデモを試したいなら about:config に行き、layout.css.properties-and-values.enabled を有効にすればよい
    • 必要なときに JavaScript からプロパティ値を設定する方法は以下のとおり
      :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 なしで、あるいはばかげた複雑さなしで実装する方法が見つからなかった
    まったく、スタイルシートだなんて! このページのデモは本当に独創的だ

    • それって単に CSS のアニメーションカーブでは?
  • 記事は本当によく書けている
    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 に変えればよい

    • RTCode を使ったことがある人はいる? 面白そうではあるけれど、ホームページを見る限り、リアルタイム対応の CodePen 代替なのか、それとも本番環境でも使えるものなのか少し分かりにくい