github に repo つくった。
高速化してあるので下のコードとちょっと違う。デモも作ってみた。
いまさら js で DOM アニメーション。やってみると意外と簡単だった。
最初は setTimeout を使ってたんだけど最近使えるようになった requestAnimationFrame に切り替え。っても setTimeout の呼び出しを requestAnimationFrame に変えるだけだったんだけど。
あと jQuery.Deferred を使ってアレが終わったらコレしてねも実装。動きを書き下せるようになった。
まず下準備として requestAnimationFrame のブラウザサポートの差異を吸収する。
var requestAnimationFrame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| function (callee) {
return window.setTimeout(callee, 1000 / 60);
};
つぎ。アニメーション用の prototype を実装。 new するときに指定した要素に対してアニメーションを実行する。 start メソッドで動きと時間を指定して動作させる仕様。 animation メソッドはプライベートにしたほうがいいんだけど手抜き。
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 秒かけて浮かび上がるアニメーションが以下のように書ける。
var animation = new Animation(document.getElementById('image'));
animation.start({
action: disappear,
duration: 2000
}).then(function () {
return animation.start({
action: appear,
duration: 2000
});
});
移動や拡縮はけっこうめんどくさい。ざひょうけいさんしたくないです。以下の様なユーティリティ関数を用意しておく。
// 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 コマごとの位置を計算するときに使う、という塩梅。
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);
};
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 のように変化量を別関数で計算できるようにしたので、その分使いでがあるかもしれない。