これでできる! クロスブラウザJavaScript入門

第17回アニメーションの基礎

こんにちは、太田です。3回ほどJavaScriptの基礎的な内容が続いたので、今回はグラフィカルなアニメーションについて解説します。グラフィカルなJavaScriptというと、難しい・応用的なイメージがあるかもしれませんが、JavaScriptでのアニメーションとはすなわちCSSを段階的に操作するということで、そのポイントを抑えれば中身は単純なものです。

アニメーションの前提知識

HTMLにおける通常のアニメーションを構成するのは、⁠特定の要素」に対して、⁠そのCSSプロパティ」「ある時点からある時点まで」の時間の中で、⁠ある値からある値に操作」する、といった要素です。

特定の要素と、CSSプロパティについては問題ないと思います。問題となるのは「時間と値の操作」です。

まず、単純にインクリメントするだけというサンプルを見てみましょう。

良くないアニメーション
var y = 0;
var element = document.getElementById('cbjs-17-1');
var id = setInterval(function(){
  y++;
  element.style.top = y/10 + 'px';
  if (y === 1000){
    clearInterval(id);
  }
}, 1); // 1ms置きに実行

単純計算で言えば、1ms置きに1000回インクリメントしているので、1秒間の処理となるはずですが、実際には1秒間に1000回もの描画(1000FPS)を行うことはまず不可能です。このままでは実行される環境に大きく依存した(低スペックな環境では極端に遅い)アニメーションになってしまいます。

そこで、開始時刻から経過した時間で現在の位置を決め、その位置に移動させる方法が一般的です。

つまり、1秒間で100px移動させたい場合なら、300msの時点では30px移動していればよく、500msでは50px、1000msを超えたら100pxにしてアニメーションを終了すればよい、ということです。時間を基準にすれば、時間通りにアニメーションが終わるというごく当たり前なことです。

修正版アニメーション
var begin = new Date() - 0;
var element = document.getElementById('cbjs-17-2');
var id = setInterval(function(){
  var current = new Date() - begin;
  if (current > 1000){
    clearInterval(id);
    current = 1000; // 1000以上になっているので、調整する
  }
  element.style.top = current / 10 + 'px';
}, 10); // 10ms置きに実行

この方法ではハイスペックな環境では滑らかなアニメーションに、ロースペックな環境では最低限のアニメーションになります。

アニメーションの汎用化とEasing関数

さて、上記の実装では汎用性がないので、もう少し使い回しができるようにしてみましょう。特に、直線的な動きだけでなく、動きにバリエーションを増やしたいと思います。その際によく使われるのがEasing関数と呼ばれるものです。

Easing関数とは、⁠主に)経過時間、初期値、変動値、継続時間の4つの変数から現在の値を求めることができる関数です。

Easing関数版アニメーション
function easing(time, from, distance, duration){
  return distance * time / duration + from;
}
var begin = new Date() - 0;
var element = document.getElementById('cbjs-17-3');
var from = 0; // 初期値
var distance = 100; // 変動値
var duration = 1000; // 継続時間
var id = setInterval(function(){
  var time = new Date() - begin; // 経過時間
  var current = easing(time, from, distance, duration);
  if (time > duration){
    clearInterval(id);
    current = from + distance;
  }
  element.style.top = current + 'px';
}, 10); // 10ms置きに実行

easing関数は自作もできますが、よく使われるのはActionScriptのライブラリであるTweenerがサポートしている関数群です。Tweener Documentation and Language Referenceにそのサンプルがあります。

JavaScriptから扱う場合はTweenerのJavaScriptポートである、JSTweenerにその関数群が実装されています。

easeOutBounce版アニメーション
var easeOutBounce = JSTweener.easingFunctions.easeOutBounce;
var begin = new Date() - 0;
var element = document.getElementById('cbjs-17-3');
var from = 0; // 初期値
var distance = 100; // 変動値
var duration = 1000; // 継続時間
var id = setInterval(function(){
  var time = new Date() - begin; // 経過時間
  var current = easeOutBounce(time, from, distance, duration);
  if (time > duration){
    clearInterval(id);
    current = from + distance;
  }
  element.style.top = current + 'px';
}, 10); // 10ms置きに実行

アニメーションのライブラリ化

さて、実際にアニメーションさせたいときはjQueryやJSTweenerなどを使えばよいのですが、折角なのでシンプルなライブラリにしてみましょう。ライブラリといっても、汎用的なアニメーション関数程度のものです。

ライブラリ化にあたって、1つのタイマー(setInterval)だけで動くように注意します。これは、ブラウザによって(主にIEにおいて)はパフォーマンスに大きな差が出るためです。

アニメーション関数
/**
 *  アニメーション関数
 *  @param target 対象オブジェクト(nodeのstyleなど)
 *  @param properties プロパティの定義
 *  @param options アニメーションのオプション(省略可)
 */
