一.動畫函數
動畫,是位移關於時間的函數: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,所以動畫也是位移關於完成度的函數
二.勻變速運動
同樣的道理,換掉勻變速運動位移公式中的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 and 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)
三.曲線運動
簡單的曲線運動可以分解成直線運動,例如正弦函數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的時代要怎麼計算位置?)
四.easing函數
對比上面得出的公式:
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));
}
};
其它複雜的easing,比如摩擦力(spring)、重力(bounce)等物理效果,常見的時間控制easing系列(各種Bezier曲線對應的緩動函數),step效果也是同樣的原理,修正完成度,也就是所謂的速度控制
五.線上Demo
通過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);
詳細見Demo:http://ayqy.net/temp/curve-path-animation.html
Demo過程中發現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,但沒什麼實際影響
參考資料
- 關於動畫,你需要知道的:最後一遍看月影前輩的這篇文章
暫無評論,快來發表你的看法吧