您的位置:首页 > Web前端 > JavaScript

最近学习JS的感悟-2(动画方面)

2013-05-03 18:58 295 查看
昨天写那篇文章花费了一个多小时,所以后面就没写了,今天继续写一点吧,主要就想说一下动画方面的内容。

W3C现在已经定义了WindowAnimationTiming interface规范,其核心方法是requestAnimactionFrame和cancelRequestAnimationFrame,在很多新浏览器中都有实现,用它可以实现动画,但是貌似兼容性不太好,特别是在天国,还有IE6扫尾,所以貌似不靠谱,不过可以做一个判定,如果支持这个规范,则使用它,否则,使用我后面讲的这种方式,不过貌似比较麻烦,所以我没有使用这种方式,如果后面有时间了,可以去写着玩儿一下。

除了这种不靠谱的方式,可能大家用的比较多的就是setInterval了,用它来完成动画是比较靠谱的。其实它的原理非常简单,就是在规定的时间内每隔一段时间执行一次回调,这个回调中可以进行一些动画的处理,比如修改元素大小,位置等。

说到这儿,就必须要提几个概念了,首先是duration(动画的执行时间),frameTime(每次执行的时间间隔),然后是FPS(每秒的帧数,也就是1000 / frameTime)。一般来说,FPS越高,动画就表现的越流畅,FPS越低,动画就越不流畅。在Jquery中,默认的frameTime为13ms,在理想情况下,FPS可以达到70多。

为什么说是理想下呢,因为JS的定时器存在精度问题,间隔不能太小,资源暂用较大的时候,这个间隔还没办法得到保障,更不靠谱的是,在一些高级浏览器中,页面不可见(如切换到其他选项卡,最小化)的时候,这个时间间隔会被自动提高,比如firefox5开始,setInterval的间隔在浏览器最小化之后至少被提高到1000ms。

好,我们来总结一下上面这一段话,也就是说,这个frameTime是不靠谱的,根本不可控的,即使你写了setInterval(xxx,20),但也不一定会是20ms执行一次。所以这里就分化出两种策略了,一种是弃时保帧,另一种是弃帧保时。所谓的弃时保帧很容易理解,我要保证执行的次数有这么多,但这样就不保证duration;所谓的弃帧保时也好理解,我保证动画在duration结束,但是不保证到底执行了多少次(理论上执行次数为duration/frameTime)。

在我写这个库的最开始,由于不知道setInterval的问题, 我采用的是弃时保帧的策略,也就是我定义了frameTime和steps,如果内部指定一个计数器,伪代码如下:

if(counter >= steps) {
clearInterval(interval);
} else {
counter++;
//动画函数
}
最开始的完整代码如下:
tp.animate.base = function(elem,options,callback) {
var _options = {
property : "left",
from : 0,
to : 200,
steps : 30,
time : 1000,
needAddPx : true,
autoStart : true
};
for(var name in options) {
_options[name] = options[name];
}
callback = callback ? callback : function(){};
var curStep = 0;
var suffix = _options.needAddPx ? "px" : "";
var interval = parseFloat((_options.from - _options.to)) / parseInt(_options.steps);
var intervalHandler = null;
var _publicMethod = {
start : function() {
intervalHandler = setInterval(_animateHelper,_options.time / _options.steps);
return this;
},
stop : function() {
clearInterval(intervalHandler);
return this;
},
toStart : function() {
curStep = 0;
elem.style[_options.property] = _options.from + suffix;
return this;
},
toEnd : function() {
curStep = _options.steps;
elem.style[_options.property] = _options.to + suffix;
return this;
}
};
var _updatePropertyVal = null;
if("opacity" === _options.property) {
_updatePropertyVal = function(val) {
tp.dom.opacity(elem,val);
};
} else {
_updatePropertyVal = function(val) {
elem.style[_options.property] = val;
};
}
function _animateHelper() {
var newVal = _options.from - (++curStep * interval);
if(curStep < _options.steps) {
_updatePropertyVal(newVal + suffix);
} else {
_updatePropertyVal(_options.to + suffix);
_publicMethod.stop();
callback(elem);
}
}
if(_options.autoStart) {
_publicMethod.start();
}
return _publicMethod;
};
这个代码当时出现了严重的抖动问题,其根本原因也是因为setInterval不准,而且IE下面有时候一个600ms的动画2m了还是没有执行完成,所以,这个直接pass掉。
使用了弃帧保时的策略之后,整个动画的编写和之前完全不一样了,动画这一块儿看了很多qwrap的代码,这个库的具体信息可点此:http://www.qwrap.com/

