前段时间的一个周末,自己在家看jQuery源码解析的文章,动手写了一个粗糙的模仿jQuery的animate方法的一个插件,也顺带了解了animate方法有用到的缓动函数。因此以下内容就是基于本次练手的一些见解。
缓动函数的概念
缓动函数并不是从JavaScript这个语言发展出来的,而是从其他可以实现动画的语言搬运过来的,例如Flash。因为这只是个纯函数,所以在语言当中迁移也很容易。
jQuery自带两个缓动函数,分别是'swing'和'linear',其中swing是默认的缓动函数,我们可以把它改成'linear'或是其他缓动函数,不过其他缓动函数还需引入第三方动画库。现在我们来看看这些缓动函数都是什么样子的。
var linear = function( t ) { return t;};var swing = function( t ) { return 0.5 - Math.cos( t * Math.PI ) / 2;};
以上两个是jQuery自带的,再看两个来自jQuery.easing.js的。
var easeInOutQuad = function (t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow( -2 * t + 2, 2 ) / 2;};var easeInOutSine = function (t) { return -( Math.cos( Math.PI * t ) - 1 ) / 2;};
以上四个函数有个共同的特点。假如有一个平面坐标系,传入的参数表示x坐标,返回值表示y坐标,那么这个函数表示的连续的曲线一定会穿过(0, 0)和(1, 1)两个点。在jQuery的animate方法这个体系里面,传入的参数就是表示当前已消耗的时间 / 动画持续的总时间,返回值是当前已经过的路程 / 一共需要经过的路程。这里用位移表示,其实换成尺寸也是成立的。因此这个函数表示的曲线一定会穿过(0, 0)和(1, 1)两个点也就不难理解了。这个函数只要能满足穿过这两个点就可以了,因此有时候会看到动画有回弹效果,就是由于函数曲线某个点的y坐标值大于1,之后运动物体的速度变为负值(即向反方向运动),移动到终点(1, 1)。
然而,从其他语言搬运过来的缓动函数其实是有四个参数的,分别表示动画已消耗的时间、物体初始位置、物体目标位置、动画持续的总时间,返回值是物体当前处在的位置。以上的值都是绝对值,之前的那种缓动函数值是相对值。看下传入四个参数的缓动函数。
var easeIn = function(t, b, c, d) { return c * (t /= d) * t + b;};
关于以上两种缓动函数的用法,将在下文“缓动函数的应用”提到。
jQuery动画其实最多能传入5个参数的,就是以上两种函数参数合并起来。在轮播插件superslide中,作者在最后有提供了几个缓动函数,例如:
var easeInQuad = function (x, t, b, c, d) {return c*(t/=d)*t + b;};
虽说jQuery动画最多能传5个参数,但要么只用第1个,要么就用最后四个,不能同时使用的。
缓动函数的概念讲完了,接下来介绍其用法。
缓动函数的应用
还是以jQuery的animate方法为例。在不支持原生的requestAnimationFrame函数的浏览器里面,只能通过使用setInterval函数来实现动画。这里我们干脆都有setInterval函数来实现动画好了。
假设下面的代码是改变dom元素的 ‘left’ 样式属性的动画函数,频率是60帧/秒,作匀速直线运动。
var interval = 16;var dom = document.getElementById('box');dom.style.left = 0;var initialLeft = 0;var distLeft = 300; //表示元素最后运动到的位置var duration = 1600;var count = 0; //统计已经实现了多少帧var timer = setInterval(function() { dom.style.left += (disLeft - initialLeft) / durtaion; count++; if (count >= duration / interval) { clearInterval(timer); }}, interval);
理论上,16ms一帧,一共执行1.6s,那就是100帧,所以实现了一帧后,计数器加1,直到计数器值为100,此时动画执行完毕。实际上,等100帧执行完后,这个时间很可能已经超过1.6s了。因为JavaScript是单线程的,要执行下一帧动画的时候可能任务队列里面还有其他代码任务要执行,所以执行帧动画的时间一般是要比预期的时间来得迟一些。如果一个页面有多个帧动画,那就更糟糕了,可能就会出现肉眼可见的掉帧的情况。
如果不用计数的方式来获取clearInterval的时机,那还有别的办法吗?
虽然每一帧的执行时间不是固定的,但动画总的持续时间是固定的。在实现下一帧之前,计算还剩多少路程和时间,两者相除再乘以16,加上已经移动的路程,就是元素在下一帧应该出现在的位置。代码如下:
var interval = 16;var dom = document.getElementById('box');dom.style.left = 0;var initialLeft = 0;var distLeft = 300; //表示元素最后运动到的位置var duration = 1600;var startTime = +new Date();var curTime;var timer = setInterval(function() { curTime = +new Date(); if (curTime - startTime < duration) { dom.style.left += (distLeft - dom.style.left) / (curTime - startTime) * interval; }}, interval);
这样的话,每次实现下一帧动画,都会重新计算元素的偏移量,可以保证基本上在规定的时机运动到规定的位置。也许还会有些偏差,但这个一般在1px以内,clearInterval的时候顺带修正就可以了。
但是这个代码还是有个问题,就是不灵活,只能实现匀速运动,如果是变速呢?这时候就要用到缓动函数了。先用传入一个参数的缓动函数看看。
var easing = { swing: function( t ) { return 0.5 - Math.cos( t * Math.PI ) / 2; }};
前面有提到,参数t表示当前已消耗的时间 / 动画持续的总时间。t对应上面改变元素位置的那行代码的什么?就是
(curTime - startTime) / duration;
我们把上面改变元素位置的那行代码拿去调整:
dom.style.left = initialLeft + (distLeft - initialLeft) * easing.swing((curTime - startTime) - duration); //代码1
如果用的是传四个参数的缓动函数呢?还是像上面缓动函数的参数一一对应一样。
var easing = { linear: function(t, b, c, d) { return b + (c - b) * t / d; }};dom.style.left = easing.linear((curTime - startTime), initialLeft, disLeft, duration); //代码2
然后比较代码1和代码2,再补充从一个参数的缓动函数变成四个参数的缓动函数的方法:
var easing = { swing: function(p) { return 0.5 - Math.cos( p * Math.PI ) / 2 }};//-->var easing = { swing: function(t, b, c, d ) { return b + (c - b) * (function(p) { return 0.5 - Math.cos( p * Math.PI ) / 2; })(t / d); }};
因为四个参数的缓动函数中的 t / d就是
(curTime - startTime) / duration;
而前面的 b + (c - b) * x是固定的,有变化的只是这个x。这样拿到一个只有一个参数的缓动函数时,我们可以很简单地改成四个参数的缓动函数。
看到这里,大家也对缓动函数有所了解,也能够自己写一个缓动函数了。