12周年記念パーティ開催! 2024/5/10(金) 19:00

イージングの仕組み 前編 イージングとはなにか

第1回目はシンプルなサンプルでイージングのコードで行われていることを解説します。どのような値を、どのように操作することでイージングが実現されているのか、その仕組みの理解を目指します。

発行

著者 高津戸 壮 テクニカルディレクター
イージングの仕組み シリーズの記事一覧

はじめに

JavaScriptでなんらかのUIを作ることが多いという人の場合、イージングがどのように動作しているかを知らずとも、それを普段から利用している人は多いでしょう。イージングの仕組みはなかなか複雑です。

そして、例えこの仕組みを理解したとしても、自らイージングを書くような機会はほぼないでしょう。しかし、普段利用しているアニメーションというものがどのようにできているのか知ることで、よりUIプログラミングへの理解を深めることができるかと思います。

このシリーズで使うサンプルのコードは、以下に置いてあります。

サンプル集

pxgrid/codegrid-easing · GitHub

イージングってこんなやつ

まずはイージングがどんなものか見てみましょう。以下のデモを開き、Runと書かれたボタンを押してみてください。アニメーションを確認することができます。

サンプル:01-easingdemo

codegrid-easing/01-easingdemo at gh-pages · pxgrid/codegrid-easing · GitHub

このデモは、色のついたspanleftをアニメーションさせていますが、このとき、それぞれに別のイージングを指定しています。このサンプルで、アニメーションを行わせている部分のコードは、以下のようになっています。

$('#easingdemo1 span').animate({ left: 200 }, 1000, 'linear');
$('#easingdemo2 span').animate({ left: 200 }, 1000, 'swing');
$('#easingdemo3 span').animate({ left: 200 }, 1000, 'easeInExpo');
$('#easingdemo4 span').animate({ left: 200 }, 1000, 'easeOutExpo');

jQueryのanimateを実行する時、3番目の引数を指定すると、その指定した種類のイージングでアニメーションが行われます。ここで指定している各イージングについては、以下のような結果をもたらします。

  • linear:常に同じ速度
  • swing:始めはおだやか、途中はちょっと速め、最後はおだやか
  • easeInExpo:始めはとてもゆっくり、最後はとても速く
  • easeOutExpo:始めはとても速く、最後はとてもゆっくり

ここで使われているイージングのうち、linearswingは、始めからjQueryに内蔵されているものです。アニメーションをさせるとき、なにも指定しなければ、自動的にswingが適用されます。

イージングの追加方法

easeInExpoeaseOutExpoは、jQueryへイージングを追加するライブラリを読み込むことで、利用可能となります。このサンプルでは、jQuery UIに内蔵されているイージングのコードを読み込ませています。jQuery UIに内蔵されているイージングの一覧は、次から参照できます。

jQuery UIに内臓されているイージングの一覧

Easings | jQuery UI API Documentation

このほか有名なのはjQuery Easing Pluginでしょう。

jQuery Easing Plugin (version 1.3)

jQuery Easing Plugin (version 1.3)

イージングの中身

さて、上記のようなイージングのライブラリを読み込むとなにが起こるかというと、jQuery.easingに存在しているオブジェクトが拡張されます。

アニメーション時にイージング名を指定すると、jQuery.easingを参照し、対応するイージング関数が適用されるようになっているのです。つまり、これらを読み込めば、jQueryのアニメーションとして指定できるイージングの種類が増えます。

では、そのjQuery.easingの拡張が行われている箇所を見てみます。以下はjQuery UIのコードの一部です。

(function() {

// based on easing equations from Robert Penner (http://www.robertpenner.com/easing)

var baseEasings = {};

$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) {
  baseEasings[ name ] = function( p ) {
    return Math.pow( p, i + 2 );
  };
});