之前采用的是steps,也就是必须要执行那么多帧,而现在肯定保证不了,所以,我们换一种思路,我们在动画开始的时候记录一下时间,也就是通过:

var startTime = (new Date()).getTime();
然后定时器执行的时候,我们再查看一下当前的时间,即currentTime,这里我们定义一个per,用这个来定义动画的执行进度,它的取值为0-1,0表示还没有开始,1代表动画执行结束。per的计算如下:
per = (currentTime - startTime) / duration;
如果per大于等于1,那么说明动画结束,也就是需要clearInterval了。
使用这个策略之后,整个代码如下:

(function() {
var animate = function(dur,options,animateFn) {
tp.object.extend(this,options);
tp.object.extend(this,{
animateFn : animateFn,
dur : dur,
per : 0,
frameTime : options.frameTime || 13,
status : 0,
startDate : 0 ,
interval : null
});
changePer(this,0);
};
function changePer(aniObj,per) {
aniObj.per = per;
aniObj.startDate = (new Date()).getTime() - per * aniObj.dur;
}
function turnOn(aniObj) {
aniObj.step(null);
if(aniObj.isPlaying()) {
aniObj.interval = window.setInterval(function() {
aniObj.step(null);
},aniObj.frameTime);
}
}
function turnOff(aniObj) {
window.clearInterval(aniObj.interval);
}
tp.object.extend(animate.prototype,{
isPlaying : function() {
return this.status == 1;
},
play : function() {
var me = this;
if(me.isPlaying()) {
me.pause();
}
changePer(me,0);
me.status = 1;
me.onplay && me.onplay();
turnOn(me);
},
step : function(per) {
var me = this;
if(null != per) {
changePer(me,per);
} else {
//计算出per
per = ((new Date()).getTime() - me.startDate) / me.dur;
me.per = per;
}
if(me.per > 1) {
me.per = 1;
}
me.animateFn(me.per);
me.onstep && me.onstep();
if(me.per >= 1) {
me.end();
}
},
end : function() {
changePer(this,1);
this.animateFn(1);
this.status = 2;
turnOff(this);
this.onend && this.onend();
},
pause : function() {
this.status = 4;
turnOff(this);
this.onpause && this.onpause();
},
resume : function() {
changePer(this,this.per);
this.status = 1;
this.onresume && this.onresume();
turnOn(this);
},
reset : function() {
changePer(this,0);
this.animateFn(0);
this.onreset && this.onreset();
}
});
tp.animate.base = animate;
})();
tp.object.extend可以扩展某一个对象的方法,其实作用和一般库的mix一样。
其实最基本的动画代码非常简单,如下:

<script>
var timerId, startTime, frameTime = 13, dur = 3 * 1000;
function animFun(time) {
var per = Math.min(1.0, (new Date - startTime) / dur);
if(per >= 1) {
clearTimeout(timerId);
} else {
document.getElementById("animated").style.left = Math.round(500 * per) + "px";
}
}
function start() {
startTime = new Date;
timerId = setInterval(animFun, frameTime);
}
</script>
<div id="animated" onclick="start()" style="position: absolute; left: 0px; padding: 50px;background: crimson; color: white">Click Me</div>

PS:这个代码是我理解动画原理的代码

,当时网上找的,感谢作者啊!!

之前我们的动画只能进行匀速的动画,对于复杂的动画,这样肯定满足不了要求,那么怎么做呢?

这里就需要引入算子了,具体的解释大家可以google一下哈。

具体如下:

<script>
var timerId, startTime, frameTime = 13, dur = 3 * 1000;
function easing(p) {
return p * p;
}
function animFun(time) {
var per = Math.min(1.0, (new Date - startTime) / dur);
if(per >= 1) {
clearTimeout(timerId);
} else {
document.getElementById("animated").style.left = Math.round(500 * easing(per)) + "px";
}
}
function start() {
startTime = new Date;
timerId = setInterval(animFun, frameTime);
}
</script>
<div id="animated" onclick="start()" style="position: absolute; left: 0px; padding: 50px;background: crimson; color: white">Click Me</div>
这里面多的代码就是:

function easing(p) {
return p * p;
}
现在网上这种算子还蛮多的,比如KISSY:

