DOM アニメーション用クラス

github に repo つくった。

https://github.com/januswel/animation.js

高速化してあるので下のコードとちょっと違う。デモも作ってみた。

---

いまさら js で DOM アニメーション。やってみると意外と簡単だった。

最初は setTimeout を使ってたんだけど最近使えるようになった requestAnimationFrame に切り替え。っても setTimeout の呼び出しを requestAnimationFrame に変えるだけだったんだけど。

あと jQuery.Deferred を使ってアレが終わったらコレしてねも実装。動きを書き下せるようになった。

まず下準備として requestAnimationFrame のブラウザサポートの差異を吸収する。

```js:requestAnimationFrame.js
var requestAnimationFrame = window.requestAnimationFrame
    || window.webkitRequestAnimationFrame
    || window.mozRequestAnimationFrame
    || function (callee) {
        return window.setTimeout(callee, 1000 / 60);
    };
```

つぎ。アニメーション用の prototype を実装。 new するときに指定した要素に対してアニメーションを実行する。 start メソッドで動きと時間を指定して動作させる仕様。 animation メソッドはプライベートにしたほうがいいんだけど手抜き。

```js:Animation.js
var Animation = function Animation(element) {
    this.element = element;
}
Animation.prototype = {
    start: function (options) {
        var dfd = $.Deferred();

        this.endTime = Date.now() + options.duration;
        this.options = options;
        this.animation(options.action, options.duration, dfd);
        return dfd.promise();
    },
    animation: function (action, duration, dfd) {
        var progress = 1 - (this.endTime - Date.now()) / duration;
        if (progress < 1) {
            requestAnimationFrame(this.animation.bind(this, action, duration, dfd));
            action(this.element, progress, this.options);
        } else {
            action(this.element, 1, this.options);
            delete this.endTime;
            delete this.options;
            return dfd.resolve();
        }
    }
};
```

start メソッドに渡すオブジェクトは action と duration というプロパティを必須としていて、 action にアニメーションの 1 コマごとの処理をする関数を、 duration にアニメーションする時間を指定する。必須としていて、と言っておきながら何もチェックしてないのは仕様。

action に指定する関数の例は以下のとおり。第 2 引数に渡される progress というのが進行率を表していてこれをもとに action 関数内で変化を計算してね、という仕様。ダックタイピング楽だ。

```js:
var disappear = function (element, progress) {
        element.style.opacity = 1 - progress;
    },
    appear = function (element, progress) {
        element.style.opacity = progress;
    };
```

こうしておくと 2 秒かけて消えたあと 2 秒かけて浮かび上がるアニメーションが以下のように書ける。

```js:opacity-animation.js
var animation = new Animation(document.getElementById('image'));

animation.start({
    action: disappear,
    duration: 2000
}).then(function () {
    return animation.start({
        action: appear,
        duration: 2000
    });
});
```

移動や拡縮はけっこうめんどくさい。ざひょうけいさんしたくないです。以下の様なユーティリティ関数を用意しておく。

```js:utility.js
// utility functions
var center = function (element, coordinates) {
        if (!coordinates) {
            return {
                x: element.offsetLeft + element.offsetWidth / 2,
                y: element.offsetTop + element.offsetHeight / 2,
            };
        }
        element.style.left = (coordinates.x - element.offsetWidth / 2).toString(10) + 'px';
        element.style.top = (coordinates.y - element.offsetHeight / 2).toString(10) + 'px';
    },
    size = function (element, size) {
        if (!size) {
            return {
                width: element.offsetWidth,
                height: element.offsetHeight
            };
        }
        element.style.width = size.width.toString(10) + 'px';
        element.style.height = size.height.toString(10) + 'px';
    },
    sigmoid = function (x, a) {
        return 1 / (1 + Math.exp(-a * x));
    },
    easeInOut = function (x) {
        return sigmoid(x - 0.5, 30);
    };
```

action の定義は以下。 action に指定した関数には第 3 引数としてオブジェクトを提供していて、アニメーションに必要な情報の退避場所にこれを使っている。初回にもとの座標やなんかを退避して、 1 コマごとの位置を計算するときに使う、という塩梅。

```js:move.js
var move = function (element, progress, options) {
    // default settings
    // if function for displacement is not assigned, move linearly
    if (!('displacementFunction' in options)) {
        options.displacementFunction = function (x) {
            return x;
        };
    }
    if (!('vector' in options)) {
        if (options.destination) {
            options.vector = {
                x: options.destination.x - element.offsetWidth / 2,
                y: options.destination.y - element.offsetHeight / 2
            }
        } else {
            options.vector = {
                x: 100,
                y: 100
            };
        }
    }

    // initialize
    if (!('baseCoordinates_' in options)) {
        options.baseCoordinates_ = center(element);
    }

    var ratio = options.displacementFunction(progress),
        coordinates = {
        x: options.baseCoordinates_.x + options.vector.x * ratio,
        y: options.baseCoordinates_.y + options.vector.y * ratio
    };

    center(element, coordinates);
};
```

```js:scaling.js
var scaling = function (element, progress, options) {
    // default settings
    if (!('scale' in options)) {
        options.scale = 2;
    }

    // initialize
    if (!('baseSize_' in options)) {
        options.baseCoordinates_ = center(element);
        options.baseSize_ = {
            width: element.offsetWidth,
            height: element.offsetHeight
        };
        options.diffScale_ = options.scale - 1;
    }

    var ratio = options.diffScale_ * progress + 1,
        scaledSize = {
            width: options.baseSize_.width * ratio,
            height: options.baseSize_.height * ratio
        };
    size(element, scaledSize);
    center(element, options.baseCoordinates_);
};
```

これでイーズインとイーズアウトしながら 2 秒で画面中央まで行って 2 倍に 1 秒で拡大しろというのは以下のように書き下せるようになった。

```js:
animation.start({
    action: move,
    displacementFunction: easeInOut,
    duration: 2000,
    destination: screenCenter
}).then(function () {
    return animation.start({
        action: scaling,
        scale: 2,
        duration: 1000
    });
});
```

よーし拡縮までできたぞあとは回転でスーファミに追いつくッ !! というところで CSS3 の transition と transform に気づいてやる気が消滅したというハナシ。

ただまぁ jQuery.Deferred 使って実装したので then や when を使えたり、 move のように変化量を別関数で計算できるようにしたので、その分使いでがあるかもしれない。