最近は Web Animation API なるものが現れて、ついにjQueryのアレがWeb標準にとしみじみとするところですが、JSで生のアニメーションコードを書くこともあるかと思います。そんな時のちょっとしたTipsです。
コードでぼんやりアニメーションを書いてみる
A - Bに値が変化するアニメーションを考えますと線形補完が浮かんできます。何も考えずに、こんな形で書いてみます。
function lerp(a: number, b: number, time: number, totalTime: number){
return (totalTime - time) / totalTime * a + time / totalTime * b;
}
そうすると、このような形で利用できます。
const startTime = Date.now();
function animate(){
const elapsedTime = (Date.now() - startTime) / 1000;
const value = lerp(0, 100, elapsedTime, 5);
console.log(value);
if(elapsedTime < 5){
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
わかりやすいですね。では、イージング関数をつけて、挙動にメリハリをつけてみます。
良くあるイージング関数を準備します。 引用元: イージング関数チートシート
function easeInOutQuad(x: number): number {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
}
この関数を、先ほどの描画関数に仕込んでみます。
const startTime = Date.now();
function animate(){
const elapsedTime = (Date.now() - startTime) / 1000;
const easedTime = easeInOutQuad(elapsedTime / 5) * 5;
const value = lerp(0, 100, elapsedTime, 5);
console.log(value);
if(elapsedTime < 5){
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
雲行きが怪しくなってきましたが、次に行って戻るようなpingPong関数を追加してみます。
function pingPong(time: number, totalTime: number){
const clamped = Math.min(Math.max(0, time), totalTime);
return clamped < totalTime / 2 ? clamped * 2 : (totalTime - clamped) * 2;
}
const startTime = Date.now();
function animate(){
const elapsedTime = (Date.now() - startTime) / 1000;
const easedTime = easeInOutQuad(elapsedTime / 5) * 5;
const value = lerp(0, 100, pingPong(elapsedTime, 5), 5);
console.log(value);
if(elapsedTime < 5){
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
なんとか実装できましたが、やや匂いの強いコードになってきました。
- コードを眺めると「5」というシンボル(変数かもしれない)が何度か出てきます。
- 利用者側のコードも、ロジック側のコードも何やら複雑になってしまいました
ことの問題は、トータル時間を計算の中に混ぜてしまったのが問題のようです。
そうだ、時間なんてすべて0-1で正規化してしまえばいいじゃない
そもそもですが、時間をわざわざ関数ロジックで扱う必要なんてなかったんです。
すなわち、あるアニメーションをする関数がある場合、 f(t: 単位時間)
としてしまえば、1秒の時は、 f(time)
5秒の時は f(time/5)
10秒の時は f(time/10)
と表現することができます。
そのようなアイディアで、コードを書き直します。
function lerp(a: number, b: number, time: number){
return (1 - time) * a + time * b;
}
function pingPong(time: number){
const clamped = clamp01(time);
return clamped < 1 / 2 ? clamped * 2 : (1 - clamped) * 2;
}
// 参考
function clamp01(x: number){
return x < 0 ? 0 : x > 1 ? 1 : x;
}
どうでしょうか、大変すっきりして、再利用できそうな感じがします。最後に利用側のコードを書いてみます。
const startTime = Date.now();
function animate(){
const elapsedTime = (Date.now() - startTime) / 1000;
const time = elapsedTime / 5;
const value = lerp(0, 100, pingPong(easeInOutQuad(time)));
console.log(value);
if(elapsedTime < 5){
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
時間計算部分が0-1で正規化されたため、時間入力から値の取得までがきれいな形になりました。この形でライブラリを大きくしていっても、さまざまな関数の互換性が得られ、最大時間を引き回すような必要性もなくなります。
以上から、コードでアニメーション時間を扱う時は0-1の正規化するのがおすすめです。