はじめに
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にするアニメーションができました。
このように、経過時間を使って進捗を算出し、要素に反映する。
単純、だけどかなり柔軟に作り込むことができます。
はいっ、自由自在なアニメーションのできあがりー!