$.extend( baseEasings, {
  Sine: function ( p ) {
    return 1 - Math.cos( p * Math.PI / 2 );
  },
  Circ: function ( p ) {
    return 1 - Math.sqrt( 1 - p * p );
  },
  Elastic: function( p ) {
    return p === 0 || p === 1 ? p :
      -Math.pow( 2, 8 * (p - 1) ) * Math.sin( ( (p - 1) * 80 - 7.5 ) * Math.PI / 15 );
  },
  Back: function( p ) {
    return p * p * ( 3 * p - 2 );
  },
  Bounce: function ( p ) {
    var pow2,
      bounce = 4;

    while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
    return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
  }
});

$.each( baseEasings, function( name, easeIn ) {
  $.easing[ "easeIn" + name ] = easeIn;
  $.easing[ "easeOut" + name ] = function( p ) {
    return 1 - easeIn( 1 - p );
  };
  $.easing[ "easeInOut" + name ] = function( p ) {
    return p < 0.5 ?
      easeIn( p * 2 ) / 2 :
      1 - easeIn( p * -2 + 2 ) / 2;
  };
});

})();

さて、このコードでなにが行われているのかわかるでしょうか。おそらく、多くの人は、ここで書かれている処理がなんなのか、よくわからないのではないでしょうか。

正直なところ筆者自身も、このコードのうち簡単なものしか理解できていませんが、このシリーズでは、このイージング関数内でどのような行われているのか、その基本的な仕組みを解説します。

元祖イージング?

このシリーズ自体、筆者がイージングの仕組みを理解していなかったので、調べ出したところから始まっているのですが、調べていたら、イージングの元祖とでもいうような人がいることがわかりました。それは、Robert Pennerという、Flashのデベロッパーです。氏のイージングに関する資料は、以下のページにまとまっています。

Robert Penner氏のイージングに関する資料

Robert Penner's Easing Functions

はっきりとした起源はわからなかったのですが、どうやら、世の中に存在するイージングは、氏の考えたイージングそのものであるか、そのバリエーションであるものが多いようです。イージングのことを調べていると、いつもRobert Pennerという名前が出てきます。

では、そのイージングとはどういったものなのか。その仕組みを、簡単なサンプルとともに見ていくとしましょう。

アニメーション

まずはイージングを理解しやすくするため、自前でアニメーションの仕組みを実装してみます。以下が、そのデモです。

サンプル:02-animation

codegrid-easing/02-animation at gh-pages · pxgrid/codegrid-easing · GitHub

このデモでは、Runと書かれたボタンを押すと、1秒かけて赤いdiv要素が動きます。ここでは、leftの値を変化させていますが、その動きにイージング処理は施していません。左から右へ常に一定の速度で動きます。このサンプルのコードを見てみましょう。

// 1フレームは何ミリ秒にするか
var MILLISEC_PER_FRAME = 13;

// アニメーションさせる関数
var animate = function($el) {

  var leftVal = 0; // left値
  var valueInChange = 200; // 変化させるleft値
  var duration = 1000; // トータル時間
  var elapsedTime = 0; // 過ぎた時間

  // 要素を動かす関数
  var tick = function() {

    // 1000ミリ秒経ってたら最終値をセットして終わり
    if(elapsedTime >= 1000) {
      leftVal = valueInChange;
      $el.css('left', leftVal);
      return;
    }

    // 経過時間の割合 = 経過時間 / トータル時間
    var elapsedTimeRate = elapsedTime / duration;
    // 現在の値 = 変化させるleft値 * 経過時間の割合
    leftVal = valueInChange * elapsedTimeRate;

    // 要素のスタイルを更新
    $el.css('left', leftVal);

    // 1フレーム分の時間を経過時間に加算
    elapsedTime += MILLISEC_PER_FRAME;
    // 1フレーム後に再び動かす
    setTimeout(tick, MILLISEC_PER_FRAME);

  };

  tick(); // 動かし始める

};

$(function() {
  var $box = $('#box');
  $('#run').click(function() {
    animate($box);
  });
  $('#reset').click(function() {
    $box.css('left', 0);
  });
});

animateという関数にjQueryオブジェクトを渡すと、その要素のleftの値が、200(px)になるまで、1秒かけてアニメーションします。