KISSY.add('anim/easing', function () {
var PI = Math.PI,
pow = Math.pow,
sin = Math.sin,
BACK_CONST = 1.70158;
var Easing = {

/**
* swing effect.
*/
swing: function (t) {
return ( -Math.cos(t * PI) / 2 ) + 0.5;
},

/**
* Uniform speed between points.
*/
'easeNone': function (t) {
return t;
},

/**
* Begins slowly and accelerates towards end. (quadratic)
*/
'easeIn': function (t) {
return t * t;
},

/**
* Begins quickly and decelerates towards end.  (quadratic)
*/
easeOut: function (t) {
return ( 2 - t) * t;
},

/**
* Begins slowly and decelerates towards end. (quadratic)
*/
easeBoth: function (t) {
return (t *= 2) < 1 ?
.5 * t * t :
.5 * (1 - (--t) * (t - 2));
},

/**
* Begins slowly and accelerates towards end. (quartic)
*/
'easeInStrong': function (t) {
return t * t * t * t;
},

/**
* Begins quickly and decelerates towards end.  (quartic)
*/
easeOutStrong: function (t) {
return 1 - (--t) * t * t * t;
},

/**
* Begins slowly and decelerates towards end. (quartic)
*/
'easeBothStrong': function (t) {
return (t *= 2) < 1 ?
.5 * t * t * t * t :
.5 * (2 - (t -= 2) * t * t * t);
},

/**
* Snap in elastic effect.
*/

'elasticIn': function (t) {
var p = .3, s = p / 4;
if (t === 0 || t === 1) return t;
return -(pow(2, 10 * (t -= 1)) * sin((t - s) * (2 * PI) / p));
},

/**
* Snap out elastic effect.
*/
elasticOut: function (t) {
var p = .3, s = p / 4;
if (t === 0 || t === 1) return t;
return pow(2, -10 * t) * sin((t - s) * (2 * PI) / p) + 1;
},

/**
* Snap both elastic effect.
*/
'elasticBoth': function (t) {
var p = .45, s = p / 4;
if (t === 0 || (t *= 2) === 2) return t;

if (t < 1) {
return -.5 * (pow(2, 10 * (t -= 1)) *
sin((t - s) * (2 * PI) / p));
}
return pow(2, -10 * (t -= 1)) *
sin((t - s) * (2 * PI) / p) * .5 + 1;
},

/**
* Backtracks slightly, then reverses direction and moves to end.
*/
'backIn': function (t) {
if (t === 1) t -= .001;
return t * t * ((BACK_CONST + 1) * t - BACK_CONST);
},

/**
* Overshoots end, then reverses and comes back to end.
*/
backOut: function (t) {
return (t -= 1) * t * ((BACK_CONST + 1) * t + BACK_CONST) + 1;
},

/**
* Backtracks slightly, then reverses direction, overshoots end,
* then reverses and comes back to end.
*/
'backBoth': function (t) {
var s = BACK_CONST;
var m = (s *= 1.525) + 1;

if ((t *= 2 ) < 1) {
return .5 * (t * t * (m * t - s));
}
return .5 * ((t -= 2) * t * (m * t + s) + 2);

},

/**
* Bounce off of start.
*/
bounceIn: function (t) {
return 1 - Easing.bounceOut(1 - t);
},

/**
* Bounces off end.
*/
bounceOut: function (t) {
var s = 7.5625, r;

if (t < (1 / 2.75)) {
r = s * t * t;
}
else if (t < (2 / 2.75)) {
r = s * (t -= (1.5 / 2.75)) * t + .75;
}
else if (t < (2.5 / 2.75)) {
r = s * (t -= (2.25 / 2.75)) * t + .9375;
}
else {
r = s * (t -= (2.625 / 2.75)) * t + .984375;
}

return r;
},

/**
* Bounces off start and end.
*/
'bounceBoth': function (t) {
if (t < .5) {
return Easing.bounceIn(t * 2) * .5;
}
return Easing.bounceOut(t * 2 - 1) * .5 + .5;
}
};

return Easing;
});
刚才我们写的tp.animate.base可以完成万能动画,我们只需要填写具体的帧动画即可。但对于颜色动画来说,还是比较麻烦的,我这儿写了一个组件,代码如下:

