本文へ移動

【120FPS問題の解決策】 gsap.ticker を使用してアニメーションを60FPSで制御する

クリエイティブなWEBサイト制作をする際によく使用するrequestAnimationFrameは、JavaScriptで滑らかなアニメーションを実現する際、ブラウザの描画サイクルに同期してコールバック関数を実行する非常に便利なAPIですよね。

https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame

しかし、近年、リフレッシュレートが120Hzやそれ以上のディスプレイが普及する中で、このAPIを適切に使用しないと、アニメーションが意図せず2倍の速さで実行されてしまう「120FPS問題」が発生してしまいます。

requestAnimationFrame の実行回数はディスプレイのリフレッシュレートに依存する

requestAnimationFrameの「描画サイクル」はディスプレイのリフレッシュレートに同期しているため、実行回数はリフレッシュレートに依存します。

例えば、リフレッシュレート60Hzのディスプレイでは、requestAnimationFrameのコールバック関数は1秒間に約60回実行されますが、リフレッシュレート120Hzのディスプレイでは、requestAnimationFrameのコールバック関数は1秒間に約120回実行されます。

これがアニメーションにどういう影響を与えるかというと次のようなケースです。

let position = 0;

function animate() {
 position += 1; // 1pxずつ移動
 element.style.left = ${position}px;

 requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

1回の描画サイクルでpositionを1ずつ増やす計算をする場合、60Hzのディスプレイでは1秒間にposition60増えますが、120Hzのディスプレイでは1秒間にposition120増えるため、60Hzを基準にした時に120Hzのディスプレイでは、アニメーションの速度は2倍になります。

このように、リフレッシュレートが高い画面ではアニメーションが速く実行されてしまう「120FPS問題」が発生するため、速度を一定に保つには、timestamp引数を使用して前のフレームからの経過時間を計算することで、速度をリフレッシュレートに依存しないように制御する必要があります。

let lastTimestamp = 0;
let position = 0;
const speed = 100; // 1秒間に100px移動する速度

function animate(timestamp) {
 const deltaTime = (timestamp - lastTimestamp) / 1000; // 経過時間を計算
 position += speed * deltaTime; // 経過時間に応じた距離だけ位置を更新
 element.style.left = ${position}px;
 lastTimestamp = timestamp;

 requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

gsap.ticker を使用してアニメーションを60FPSで制御する

gsap.tickerは、requestAnimationFrameを内部的に使いつつ、経過時間やフレームレートを自動的に管理してくれるユーティリティです。

https://gsap.com/docs/v3/GSAP/gsap.ticker()/

gsap.tickerには、アニメーションの最大フレームレートを制御するfps()があります。

gsap.ticker.fps(60)と記述するだけで、リフレッシュレートが120Hzのディスプレイであっても、アニメーションの更新を意図的に60FPSに制限することができます🙆‍♂️

let position = 0;

function animate() {
 position += 1; // 1pxずつ移動
 element.style.left = ${position}px;
}

gsap.ticker.fps(60);
gsap.ticker.add(animate);

gsap.ticker は登録したコールバック関数の解除もシンプルに記述できる

gsap.tickerは、登録したコールバック関数の解除も非常に簡単です。

requestAnimationFrameの場合は次のアニメーションフレームのIDを保存しておく必要があります。

let animationId;
let position = 0;

function animate() {
 position += 1;
 element.style.left = ${position}px; 
 // 次のアニメーションフレームをリクエストし、IDを保持
 animationId = requestAnimationFrame(animate);
}

animate();

// 保持しておいたIDを使ってアニメーションをキャンセル
cancelAnimationFrame(animationId);

gsap.tickerは、add()で登録したコールバック関数をremove()で簡単に解除できます🙆‍♂️

let position = 0;

function animate() {
 position += 1; // 1pxずつ移動
 element.style.left = ${position}px;
}

// add()でコールバックを登録
gsap.ticker.add(animate);

// remove()でコールバックを解除
gsap.ticker.remove(animate);

まとめ

この問題、結構あるあるなんじゃないでしょうか。フロントエンド開発って多様なデバイスに最適化した書き方をしないといけないから難しいですよね...。

「このデバイスだけアニメーションが速いんだけど...」と指摘された際には、この問題を疑ってみてください。皆さんもぜひgsap.tickerを使用してシンプルにアニメーションのFPSを管理しましょう

くりちゃん

東京で働くクリエイティブ・フロントエンドデベロッパー。
プログラミングが好きで休みの日もコードを書いて、モノづくりを楽しんでいる。文章を書くのは苦手。

FOLLOW

PR