Posted at

JavaScriptのrequestAnimationFrameでアニメーションを自由自在に操る


はじめに

Webページをアニメーションさせようとしたときに、選択肢として楽なのは


  • CSSアニメーション

  • jQueryアニメーション

この辺だろうか。

これらは、以下の情報を指定することで、途中の状態をよしなにやってもらうものです。


  • 始点

  • 終点

  • 時間

  • 動かし方

ただ今回は、よしなにやってもらうのではなく、途中の状態も自分で指定する!という自由自在への道を開こうと思います。


自由自在の道にも更なる選択肢

よし、じゃあJavaScriptで自由自在アニメーションを実装しよう!ってなったときに、どういう風にするかっていうのざっくり言うと、


  • ループを回す

  • 経過時間や距離によって状態を算出し、要素に反映する

っていう感じに進むわけです。

じゃあ早速ループを回そー!・・・ん、ループ?

さあ、ここでループの回し方が複数あって迷うっていうイベントが発生します。


僕らを悩ませるループ

さあさあみんなで声を揃えてループを回しましょう。

わかるわかる、setTimeoutとかsetIntervalやろ?

ってなったあなたには朗報です。

ここに出でし、アニメーションループの申し子。

requestAnimationFrame

使うしかない。


setIntervalとrequestAnimationFrameの違い

まあまあ違う箇所はありますが、アニメーションとして注目すべきは

こいつらに指定したメソッドが叩かれるタイミングです。


setIntervalの場合

指定時間毎


requestAnimationFrameの場合

ブラウザの描画タイミング毎

指定時間と描画タイミング毎だと、どっちの方がよりアニメーションに適しているかは一目瞭然かと思います。

描画のタイミングで状態を変更する方が、圧倒的に良さげですね。


実装

ということで、requestAnimationFrameを使って実装していくわけです。

今回の出演者は、以下の二名です。


  • ループを司るもの: Updater

  • 要素の状態を司るもの: Animator

ではでは、中身を見ていきましょう。

まずはUpdater

class Updater {

constructor() {
this.targets = [];
}

// アニメーション対象を追加
add(target) {
this.targets.push(target);
}

// アニメーション対象を削除
remove(target) {
this.targets = this.targets.filter(element => element !== target);
}

// アニメーションループ開始
start() {
this.targets.forEach(element => element.start());
window.requestAnimationFrame(this.update.bind(this));
}

// 各アニメーション対象に更新を通知
update() {
window.requestAnimationFrame(this.update.bind(this));
this.targets.forEach(element => element.update());
}
}

こんな感じ。

次に、Animator

class Animator {

constructor() {
this.startTime = 0;
}

// アニメーション開始時の時間を取得
start() {
this.startTime = performance.now();
}

// 描画毎にここが呼ばれる
update() {
// ここで経過時間をもとに進捗を算出し、要素に反映
}

// 進捗算出
// e: easingType
// t: currentTime
// b: startValue
// c: endValue
// d: totalTime
getProgress(e, t, b, c, d) {
let progress = 0;

if (t < 0) return b;
if (t > d) return c;

switch (e) {
case 'linear':
progress = c * t / d + b;
break;
case 'easeInSine':
progress = -c * Math.cos(t / d * this.PI) + c + b;
break;
default:
break;
}
return progress;
}
}

こんな感じ。

あとは、


  • Updater, Animatorのインスタンスを作成

  • UpdaterにAnimatorのインスタンスをaddする

  • Updaterをstartする

という感じで準備が完了です。

Animatorのupdateメソッド内に、アニメーションの内容を書きます。

例えば、

update() {

const elapsed = performance.now() - this.startTime
, opacity = this.getProgress('easeInSine', elapsed, 0, 1, 1000);

$('#js-content').css('opacity', opacity);
}

といった形で、1000秒かけてopacityを0→1にするアニメーションができました。

このように、経過時間を使って進捗を算出し、要素に反映する。

単純、だけどかなり柔軟に作り込むことができます。

はいっ、自由自在なアニメーションのできあがりー!