define("tp.animate.color",[],function() {
//特殊的颜色值
var KEYWORDS = {
'black':[0, 0, 0],
'silver':[192, 192, 192],
'gray':[128, 128, 128],
'white':[255, 255, 255],
'maroon':[128, 0, 0],
'red':[255, 0, 0],
'purple':[128, 0, 128],
'fuchsia':[255, 0, 255],
'green':[0, 128, 0],
'lime':[0, 255, 0],
'olive':[128, 128, 0],
'yellow':[255, 255, 0],
'navy':[0, 0, 128],
'blue':[0, 0, 255],
'teal':[0, 128, 128],
'aqua':[0, 255, 255]
};
/*
解析颜色,支持rgb和hex颜色值,特殊颜色如black
*/
function parseColor(color) {
var r = 0,
g = 0,
b = 0;
if(/rgb/.test(color)) {
//参数为RGB模式,不做进制转换,直接截取字符串,如rgb(2,2,2)
var arr = color.match(/\d+/g);
r = parseInt(arr[0]);
g = parseInt(arr[1]);
b = parseInt(arr[2]);
} else if(/#/.test(color)) {
//参数为十六进制,需要进行进制转换
var len = color.length;
if(7 === len) {
//非简写,#ffccdd
r = parseInt(color.slice(1,3),16);
g = parseInt(color.slice(3,5),16);
b = parseInt(color.slice(5),16);
} else if(4 === len) {
//简写模式 #eaf
r = parseInt(color.charAt(1) + color.charAt(1),16);
g = parseInt(color.charAt(2) + color.charAt(2),16);
b = parseInt(color.charAt(3) + color.charAt(3),16);
} else {
tp.log("error format of color:" + color);
}
} else {
//特殊颜色,如black
var tmpColor = KEYWORDS[color.toLowerCase()];
if(tmpColor) {
r = tmpColor[0];
g = tmpColor[1];
b = tmpColor[2];
} else {
tp.log("error format of color:" + color);
}
}
return [r,g,b];
}
function setValue(ele,attr,color) {
if(tp.type.isString(color)) {
//直接设置如#cccccc
tp.dom.css(ele,attr,color);
} else {
//数组,设置如rgb(22,22,22)
tp.dom.css(ele,attr,"rgb(" + color.join(",") +  ")");
}
}
//preColor为arr
function getNewColor(preColor,spacing,per) {
var arr = new Array();
arr[0] = Math.max(Math.min(parseInt(preColor[0] + per * spacing[0]),255),0);
arr[1] = Math.max(Math.min(parseInt(preColor[1] + per * spacing[1]),255),0);
arr[2] = Math.max(Math.min(parseInt(preColor[2] + per * spacing[2]),255),0);
return "rgb(" + arr.join(",") + ")";
}

/**
* @namespace tp.animate.color
* @param {Object} options
* @config {Object} [ele] 动画元素
* @config {String} [from] 起始颜色值
* @config {String} [to] 结束颜色值
* @config {String} [attr] 操作的属性名
*/
var color = function(options) {
var fromArr = options.from ? parseColor(options.from) : parseColor(tp.dom.css(options.ele,options.attr)),
toArr = parseColor(options.to);
tp.object.extend(this,{
spacing : [toArr[0] - fromArr[0] , toArr[1] - fromArr[1] , toArr[2] - fromArr[2]],
attr : options.attr,
fromArr : fromArr,
ele : options.ele
});
};
tp.object.extend(color.prototype,{
action : function(per) {
var newColor = getNewColor(this.fromArr,this.spacing,per);
setValue(this.ele,this.attr,newColor);
}
});
tp.modules.add("tp.animate.color",color);
});
调用如下:

tp.use(["tp.animate.color","tp.animate.easing"],function(color,easing) {
var _color = new color({
ele : test,
attr : "backgroundColor",
to : "#AACCBB"
});
tp.event.on(btn,"click",function() {
var _a = new tp.animate.base(900,{
dur : 150
},function(per) {
_color.action(easing.backIn(per));
});
_a.play();
});
});
其实颜色动画的原理也很简单,比如从一个颜色A到另一个颜色B,每一个颜色都有一个RGB值,所以可以把整个过程分开,R从A到B,G从A到B,B从A到B,而我们之前已经实现了tp.animate.base了,这里有一个per,在任意时刻,假设计算R,R差值C为B的R值减去A的R值,那么此刻的R值为:A的R值+per * C ,但是由于R的范围为0-255,所以稍微改变一下:

Math.max(Math.min(parseInt(A_R + per * C),255),0);
个人感觉这个里面有点麻烦的还是颜色值的获取,而我在parseColor中也解决了这个问题,其实就是将一个颜色值如:#232322变成RGB的数组,方便后面调用而已。

除了这种,动画中还有动画队列,这里我就不说了。

差不多一个小时,写完收工。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: