一.动画函数
动画,是位移关于时间的函数: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和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,但没什么实际影响
参考资料
- 关于动画,你需要知道的:最后一遍看月影前辈的这篇文章