javascript的缓动效果(第2部分)
这部分对原先的缓动函数进行抽象化,并结合缓动公式进行强化。成品的效果非常惊人逆天。走过路过不要错过。
好了,打诨到此为止。普通的加速减速是难以让人满意的,为了实现弹簧等让人眼花缭乱的效果必须动用缓动公式。我见过两套缓动公式,一套是早期Robert Penner大神的缓动公式,内置到tween类中,不过现在人们越来越推荐tweenlite这个新秀了。另一套是script.aculo.us与mootools里面的,由于mootools可称之为prototype的升级版,script.aculo.us则是基于prototype,我们就把它们并称为prototype流派。与flash流派最大的不同是,它们封装得更好,并只需传入一个参数(0~1的小数),并且拥有严密的队列机制来调用各种回调函数。如在回调函数设置元素的长宽,就弄成Scale特效,利用它我们进一步制作SlideUp,SlideDown,Squish等复合特效。
我们先来看flash流派的缓动公式,它们基本都有如下四个参数。
- t:timestamp,指缓动效果开始执行到当前帧开始执行时经过的时间段,单位ms
- b:beginning position,起始位置
- c:change,要移动的距离,就是终点位置减去起始位置。
- d: duration ,缓和效果持续的时间。
我们把这四个参数传入Robert Penner大神的缓动公式,它就会计算出当前帧物体移动的位置。我们对比原来的函数来改写。
var transition = function(el){ transition.linear = function(t,b,c,d){ return c*t/d + b; };//免费提供一个缓动公式(匀速运动公式) el.style.position = "absolute"; var options = arguments[1] || {}, begin = getCoords(el).left,//开始位置 change = parseFloat(getStyle(_("taxiway"),"width")) - parseFloat(getStyle(el,"width")),//要移动的距离 duration = options.duration || 500,//缓动效果持续时间 ease = options.ease || transition.linear,//要使用的缓动公式 end = begin + change,//结束位置 startTime = new Date().getTime();//开始执行的时间 (function(){ setTimeout(function(){ var newTime = new Date().getTime(),//当前帧开始的时间 timestamp = newTime - startTime;//逝去时间 el.style.left = ease(timestamp,begin,change,duration) + "px";//移动 if(duration <= timestamp){ el.style.left = end + "px"; }else{ setTimeout(arguments.callee,25);//每移动一次停留25毫秒 } },25) })() }
接着是各种缓动公式大阅兵,共分为十一大类,除了linear。其他类又分为三种。
- easeIn方法控制补间如何从开始到最高速度。
- easeOut 方法控制补间减速并停在目标位置
- easeInOut方法同时控制上述两者。
具体公式见下面(共31种)。
//***********@author:Robert Penner and cloudgamer************* //http://www.cnblogs.com/cloudgamer/archive/2009/01/06/Tween.html var Tween = { Linear: function(t,b,c,d){ return c*t/d + b; }, Quad: { easeIn: function(t,b,c,d){ return c*(t/=d)*t + b; }, easeOut: function(t,b,c,d){ return -c *(t/=d)*(t-2) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t + b; return -c/2 * ((--t)*(t-2) - 1) + b; } }, Cubic: { easeIn: function(t,b,c,d){ return c*(t/=d)*t*t + b; }, easeOut: function(t,b,c,d){ return c*((t=t/d-1)*t*t + 1) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t*t + b; return c/2*((t-=2)*t*t + 2) + b; } }, Quart: { easeIn: function(t,b,c,d){ return c*(t/=d)*t*t*t + b; }, easeOut: function(t,b,c,d){ return -c * ((t=t/d-1)*t*t*t - 1) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t*t*t + b; return -c/2 * ((t-=2)*t*t*t - 2) + b; } }, Quint: { easeIn: function(t,b,c,d){ return c*(t/=d)*t*t*t*t + b; }, easeOut: function(t,b,c,d){ return c*((t=t/d-1)*t*t*t*t + 1) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; return c/2*((t-=2)*t*t*t*t + 2) + b; } }, Sine: { easeIn: function(t,b,c,d){ return -c * Math.cos(t/d * (Math.PI/2)) + c + b; }, easeOut: function(t,b,c,d){ return c * Math.sin(t/d * (Math.PI/2)) + b; }, easeInOut: function(t,b,c,d){ return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; } }, Expo: { easeIn: function(t,b,c,d){ return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; }, easeOut: function(t,b,c,d){ return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; }, easeInOut: function(t,b,c,d){ if (t==0) return b; if (t==d) return b+c; if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; } }, Circ: { easeIn: function(t,b,c,d){ return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; }, easeOut: function(t,b,c,d){ return c * Math.sqrt(1 - (t=t/d-1)*t) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; } }, Elastic: { easeIn: function(t,b,c,d,a,p){ if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; if (!a || a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; }, easeOut: function(t,b,c,d,a,p){ if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; if (!a || a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); return (a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b); }, easeInOut: function(t,b,c,d,a,p){ if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); if (!a || a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; } }, Back: { easeIn: function(t,b,c,d,s){ if (s == undefined) s = 1.70158; return c*(t/=d)*t*((s+1)*t - s) + b; }, easeOut: function(t,b,c,d,s){ if (s == undefined) s = 1.70158; return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; }, easeInOut: function(t,b,c,d,s){ if (s == undefined) s = 1.70158; if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; } }, Bounce: { easeIn: function(t,b,c,d){ return c - Tween.Bounce.easeOut(d-t, 0, c, d) + b; }, easeOut: function(t,b,c,d){ if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; } else { return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; } }, easeInOut: function(t,b,c,d){ if (t < d/2) return Tween.Bounce.easeIn(t*2, 0, c, d) * .5 + b; else return Tween.Bounce.easeOut(t*2-d, 0, c, d) * .5 + c*.5 + b; } } }
<div id="taxiway"> <div id="move" onclick="transition(this,{ease:Tween.Bounce.easeOut})"></div> </div>
但我不喜欢flash流派的缓动公式,为了使用prototype流派的缓动公式,我进一步改进与抽象化我的缓动函数
//******************@author : 司徒正美************ var transition = function(el){ el.style.position = "absolute"; var options = arguments[1] || {}, begin = options.begin,//开始位置 change = options.change,//变化量 duration = options.duration || 500,//缓动效果持续时间 field = options.field,//必须指定,基本上对top,left,width,height这个属性进行设置 ftp = options.ftp || 50, onStart = options.onStart || function(){}, onEnd = options.onEnd || function(){}, ease = options.ease,//要使用的缓动公式 end = begin + change,//结束位置 startTime = new Date().getTime();//开始执行的时间 onStart(); (function(){ setTimeout(function(){ var newTime = new Date().getTime(),//当前帧开始的时间 timestamp = newTime - startTime,//逝去时间 delta = ease(timestamp / duration); el.style[field] = Math.ceil(begin + delta * change) + "px" if(duration <= timestamp){ el.style[field] = end + "px"; onEnd(); }else{ setTimeout(arguments.callee,1000/ftp); } },1000/ftp) })() }
参数 | 类型 | 说明 |
---|---|---|
el | element | 必需,为页面元素 |
begin | number | 必需,开始的位置 |
change | number | 必需,要移动的距离 |
duration | number | 可选,缓动效果持续时间,默认是500ms。建议取300~1000ms。 |
field | string | 必需,要发生变化的样式属性。请在top,left,bottom,right,width与height中选择。 |
ftp | number | 可选,每秒进行多少帧动画,默认50帧,保证流畅播放。一些参考资料,日本动画1秒36帧,中国卡通24帧,赛车游戏60帧。 |
ease | function | 必需,缓动公式,参数为0~1之间的数。可参考我下面给出的45条公式。 |
onStart | function | 可选,在开始时执行。 |
onEnd | function | 可选,在结束时执行。 |
prototype流派的缓动公式,只需一个参数(增至45种)
var tween = { easeInQuad: function(pos){ return Math.pow(pos, 2); }, easeOutQuad: function(pos){ return -(Math.pow((pos-1), 2) -1); }, easeInOutQuad: function(pos){ if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,2); return -0.5 * ((pos-=2)*pos - 2); }, easeInCubic: function(pos){ return Math.pow(pos, 3); }, easeOutCubic: function(pos){ return (Math.pow((pos-1), 3) +1); }, easeInOutCubic: function(pos){ if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,3); return 0.5 * (Math.pow((pos-2),3) + 2); }, easeInQuart: function(pos){ return Math.pow(pos, 4); }, easeOutQuart: function(pos){ return -(Math.pow((pos-1), 4) -1) }, easeInOutQuart: function(pos){ if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,4); return -0.5 * ((pos-=2)*Math.pow(pos,3) - 2); }, easeInQuint: function(pos){ return Math.pow(pos, 5); }, easeOutQuint: function(pos){ return (Math.pow((pos-1), 5) +1); }, easeInOutQuint: function(pos){ if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,5); return 0.5 * (Math.pow((pos-2),5) + 2); }, easeInSine: function(pos){ return -Math.cos(pos * (Math.PI/2)) + 1; }, easeOutSine: function(pos){ return Math.sin(pos * (Math.PI/2)); }, easeInOutSine: function(pos){ return (-.5 * (Math.cos(Math.PI*pos) -1)); }, easeInExpo: function(pos){ return (pos==0) ? 0 : Math.pow(2, 10 * (pos - 1)); }, easeOutExpo: function(pos){ return (pos==1) ? 1 : -Math.pow(2, -10 * pos) + 1; }, easeInOutExpo: function(pos){ if(pos==0) return 0; if(pos==1) return 1; if((pos/=0.5) < 1) return 0.5 * Math.pow(2,10 * (pos-1)); return 0.5 * (-Math.pow(2, -10 * --pos) + 2); }, easeInCirc: function(pos){ return -(Math.sqrt(1 - (pos*pos)) - 1); }, easeOutCirc: function(pos){ return Math.sqrt(1 - Math.pow((pos-1), 2)) }, easeInOutCirc: function(pos){ if((pos/=0.5) < 1) return -0.5 * (Math.sqrt(1 - pos*pos) - 1); return 0.5 * (Math.sqrt(1 - (pos-=2)*pos) + 1); }, easeOutBounce: function(pos){ if ((pos) < (1/2.75)) { return (7.5625*pos*pos); } else if (pos < (2/2.75)) { return (7.5625*(pos-=(1.5/2.75))*pos + .75); } else if (pos < (2.5/2.75)) { return (7.5625*(pos-=(2.25/2.75))*pos + .9375); } else { return (7.5625*(pos-=(2.625/2.75))*pos + .984375); } }, easeInBack: function(pos){ var s = 1.70158; return (pos)*pos*((s+1)*pos - s); }, easeOutBack: function(pos){ var s = 1.70158; return (pos=pos-1)*pos*((s+1)*pos + s) + 1; }, easeInOutBack: function(pos){ var s = 1.70158; if((pos/=0.5) < 1) return 0.5*(pos*pos*(((s*=(1.525))+1)*pos -s)); return 0.5*((pos-=2)*pos*(((s*=(1.525))+1)*pos +s) +2); }, elastic: function(pos) { return -1 * Math.pow(4,-8*pos) * Math.sin((pos*6-1)*(2*Math.PI)/2) + 1; }, swingFromTo: function(pos) { var s = 1.70158; return ((pos/=0.5) < 1) ? 0.5*(pos*pos*(((s*=(1.525))+1)*pos - s)) : 0.5*((pos-=2)*pos*(((s*=(1.525))+1)*pos + s) + 2); }, swingFrom: function(pos) { var s = 1.70158; return pos*pos*((s+1)*pos - s); }, swingTo: function(pos) { var s = 1.70158; return (pos-=1)*pos*((s+1)*pos + s) + 1; }, bounce: function(pos) { if (pos < (1/2.75)) { return (7.5625*pos*pos); } else if (pos < (2/2.75)) { return (7.5625*(pos-=(1.5/2.75))*pos + .75); } else if (pos < (2.5/2.75)) { return (7.5625*(pos-=(2.25/2.75))*pos + .9375); } else { return (7.5625*(pos-=(2.625/2.75))*pos + .984375); } }, bouncePast: function(pos) { if (pos < (1/2.75)) { return (7.5625*pos*pos); } else if (pos < (2/2.75)) { return 2 - (7.5625*(pos-=(1.5/2.75))*pos + .75); } else if (pos < (2.5/2.75)) { return 2 - (7.5625*(pos-=(2.25/2.75))*pos + .9375); } else { return 2 - (7.5625*(pos-=(2.625/2.75))*pos + .984375); } }, easeFromTo: function(pos) { if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,4); return -0.5 * ((pos-=2)*Math.pow(pos,3) - 2); }, easeFrom: function(pos) { return Math.pow(pos,4); }, easeTo: function(pos) { return Math.pow(pos,0.25); }, linear: function(pos) { return pos }, sinusoidal: function(pos) { return (-Math.cos(pos*Math.PI)/2) + 0.5; }, reverse: function(pos) { return 1 - pos; }, mirror: function(pos, transition) { transition = transition || tween.sinusoidal; if(pos<0.5) return transition(pos*2); else return transition(1-(pos-0.5)*2); }, flicker: function(pos) { var pos = pos + (Math.random()-0.5)/5; return tween.sinusoidal(pos < 0 ? 0 : pos > 1 ? 1 : pos); }, wobble: function(pos) { return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; }, pulse: function(pos, pulses) { return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; }, blink: function(pos, blinks) { return Math.round(pos*(blinks||5)) % 2; }, spring: function(pos) { return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); }, none: function(pos){ return 0 }, full: function(pos){ return 1 } }
除了这45条公式外,我们还可以制定自己的缓动公式。正如我在上面表格中提到, 它在运行过程是不执行回调函数时,但你们可以在运行框中看到,我可以实现一边移动一边记录点的坐标。这是怎样实现的呢? 我们只要把上面的缓动公式的任何一条塞进一个只有一个参数的函数就行了。当然此函数要有返回,供继续向下调用。以下就是一个模板:
var myTween = function(pos){ //缓动公式 var value = tween[ease](pos); //***********这上面是固定的************** indicator.style.display = "block"; marker.style.display = "block"; marker.style.left = Math.round((pos*200))+'px'; marker.style.bottom = Math.round(((value*200)-min)*factor)+'px'; label.innerHTML = Math.round((pos*200))+'px'; //************这下面是固定的************* return value; }
更多示例,不懂再留言给我。
<div class="taxiway"> <div class="move" onclick="transition(this,{field:'left',begin:parseFloat(getCoords(this).left),change:700,ease:tween.bouncePast})"></div> </div> <div class="taxiway"> <div class="move" onclick="transition(this,{field:'width',begin:parseFloat(getStyle(this,'width')),change:300,ease:tween.spring})"></div> </div>