細かくパフォーマンスのことを追求すると、いろいろと改良の余地はありますが、ひとまず、今回はこのシンプルなサンプルを解説していきます。

JavaScriptでのアニメーションの仕組み

まずJavaScriptでアニメーションを行う場合、それがどのように行われるかを簡単に説明しておきます。

映像などのアニメーション(動画)と同様に、JavaScriptでアニメーションを行う場合も、少しずつ状態が変化する静止画(コマ)を連続して表示し、動いているように見せる方法を使います。

しかし、JavaScriptには、アニメーションを簡単な指定で実現するような機能を持ちあわせていません。このため、ごく短い時間間隔をおきながら、少しずつ要素のスタイルを変化させるという処理をイチから書く必要があります*。これがJavaScriptでアニメーションを行う際の一般的な実装方法です。

*注:アニメーションの実装方法

「処理をイチから書く」といっても、私たちはそのようなアニメーションの処理を行いたいときは、jQueryなどのライブラリを使います。jQueryをはじめとしたライブラリが用意してくれているアニメーションの機能は、裏側でこのような実装を行っているものがほとんどです(もしくはCSSによるアニメーションを利用します)。

この時、コマ送りの1コマ分のことを、1フレームといいます。1フレームを表示しておく時間を短くすればするほど、アニメーションがなめらかになります。jQueryでは、1フレームを13ミリ秒として扱っていますので、このサンプルでも同様に13ミリ秒にしました。それを指定しているのが、次の部分です。

// 1フレームは何ミリ秒にするか
var MILLISEC_PER_FRAME = 13;

1フレームが13ミリ秒ということは、1秒は1000ミリ秒ですので、1000÷13で約77フレーム毎秒ということになります*。77回のステップを経て、1秒のアニメーションが実現される計算です。

*注:フレームレート(fps)

このように単位時間に何フレームを表示する(処理するか)を表す値をフレームレートと呼ぶことがあります。通常は本文で計算しているように、1秒間を単位時間にしますので、Frames Per Second(fps)という表記もします。

このサンプルの場合、アニメーションを実行すると、13ミリ秒ごとにleftの値を変化させるような処理が走り、経過時間が1000ミリ秒を超えると、このアニメーションは止まるようになっています。

この値を小さくすればするほど、1フレームの表示時間が減るので、スムーズにアニメーションしているように見えます。

注:setTimeoutの正確性

MILLISEC_PER_FRAMEが13のところを、1にすれば、アニメ—ションが非常になめらかになるのではないかと想像されますが、実は、実際のところはそんなことはありません。1にしても、おそらくたいして変わっていないように見えるはずです。

これというのは、JavaScriptのsetTimeoutが、指定された処理を、その時間が経過した時に実行することを約束するものではないためです。setTimeoutで13ミリ秒後になにかの処理を実行させる場合でも、そのときに、他のさまざまな処理が走っており、ブラウザの処理が追いつかなかった場合は、その分、遅れます。

jQueryでたくさんのアニメーションを同時に走らせるような場合、アニメーションが遅れたりすることがありますが、それは、これが原因です。また、HTML5では、setTimeoutに4以下の数値を指定しても、それを4として扱うような仕様もあります(参考)。

また、ほとんどのブラウザは、アクティブでないタブで走っているJavaScriptの処理速度を、著しく落とします。このためsetTimeoutが刻む時間は、非常にゆっくりになります。setTimeoutが、完全に正確な時間を刻むわけではないという点は、頭の片隅にでも入れておくとよいでしょう。

ちなみに最近のブラウザには、よりアニメーションに利用しやすい、requestAnimationFrameという機能が備わっています。興味のある方は調べてみるとよいでしょう。

animate関数の実装

では、このアニメーションの実装を見てみましょう。animate関数の中身です。

  var leftVal = 0; // left値
  var valueInChange = 200; // 変化させるleft値
  var duration = 1000; // トータル時間
  var elapsedTime = 0; // 過ぎた時間

