16
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-08-25

github に repo つくった。

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


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

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

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

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

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

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

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 関数内で変化を計算してね、という仕様。ダックタイピング楽だ。

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

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

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
    });
});

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

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 コマごとの位置を計算するときに使う、という塩梅。

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);
};
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 秒で拡大しろというのは以下のように書き下せるようになった。

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 のように変化量を別関数で計算できるようにしたので、その分使いでがあるかもしれない。

16
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
16