1からのwebアニメーション 〜 イージングの本質を理解する


はじめに

普段からなんとな〜く、CSSのtransitionやjQueryやgsapで済ませていたアニメーション。
ただ、ちょっと凝ったアニメーションやwebGLなど、
難しいことを学ぼうとすると途端に躓いてしまう。。。
そこで、今までなあなあに理解していたwebのアニメーションを基礎から丁寧に学んで、
応用が効くフロントエンドエンジニアになろうと思いました。
ということでこの記事を書いております。

ちなみにこの記事は、CodeGridさんの
イージングの仕組み - イージングとはなにか | CodeGrid
をもとに、理解したことを深めるため自分なりの振り返りをした記事になります。

CodeGridさん、本当にいつもありがとうございます。。。
以下、長文ですが読んでいただけると嬉しいです。

アニメーションとは、静止画の連続である

映像の世界では当たり前ですが、これは当然webの世界でも同じ!
この静止画1枚1枚のことを、フレームと呼びます。
そして、「単位時間あたりに何フレーム表示させるか」をフレームレートという単位で表現し、
特に「1秒間あたりに何フレーム表示させるか」の単位をfps(Frames Per Second)と呼びます。

youtuber実況者がいつもやってるゲームとは別物ですか??
別物でした。

『ということはFPSが高いほど滑らかじゃん!次の案件は10000fpsで!!』

...という訳には行かないようで。
10000fpsってつまり、「いーーーち」って数える間に10000枚の静止画が入るってこと。

  1. そんなfpsに耐えうるモニターなんてないし、PCの限界を超えちゃう
  2. 人間の目の限界もある

以上2つの理由(たぶん)から、fpsはただ数値が大きければよい訳ではないと。
「おそろしく早いアニメーション、俺でなきゃ見逃しちゃうね」っていう事態になってしまうということですね?

まあ詳しいことはよくわからないのですが、
ここは本筋ではないので深く突っ込まないことにします。

ちなみに映画は24fps、テレビは60fpsだそうです。
webで目指すべきは60fps
ということで60fpsという数字を覚えておきます。

まとめると、
アニメーションの三大要素は、
①決められた頻度で(fps)
②静止画を
③繰り返す
である!!ということになります。

実際にコードを書いてみる

オレンジの箱を、左から右に動かすだけのアニメーションをjQueryで書きます。
基礎に忠実に、②静止画(フレーム)③繰り返しすればいいんだよね!
①ペースは、60fpsを目指せばOK!

※図形をクリックするとスタートします。

See the Pen learning about Easing system by yuki tanabe (@yuki-tanabe-plusd) on CodePen.

①頻度を決める(fpsについて)

const secondPerFrame = 16.66666666;//1フレーム何ミリ秒にするか
const FPS = 1000 / secondPerFrame;
//そして、毎秒何フレームを表示するか: 約60フレーム = 60FPS

60fpsを目指します。
60fpsを目標に計算したら、1フレームあたり16.66666ミリ秒(1000ミリ秒 / 60)であればいいとのこと。
この値をsecondPerFrameとして変数に設定します。
FPS変数は参考までに用意しましたが、今回は使いません。

ちなみにjQueryアニメーションのデフォルトは13ミリ秒(約77fps)のようです。
jQuery.fx.interval | jQuery API Documentation

※最初はjQueryのデフォルトに習い、secondPerFrame = 13としました。
しかしカクつきが発生...
上記のように60fpsを目標にするとスムーズになりました。

②静止画のコマを用意する(tick関数)

1フレームで行うことを、tickと名付けた関数に書きます。

  let leftPos = 0;//箱のポジション値を保存
  let goalVal = 1000;//最終的にいてほしいポジション
  let duration = 1000;//適当に決めたトータル秒
  let elapsedTime = 0;//経過した時間(秒)を保存

  let tick = () => {

    if(elapsedTime >= duration) {//1秒たったら終わり
      leftPos = goalVal;
      $el.css('left',leftPos);
      return;//アニメーション終了
    }

    let elapsedTimeRate = elapsedTime / duration;
    //経過時間割合(進捗率) 〜 どれくらい時間が経過したか?

    let valueChangeRate = elapsedTimeRate;
    //値の変化割合 〜 とりあえず経過時間割合と同じに
    //これがイージング。例えば2乗したり3乗したり。。

    leftPos = goalVal * valueChangeRate;
    //値の変化割合に『最終目的値』をかけると、1フレーム後にいてほしい場所が出る

    $el.css('left', leftPos);
    elapsedTime += secondPerFrame;
    //1フレーム分の時間を進める

    setTimeout(tick, secondPerFrame);
    //以後、ずっとこれの繰り返し。。。
  }