まずはアニメーションに必要な変数を用意します。valueInChangeは変化させたいleftの値、durationは、アニメーションにかける時間(ミリ秒)です。1000ミリ秒かけてleftの値を200にするため、これを変数として用意しておきます。elapsedTimeは、時間経過を保存しておくための変数、leftValは、leftの値を一時的に保存しておくための変数です。

時間の処理の実装

次は、この中で作っているtickという関数を見てみます。このtickは、1フレーム分の動きを表現しています。これが繰り返し呼ばれることでアニメーションになるのですが、まずはその時間の仕組みを実現している部分だけをみてみます。

  // 要素を動かす関数
  var tick = function() {

    // 1000ミリ秒経ってたら最終値をセットして終わり
    if(elapsedTime >= 1000) {
      ...
      return;
    }

    ....

    // 1フレーム分の時間を経過時間に加算
    elapsedTime += MILLISEC_PER_FRAME;
    // 1フレーム後に再び動かす
    setTimeout(tick, MILLISEC_PER_FRAME);

  };

  tick(); // 動かし始める

animate関数を呼ぶと、このtick関数が呼ばれます。そしてその中で最後、setTimeoutを使い、13ミリ秒後に再びtickを呼んでいます。このようにして、繰り返しtickが呼ばれ続けます。

しかし、このままだと永遠にループしてしまいますので、tickの処理の中で、経過時間を変数elaspsedTimeに加算していきます。これが1000を超えると、1秒経ったことになりますのでreturnしてループを止めます。

leftの値の変化の実装

では次に、このtickの中で、要素のleftを変化させている場所を見てみます。この部分は、イージングとも深く関わってくる部分です。

  var tick = function() {

    ...

    // 経過時間の割合 = 経過時間 / トータル時間
    var elapsedTimeRate = elapsedTime / duration;
    // 現在の値 = 変化させるleft値 * 経過時間の割合
    leftVal = valueInChange * elapsedTimeRate;

    // 要素のスタイルを更新
    $el.css('left', leftVal);

    ...

  };

経過時間であるelapsedTimeは、始めは0、1度tickが呼ばれると13、2度めは26、3度めは39…と加算されていき、これが1000(durationの値)を超えるとアニメーションは終わります。

経過時間であるelapsedTimeをトータル時間であるdurationで割ると、経過した時間の割合が計算できます。elapsedTimeは1000を超えることはないので、この値は0から1の間の値になります。これを変数elapsedTimeRateに保存しています。この値はアニメーション完了にかかる時間を1とした時、今どのくらいの時間が経ったのかという割合です。0〜1の範囲で、経過時間の割合を表現しています。

アニメーションが完了した時、leftの値は200(valueInChange)になります。均一にleftの値が増えていくようにするのならば、tickが呼ばれた時、設定すべきleftの割合は、最終的に指定する200に経過時間の割合を掛けあわせれば求めることができます。そして、その時点でのleftの値をスタイルとして要素に設定します。

これが、前述のコードで行っている処理です。

時間経過と値の変化をグラフにする

ここでもうひとつ、値の変化割合valueChangeRateという値を考えます。これはアニメーション完了時のleftの値200を1としたとき、現時点での値の割合です。200で1ですから、経過時間の割合と同様、0〜1の範囲で値の変化割合を考えます。

経過時間の割合(elapsedTimeRate)をX軸、値の変化の割合(valueChangeRate)をY軸とすると、次のようなグラフになります。X座標、Y座標ともに、最小値0、最大値1のグラフとなり、均一な変化のグラフとなります。

サンプル:03-graph

codegrid-easing/02-animation at gh-pages · pxgrid/codegrid-easing · GitHub

elapsedTimeRateと、valueChangeRateは、どのタイミングでも常に同じ値となっています。まぁ、まっすぐ動かしているだけなので当たり前ですが……。

ただ均一に値を変化させるだけのことについて、なにを細かく説明しているんだと感じられるかもしれません。ですが、今解説した考え方は、イージングを理解する上で重要です。

なぜならアニメーション中に「経過時間に応じて、どのように値の変化の割合を決めるか」というのが、イージングだからです。

