1. アニメーション関数
アニメーションとは、変位を時間の関数として表したものです:s = f(t)
独立変数はt、従属変数はsであり、物体の変位が時間とともに変化することでアニメーションに見えます。例えば:
// 已知
var property = 'marginLeft';
var s0 = 100; // 起点
var s1 = 200; // 终点
var duration = 1000;
// 由题意得
var S = s1 - s0; // 总位移
var T = duration; // 总时间
// 求任意时刻t对应的位移
var t0 = +new Date();
var tick, interval = 1000 / 60;
setTimeout(tick = function() {
var t = +new Date() - t0;
// 完成度
var p = Math.min(t / T, 1);
// t时刻相对起点的位移s
var s = S * p;
document.body.style[property] = s0 + s + 'px';
if (p !== 1) setTimeout(tick, interval);
}, interval);
marginLeft が 100px から 200px まで均等に変化し、body がまず右に 100px ジャンプした後、1秒かけて等速で右に 100px 移動します。
このようなアニメーションを実現するために直面する唯一の問題は、総変位Sと総時間Tが既知であるとき、任意の時刻tにおける起点からの変位sを求めることです。
等速直線運動を実装しましたが、s = vt を使っていないように見えて、実は使っています:
s = v * t
= (S / T) * t
= S * (t / T)
= S * p
アニメーション関数は s = f(t) であり、その中に v は含まれないため、v を既知の量に置き換える必要があります。進捗度 p = t / T であるため、アニメーションは変位が進捗度の関数であるとも言えます。
2. 等加速度運動
同様に、等加速度運動の変位公式における v と a を置き換え、変位 s を時間 t の関数として求めます。
等加速
変位公式:
// v0 = 0时,只有一个未知量a
s = 1/2at^2
総時間T、総変位S、進捗度p = t / Tが既知であるとき、任意の時刻tにおける起点からの変位sを求めます:
// 终点处有
S = 1/2 * a * T^2
// 得
a = 2S / T^2
// 任意时刻
s = 1/2 * a * t^2
= 1/2 * (2S / T^2) * t^2
= 1/2 * 2S * (t^2 / T^2)
= S * p^2
等減速
変位公式:
// 含有2个未知量v0和a
s = v0t - 1/2at^2
総時間T、総変位S、進捗度p = t / Tが既知であるとき、任意の時刻tにおける起点からの変位sを求めます:
// 1.逆向匀加速求v0
// 起点处有
S = 1/2 * a * T^2
// 得
a = 2S / T^2
v0 = aT = 2S / T^2 * T = 2S / T
// 2.任意时刻
s = v0 * t - 1/2 * a * t^2
= (2S / T) * t - 1/2 * (2S / T^2) * t^2
= 2S * (t / T) - S * (t^2 / T^2)
= 2S * p - S * p^2
= S * p * (2 - p)
3. 曲線運動
単純な曲線運動は直線運動に分解できます。例えば、正弦関数 y = sinx は以下のように分解できます:
// x轴匀速直线
x = S * p = 2PI * p
// y轴sinx
y = sinx = sin(2PI * p)
水平投射運動は以下のように分解できます:
// x轴匀速直线
x = S * p = X * p
// y轴匀加速
y = S * p^2 = Y * p^2
放物線の左半分は、左方向への水平投射の逆運動とみなすことができます:
// x轴匀速直线
x = S * p = X * p
// y轴匀减速
y = S * p * (2 - p) = Y * p * (2 - p)
円運動は少し特殊です。代数方程式 (x - a)^2 + (y - b)^2 = r^2 では、x, y の計算で符号の問題が発生します(面倒ですが可能です)。そのため、媒介変数表示(パラメータ方程式)を検討します:
// 对于圆上任意一点,有
sinθ = y / r, cosθ = x / r
// 得参数方程
x = a + r * cosθ, y = b + r * sinθ
// 圆心为(0, 0)时
x = r * cosθ = r * cos(θ * p), y = r * sinθ = r * sin(θ * p)
角度 [0, 2PI] が均一に変化し、それに応じて x, y が変化します。
P.S. 円運動を極座標で説明するのは少し強引かもしれません(r(θ) = r とし、中心を設定して [0, 360] 度を均一に rotate させる場合、transform がなかった時代はどうやって位置を計算していたのでしょうか?)。
4. イージング関数
上記で得られた公式を比較します:
s = S * p // 匀速
s = S * p^2 // 匀加速
s = S * p * (2 - p) // 匀减速
s = S * cos(2PI * p) // cos
s = S * sin(2PI * p) // sin
総変位 S は変わらず、後ろの部分が異なっていることがわかります。したがって:
var easings = {
linear: function(p) { return p; },
acceleration: function(p) { return p * p; },
deceleration: function(p) { return p * (2 - p); },
sin: function(p) { return Math.sin(2 * Math.PI * p); },
cos: function(p) { return Math.cos(2 * Math.PI * p); }
}
これらの easing 関数は p を補正するために使用されるため、アニメーションは以下のようになります:
// 任意时刻t对应的位移
st = s0 + S * easing(p)
// 即
// 当前值 = 初始值 + totalDelta * easing函数修正后的完成度
p = t / T であるため、実際には easing は t に作用します。これは時間制御関数(timingFunction)とも呼ばれます。
アニメーションライブラリはすべてこのように動作します。例えば jQuery :
// from https://github.com/jquery/jquery/blob/2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/effects/Tween.js
jQuery.easing = {
linear: function( p ) {
return p;
},
swing: function( p ) {
return 0.5 - Math.cos( p * Math.PI ) / 2;
},
_default: "swing"
};
velocity :
// from https://github.com/ayqy/velocity-1.4.1/blob/master/velocity.js
Velocity.Easings = {
linear: function(p) {
// 线性,直接返回完成度
return p;
},
swing: function(p) {
// 两头慢中间快,cos从+1到-1变化,中间斜率最大变化最快
return 0.5 - Math.cos(p * Math.PI) / 2;
},
/* Bonus "spring" easing, which is a less exaggerated version of easeInOutElastic. */
spring: function(p) {
// easeInOutElastic的温和版
return 1 - (Math.cos(p * 4.5 * Math.PI) * Math.exp(-p * 6));
}
};
摩擦力(spring)や重力(bounce)などの物理効果を伴う他の複雑な easing、一般的な時間制御 easing シリーズ(各種ベジェ曲線に対応するイージング関数)、step 効果も同じ原理です。進捗度を補正すること、つまりいわゆる速度制御です。
5. オンラインデモ
velocity でカスタム easing と Redirects を使用して、これらの曲線軌道を実現します。例えば:
// 自定义缓动函数
// 匀加速
Velocity.Easings.acceleration = function (p, opts, tweenDelta) {
return p * p;
};
// 匀减速
Velocity.Easings.deceleration = function (p, opts, tweenDelta) {
return p * (2 - p);
};
// 自定义动画效果
Velocity.Redirects['throw-h'] = function(element, options, elementsIndex, elementsSize, elements, promiseData) {
Velocity(this, {
translateX: [300, 'linear', 0],
translateY: [300, 'acceleration', 0]
}, options);
};
// run
Velocity(document.body, 'throw-h', 3000);
詳細はデモを参照:http://ayqy.net/temp/curve-path-animation.html
デモの過程で、 velocity は進捗度が 1 になったときに終点の値を強制的に代入することがわかりました。これは sin のようなケースでは問題が発生します。ソースコードは以下の通りです:
else if (percentComplete === 1) {
// 已完成,手动赋值,确保终点准确(不受计算精度影响)
//!!! 不应该手动赋值终点了,因为sin之类的,终点是0
//!!! 强制赋值就错了
/* If this is the last tick pass (if we've reached 100% completion for this tween),
ensure that currentValue is explicitly set to its target endValue so that it's not subjected to any rounding. */
// currentValue = tween.endValue;
currentValue = tween.currentValue;
}
修正方法は、 easing 関数を信頼することです(上記のコード部分を削除します)。終点においても easing によって現在の値を計算します。この方法の欠点は計算精度の問題があること(例えば sin の 2PI は 0 ですが、計算結果が 0 ではなく極小値になるなど)ですが、実用上の影響はほとんどありません。
参考文献
- アニメーションについて知っておくべきこと:月影(Yueying)先輩のこの記事を改めて読み返しました
コメントはまだありません