ストレスの無いCSSアニメーションを

今日は、CSSアニメーション機能を担当する uu.tween.js のリライトをしていました。

uu.tween.js を組み込むと、CSS の色, サイズ, 位置を利用したアニメーションが可能になります。これ自体はよくある機能なのですが、他のライブラリにない特徴として、CSSプロパティ個別に easing 関数を割り当てられます。

デモ

http://pigs.sourceforge.jp/blog/20091216/
ノード3個 と 15個版があります。ノード数が違うだけで何身は一緒です。デモにある円弧軌道は、left, top に別々の easing 関数を割り当てて実現しています。

easing 関数

デフォルトの easing関数 は easeInOutQuad です。plug/easing/uup.easing.js を組み込むと他の関数を利用できます。

http://code.google.com/p/uupaa-js/source/browse/trunk/0.7/plug/easing/uup.easing.js

コード

http://code.google.com/p/uupaa-js/source/browse/trunk/0.7/uu.tween.js?r=207

計算量の最小化やループを排除するため、文字列を組み立て動的に関数を生成しています。初見だと複雑に見えると思いますが、やってることは非常にシンプルです。処理時間もたいしたことありません。

以下の _twloop が setInterval タイマーから起こされ、CSSプロパティを評価する部分です。

  function _twloop() {
    var q = node.uutweenq[0],
        tm = q.tm ? +new Date
                  : (q.js = _twjs(node, q.pa), q.tm = +new Date),
        fin = q.fin || (tm >= q.tm + q.ms);


    q.js(node, fin, tm - q.tm, q.ms); // call(node, finish, gain, ms)


    if (fin) {
      clearInterval(node.uutween);
    }
  }

_twloop から呼ばれる q.js は以下のようになっています。

function(node, fin, gain, ms) {
  var t, b, c, ms2 = ms / 2,
      ns = node.style;

  // node.style.left
  ns.left = (fin ? 400 : Math.easeOutBack(gain, 0, 400, ms)) + "px";

  // node.style.top
  ns.top = (fin ? 400 : (t = gain, b = 30, c = 370,
                         (t /= ms2) < 1 ? c / 2 * t * t + b : -c / 2 * ((--t) * (t - 2) - 1) + b)) + "px";

  // node.style.width
  var v = fin ? 200 : (t = gain, b = 100, c = 100,
                       (t /= ms2) < 1 ? c / 2 * t * t + b : -c / 2 * ((--t) * (t - 2) - 1) + b);
  v = v < 0 ? 0 : v;
  ns.width = v + "px";

  // node.style.backgroundColor
  var gms = gain / ms,
      hex = uu.dmz.HEX2;

  ns.backgroundColor = "#" +
       (hex[(fin ? 135 : (135 - 128) * gms + 128) | 0] || 0) +
       (hex[(fin ? 206 : (206 - 128) * gms + 128) | 0] || 0) +
       (hex[(fin ? 235 : (235 - 128) * gms + 128) | 0] || 0);
  
  // node.style.opacity
  var _2 = (t = gain, b = 1, c = -0.5,
            (t /= ms2) < 1 ? c / 2 * t * t + b : -c / 2 * ((--t) * (t - 2) - 1) + b);
  _2 = (_2 > 0.999) ? 1 : (_2 < 0.001) ? 0 : _2;
  ns.opacity = fin ? 0.5 : _2;

  // node.style.fontSize
  ns.fontSize = (fin ? 36 : (t = gain, b = 16, c = 20,
                             (t /= ms2) < 1 ? c / 2 * t * t + b : -c / 2 * ((--t) * (t - 2) - 1) + b)) + "px";
}

この戦略は、Ext.js の querySelectorAll が内部的にやっていることと大体おんなじです。

関数の中身は sprintf で作っています。

    _FMT = ['var t,b,c,ms2=ms/2,ns=node.style;',
            'var _2=%3$s;_2=(_2>0.999)?1:(_2<0.001)?0:_2;' + // opacity
            (uu.ie ? 'ns.filter=((_2>0&&_2<1)?"alpha(opacity="+(_2*100)+")":"");' +
                     'fin&&uu.css.opacity.set(node,%2$f)&&(ns.filter+=" %1$s");'
                   : 'ns.opacity=fin?%2$f:_2;'),
            'var gms=gain/ms,hex=uu.dmz.HEX2;' +
            'ns.%s="#"+(hex[(fin?%5$d:(%5$d-%2$d)*gms+%2$d)|0]||0)+' + // (bg)color
                      '(hex[(fin?%6$d:(%6$d-%3$d)*gms+%3$d)|0]||0)+' +
                      '(hex[(fin?%7$d:(%7$d-%4$d)*gms+%4$d)|0]||0);',
            'ns.%s=(fin?%f:%s)+"px";', // left, top, other
            'var v=fin?%2$f:%3$s;v=v<0?0:v;ns.%1$s=v+"px";']; // width, height

分かってしまえば実に単純なロジックですが、このロジックに到達するまで2年程時間が必要でした。
JavaScript ってやっぱり難しいですね。