変化の割合に変化を与える

では次に、変化の割合が均一ではないアニメーションを考えてみます。

次に示すのは、イージングeaseInQuadを適用した場合のグラフです。easeInQuadを適用したアニメーションは「最初ゆっくり、最後速く」になります。

サンプル:04-easeInQuad

codegrid-easing/04-easeInQuad at gh-pages · pxgrid/codegrid-easing · GitHub

この動きは、先ほどの変化が均一なアニメーションに、わずかばかりの変化を加えるだけで実現できます。このサンプルで値を変化させている部分は、次のようになっています。

// 経過時間の割合
var elapsedTimeRate = elapsedTime / duration;
// 値の変化の割合 = 経過時間の割合の2乗
var valueChangeRate = Math.pow(elapsedTimeRate, 2);
// 指定する値 = 変化させる量 * 値の変化の割合
leftVal = valueInChange * valueChangeRate;

Math.pow(x, num)は、xをnum乗した値を返します。

均一な変化のサンプルでは、経過時間の割合elapsedTimeRateと値の変化の割合valueChangeRateは、まったく同一でした。時間が半分進めば、経過時間の割合elapsedTimeRateは0.5、このとき、均一にアニメーションさせるのであれば、leftは半分まで進んでいるのですから、値の変化の割合valueChangeRateも0.5です。

しかし、今回はelapsedTimeRateを2乗したものを、変化の割合としています。ここでelapsedTimeRateは、アニメーション中、0〜1の範囲の値となります。1以下の数字を2乗すれば、それが小さい値であればあるほど、その結果の値は小さくなります。

例えば、elapsedTimeRateが0.5であるとき、ひとつ前のサンプル(03-graph)では、valueChangeRateも0.5でした。

ですが、今回のサンプルでは、それを2乗するのですから、0.25です。これは0.5の半分となりますので、均一な変化の時よりも、変化の割合が少ないということになります。

elapsedTimeRateが0.1であるとき、0.1の2乗ということは、その0.1倍の数となりますから、その結果は0.01。0.1と比較すると、ずっと小さい値です。

この値を大きくしていき、elapsedTimeRateが1に近い値の場合、例えば0.9のときを考えてみると、0.9の2乗は0.9の0.9倍ですから、0.81。0.9とはそうかけ離れた値にはなりません。そして、elapsedTimeRateが1であれば、1の2乗は1の1倍ですから、この値は1です。

このようにひとつずつ考えていくと、上記のように、ゆるやかに加速していくようなグラフとなり、このように値が変化していくアニメーションは「始めゆっくり、最後速く」となります。

このように経過時間の割合elapsedTimeRateに、なにかしらの処理を施し、値の変化の割合valueChangeRateを決定するという仕組みがイージングです。そして、この処理を担う関数のことをイージング関数と呼びます。今のサンプルに適用した処理をイージング関数として分けると、次のように表すことができます。

// イージング関数
easing = function(elapsedTimeRate) {
  return Math.pow(elapsedTimeRate, 2);
};

// 値の変化の割合
valueChangeRate = easing(elapsedTimeRate);

// leftの値 = 変化させるleft値 * 値の変化の割合
leftVal = valueInChange * valueChangeRate;

easeInQuadのquadはquadraticの略で、辞書によれば「二次の」とありました。二次関数を利用したイージングということでしょう。

ちなみに、始めのサンプルにあった均一な変化は、次のようなイージングで表すことができます。

easing = function(elapsedTimeRate) {
  return elapsedTimeRate;
};

経過時間の割合がそのまま値の変化の割合になるので、均一な変化となります。早い話、なにも変化を付けないということです。これは、linearという名前のイージングとして、始めからjQueryに内蔵されています。

まとめ

今回はイージングの仕組みをいくつかのデモを通して解説しました。イージングとはなにか、イージング関数の中でどんな処理が行われているのか、シンプルな例で見てもらいました。次回はこの基礎を踏まえた上で、引き続きイージングについて見ていきます。