基本的にコードにコメントを残しましたが、
ポイントはelapsedTimeRatevalueChangeRateです。
elapsedTimeRateは、アニメーションの進捗率です。
「アニメーションにかけるトータル時間のうち、現在どれくらい時間が経過したか?」
を表す値になります。
一旦変数valueChangeRateにそのまま代入します。

「1フレームがすぎた後に、オレンジの箱はどの地点にいるか?」
それは以下の式で表現することができます。

leftPos = goalVal * valueChangeRate;

この式は確かにそうです。
単純なアニメーションを考えるならば、
例えば、時間が50%経過していたら、
箱も(最終目的地に対して)50%の位置にいてほしいですし、
(目的地1000px * 50% = 500px地点)
時間が95%経過していたら、箱も95%の位置にいてほしいです。
(目的地1000px * 95% = 950px地点)

ここで、
elapsedTimeRate(時間がどれくらい経過したか?)をx軸に、
valueChangeRate(値がどれくらい変化するか?)をy軸に設定したグラフを考えます。
常にelapsedTimeRate = valueChangeRateなので、これはy=xの直線になるのですが、
このグラフ、実はよく見るlinearのイージングになります!

あー、chromeでよく見るやつこれだったのか。
つまり、イージングとは「時間経過に応じて、値がどのように変化するか?」を表現したものだったわけですね。
たしかにcodepenの例、これlinearの動きしてるね。

こう考えると、もっと複雑なアニメーションが想像しやすいですね!
例えば『時間が50%経過していても、まだ箱は20%の位置にいるけど、
時間が60%経過していたら、気づいたら箱が95%の位置にいる!?急に早!』など。

上記は厳密な計算をしていないのですが、
「最初ゆっくりで、後が早い」っていうイージングはよく見かけますよね。
グラフで言うと下記のイメージです。

このグラフがどういう関数になるかはわかりませんが、
x軸の値であるvalueChangeRateを2乗したり3乗したりふにゃふにゃしたりすることで、
色々なイージングが実現できる、ということになります。
数学的な知識はアレな自分でも、なんとなく感覚的に理解できました。

③繰り返す(setTimeout)

これはtick()を繰り返せばよい!
だから一回tick()を実行した後は、内部で再帰的に実行されるよう
setTimeout(tick, secondPerFrame);と書けばいいんだね。

fps設定は頑張らなくても、requestAnimationFrameってやつがちゃんとある

今回、「60fps目指そう!」ってことで変数を色々いじったりしたんですが、
そんなことしなくても便利なものがあるようで。。

requestAnimationFrameは、
プログラムでfpsをわざわざ設定しなくても、使用ブラウザのfpsに合わせてくれるもの。

window.requestAnimationFrame(function() {
  tick();
});

ってことは大体60fpsにしてくれるってことですね!賢い!
しかもこれ、「ブラウザに合わせてくれる」っていうのが結構肝な部分なようで。
どういうことかと言うと、setTimeoutの比較を考えるとわかりやすい。

setTimeoutは超真面目。ブラウザがどうなっていようと必ず実行される。
だから、例えばブラウザの別タブを開いていても、
setTimeoutは見えない裏でずっと働き続ける。
24時間働けますか?って感じだね。
少し前時代的な働き方かな。
ブラウザ重くなっちゃうよ。

でもrequestAnimationFrameはちゃんとサボる。
別タブを開いて見ていないときには、fpsを下げてくれるのです。
「おい!サボってんだろ!」と言ってタブを戻すと
「いや?60fpsで動いてますけど〜?」と働き出す。
確かにこれだとブラウザを圧迫しないで賢いな。今時の若者感でてる。
たぶんこいつ定時退社決めて家でNetflix見るし、上司との飲み会も参加しないだろうな。

この柔軟さはsetTimeoutには無理ですね。
事実、私が書いたcodepenで実現することはできません。
最初にfps系は変数決めてしまっているわけなので。。

一点注意点として、「ブラウザに合わせてくれる」ことから、
60fpsを常に保証するわけではないとのことです。
参照 : window.requestAnimationFrame | MDN

まあでも最適化してくれるって言うのなら、requestAnimationFrame使っていこう!ってなりますね。

ちなみにブラウザ対応状況は、IE11どころかIE10でも大丈夫なようで。
Can I use... Support tables for HTML5, CSS3, etc
受託制作では問題ないレベル!
どんどん使っていきましょう。

まとめ

実際、私が書いたコードはTweenMaxやjQueryで実装すれば一発なのですが、
内部ではこんなことをしているのかな、と知るのは応用を効かせる上ですごい大事なのだと思います。
この知識をもとに、引き続きアニメーションを極めていきたいなと思います!

ではでは
ありがとうございました。