function MiniTweener(target, properties, options){
  // 入れ物となるオブジェクトを用意
  var item = {};
  item.target = target;
  item.properties = properties;
  item.options = options || {};
  // プロパティの入れ物も用意
  item.props = [];
  for (var name in properties){
    // 各プロパティをオブジェクトにまとめ直して配列に挿入
    // プロパティごとに初期値と最終値、接頭辞、接尾辞を持つ
    var prop = properties[name];
    item.props.push({
      name    :name,
      from    :prop.from,
      to      :prop.to,
      distance:prop.to - prop.from,
      prefix  :prop.prefix || '',
      suffix  :prop.suffix || 'px'
    });
  }
  // アニメーションのオプション定義
  item.duration = item.options.duration || 1000;
  item.easing = item.options.easing || MiniTweener.easing;
  item.onComplete = item.options.onComplete;
  // 開始時刻
  item.begin = new Date() - 0;
  // アニメーションの定義セットを配列に入れる
  MiniTweener.Items.push(item);
  if (!MiniTweener.timerId){
    // アニメーションが開始していなければ開始
    MiniTweener.start();
  }
  return item;
}
MiniTweener.easing = function(time, from, dist, duration){
  return dist * time / duration + from;
};
MiniTweener.FPS = 60;
MiniTweener.Items = [];
MiniTweener.loop = function(){
  var items = MiniTweener.Items;
  var now  = new Date() - 0; // 現在時刻
  var time;
  var n = items.length;
  // アニメーションが終了した時に定義を配列から削除するので、
  // 削除しても添え字に影響が出ないように配列を後ろから走査する
  while (n--){
    var item = items[n];
    // 経過時刻
    time = now - item.begin;
    if (time < item.duration){
      // 各プロパティの途中経過を求め、反映
      for(var i = 0; i < item.props.length;i++){
        var prop = item.props[i];
        var current = item.easing(time, prop.from,
                      prop.distance, item.duration);
        item.target[prop.name] = prop.prefix +
                                 current + prop.suffix;
      }
    } else {
      // 最終的な値を設定
      for(var i = 0; i < item.props.length;i++){
        var prop = item.props[i];
        item.target[prop.name] = prop.prefix +
                                 prop.to + prop.suffix;
      }
      // 終了後のコールバック
      if (item.onComplete){
        item.onComplete();
      }
      // 配列から削除
      items.splice(n, 1);
    }
  }
  if (!items.length){
    // アニメーション定義が空になっていたら停止
    MiniTweener.end();
  }
};
MiniTweener.start = function(){
  // TimerのIDを格納しておく
  MiniTweener.timerId = setInterval(MiniTweener.loop,
                        1000 / MiniTweener.FPS);
};
MiniTweener.end = function(){
  MiniTweener.Items = [];
  // タイマーを停止
  clearInterval(MiniTweener.timerId);
  delete MiniTweener.timerId;
};

少々長いですが、コメントなどを除けば70行程度しかありません。また、ブラウザごとの分岐処理などもありません(実際にCSSを操作する上ではIE用の対応などが必要になることは多いですが⁠⁠。

なお、記述がシンプルになるのでアニメーション関数のプロパティとしてメソッドや配列などを定義していますが、もちろん通常のオブジェクトに各メソッドを定義する実装でも構いません。

この関数を使うときは次のように呼び出します。

アニメーション関数の使い方
MiniTweener(element.style, {
  top  :{from:100, to:200},
  left :{from:50 , to:100, suffix:'%'},
},{
  duration: 500,
});

第一引数に操作対象の要素のstyleオブジェクトを渡し、第2引数でそのプロパティと値を定義して、第3引数でアニメーションの定義を決定しています。

最後に簡単なアニメーション・デモを紹介します。

アニメーションのサンプル
var node = document.getElementById('cbjs-17-5');
var timerId = setInterval(function(){
  var i = node.cloneNode(true);
  // ランダムな文字を生成
  var s = Math.floor(Math.random()*36).toString(36);
  i.appendChild(document.createTextNode(s));
  node.parentNode.appendChild(i);
  // アニメーション適用
  MiniTweener(i.style, {
    top     :{from:100,to:200*Math.random()},
    left    :{from:50 ,to:100*Math.random(), suffix:'%'},
    fontSize:{from:50 ,to:300*Math.random(), suffix:'%'}
  }, {
    duration: Math.random()*1000,
    onComplete:function(){
      // アニメーションが終わった要素を削除
      node.parentNode.removeChild(i);
    }
  });
}, 10);

まとめ

今回はアニメーションの基礎とそれをベースにライブラリ化を行ってみました。次回はアニメーションの実用的なケースとCSS3に関係した部分について解説します。

おすすめ記事

記事・ニュース一覧