现现在,许多页面上均有一些动画效果。适当的动画效果能够在必定程度上提升页面的美观度,具备提示效果的动画能够加强页面的易用性。javascript
实现页面动画的途径通常有两种。css
咱们今天只讲第一种实现方式。html
所谓的动画,就是经过一些列的运动造成的动的画面。在网页中,咱们能够经过不断的改变元素的css值,来达到动的效果。 java
JavaScript的动画用的最多的3个api就是setInterval()、setTimeout()和requestAnimationFrame()算法
听说,普通人眼能看到1/24秒,就是说1秒至少24帧,每次移位间隔须要小于1000/24=41.7毫秒,也就说setInterval要每隔至少40毫秒执行一次,通常地,咱们采用10毫秒,固然间隔时间越短,客户端执行计算次数就越多,若是你code计算量大则能够适当调长些。api
像setTimeout、setInterval同样,requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据本身的频率进行一次重绘,它接收一个回调函数做为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间做为参数。因为requestAnimationFrame的功效只是一次性的,因此若想达到动画效果,则必须接二连三的调用requestAnimationFrame,就像咱们使用setTimeout来实现动画所作的那样。requestAnimationFrame函数会返回一个资源标识符,能够把它做为参数传入cancelAnimationFrame函数来取消requestAnimationFrame的回调。跟setTimeout的clearTimeout很类似啊。浏览器
能够这么说,requestAnimationFrame是setTimeout的性能加强版。异步
有一点须要注意的是,requestAnimationFrame不能自行指定函数运行频率,而是有浏览器决定刷新频率。因此这个更能达到浏览器所能达到的最佳动画效果了。函数
这个方法不是全部的浏览器都兼容。oop
div{ width: 100px; height: 100px; background-color: #f00; position: absolute; }
<div id="div"></div>
<script type="text/javascript"> var id; function step() { var temp = div.offsetLeft + 2; div.style.left = temp + "px"; //和setTimeout同样,要手动调用才能实现连续动画。 id = window.requestAnimationFrame(step); //返回值是一个id,能够经过这个id来取消 } id = window.requestAnimationFrame(step); //取消回调函数 window.cancelAnimationFrame(step); </script>
咱们知道JavaScript试单线程的产物,两个函数就是利用了插入代码的方式实现了伪异步,和AJAX的原理其实是同样的。
console.log("1"); setTimeout(function(){ console.log("3") },0); console.log("2"); //输出结果是什么? //1 2 3
function fn() { setTimeout(function(){ console.log('can you see me?'); },1000); while(true) {} } //输出结果是什么?
function step() { var temp = div.offsetLeft + 2; div.style.left = temp + "px"; window.requestAnimationFrame(step); for (var i = 0; i < 50000; i++) { console.log("再牛逼的定时器也得等到我执行完才能执行") } } window.requestAnimationFrame(step);
动画实际上是 “位移”关于“时间”的函数:s=f(t)
因此不应采用增量的方式来执行动画,为了更精确的控制动画,更合适的方法是将 动画与时间关联起来
function startAnimation() { var startTime = Date.now(); requestAnimationFrame(function change() { var current = Date.now() - startTime; console.log("动画已执行时间" + current); requestAnimationFrame(change); }); } startAnimation();
动画一般状况下有终止时间,若是是循环动画,咱们也能够看作特殊的——当动画达到终止时间以后,从新开始动画。所以,咱们能够将动画时间归一(Normalize)表示:
//duration 是动画执行时间 isLoop是否为循环执行。 function startAnimation(duration, isLoop){ var startTime = Date.now(); requestAnimationFrame(function change(){ // 动画已经用去的时间占总时间的比值 var p = (Date.now() - startTime) / duration; if(p >= 1.0){ if(isLoop){ // 若是是循环执行,则开启下一个循环周期。而且把开始时间改为上个周期的结束时间 startTime += duration; p -= 1.0; //动画进度初始化 }else{ p = 1.0; //若是不是循环,则把时间进度至为 1.0 表示动画执行结束 } } console.log("动画已执行进度", p); if(p < 1.0){ //若是小于1.0表示动画还诶有值完毕,继续执行动画。 requestAnimationFrame(change); } }); }
示例1:用时间控制动画周期精确到2s中
block.addEventListener("click", function() { var self = this, startTime = Date.now(), duration = 2000; setInterval(function() { var p = (Date.now() - startTime) / duration; // 时间已经完成了2000的比例,则360度也是进行了这么个比例。 self.style.transform = "rotate(" + (360 * p) + "deg)"; }, 100); });
示例2:让滑块在2秒内向右匀速移动600px
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 600, duration = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); self.style.transform = "translateX(" + (distance * p) +"px)"; if(p < 1.0) { requestAnimationFrame(step); } }); });
用时间来控制进度
$$
s = S*p
$$
加速度恒定,速度从0开始随时间增长而均匀增长。
匀加速公式:大写S:要移动的总距离 p:归一化的时间进度
$$
s = S*p^2
$$
// 2s中内匀加速运动2000px block.addEventListener("click", function() { var self = this, startTime = Date.now(), distance = 1000, duration = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / duration); self.style.transform = "translateX(" + (distance * p * p) + "px)"; if(p < 1.0) requestAnimationFrame(step); }); });
匀减速运动公式:
$$
s=Sp(2-p)
$$
//2s中使用速度从最大匀减速到0运动1000px block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 1000, duration = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); self.style.transform = "translateX(" + (distance * p * (2-p)) +"px)"; if(p < 1.0) requestAnimationFrame(step); }); });
课堂练习:小球的自由落体运动
匀速水平运动和自由落体运动的组合。
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), disX = 1000, disY = 1000, duration = Math.sqrt(2 * disY / 10 / 9.8) * 1000; // 落到地面须要的时间 单位ms //假设10px是1米,disY = 100米 requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); var tx = disX * p; //水平方向是匀速运动 var ty = disY * p * p; //垂直方向是匀加速运动 self.style.transform = "translate(" + tx + "px" + "," + ty +"px)"; if(p < 1.0) requestAnimationFrame(step); }); });
正弦运动:x方向匀速,垂直方向是时间t的正弦函数
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 800, duration = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); var ty = distance * Math.sin(2 * Math.PI * p); var tx = 2 * distance * p; self.style.transform = "translate(" + tx + "px," + ty + "px)"; if(p < 1.0) requestAnimationFrame(step); }); });
圆周运动公式:
$$
x = R.sin(2πp) , y = R.cos(2πp)
$$
block.addEventListener("click", function() { var self = this, startTime = Date.now(), r = 100, duration = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / duration); var tx = r * Math.sin(2 * Math.PI * p), ty = -r * Math.cos(2 * Math.PI * p); self.style.transform = "translate(" + tx + "px," + ty + "px)"; requestAnimationFrame(step); }); });
对于一些比较复杂的变化,算法也比较复杂,就要用到动画算子。动画算子 是一个函数,能够把进度转化成另一个值。其实也就是一种算法。
咱们总结一下上面的各种动画,发现它们是很是类似的,匀速运动、匀加速运动、匀减速运动、圆周运动惟一的区别仅仅在于位移方程:
匀速运动:
$$
s = S *p
$$
匀加速运动:
$$
s = S *p^2
$$
$$
s = Sp(2-p)
$$
$$
x = Rsin(2PI*p)
$$
$$
y = Rcos(2PI*p)
$$
咱们把共同的部分 S 或R 去掉,获得一个关于 p 的方程 ,这个方程咱们称为动画的算子(easing),它决定了动画的性质。
$$
e = p
$$
$$
e = p^2
$$
$$
e = p*(2 - p)
$$
$$
e = sin(2PIp)
$$
$$
e = cos(2 * PI * p)
$$
一些经常使用的动画算子
var pow = Math.pow, BACK_CONST = 1.70158; // t指的的是动画进度 前面的p Easing = { // 匀速运动 linear: function (t) { return t; }, // 加速运动 easeIn: function (t) { return t * t; }, // 减速运动 easeOut: function (t) { return (2 - t) * t; }, //先加速后减速 easeBoth: function (t) { return (t *= 2) < 1 ? .5 * t * t : .5 * (1 - (--t) * (t - 2)); }, // 4次方加速 easeInStrong: function (t) { return t * t * t * t; }, // 4次方法的减速 easeOutStrong: function (t) { return 1 - (--t) * t * t * t; }, // 先加速后减速,加速和减速的都比较剧烈 easeBothStrong: function (t) { return (t *= 2) < 1 ? .5 * t * t * t * t : .5 * (2 - (t -= 2) * t * t * t); }, // easeOutQuart: function (t) { return -(Math.pow((t - 1), 4) - 1) }, // 指数变化 加减速 easeInOutExpo: function (t) { if (t === 0) return 0; if (t === 1) return 1; if ((t /= 0.5) < 1) return 0.5 *Math.pow(2, 10 * (t - 1)); return 0.5 * (-Math.pow(2, - 10 * --t) + 2); }, //指数式减速 easeOutExpo: function (t) { return (t === 1) ? 1 : -Math.pow(2, - 10 * t) + 1; }, // 先回弹,再加速 swingFrom: function (t) { return t * t * ((BACK_CONST + 1) * t - BACK_CONST); }, // 多走一段,再慢慢的回弹 swingTo: function (t) { return (t -= 1) * t * ((BACK_CONST + 1) * t + BACK_CONST) + 1; }, //弹跳 bounce: 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; } };
为了实现更加复杂的动画,咱们能够将动画进行 简易 的封装,要进行封装,咱们先要抽象出动画相关的要素:
动画时长:T = duration
动画进程:p = t/T
easing: e = f(p) (动画算子:p的函数 )
动画方程: x = g(e) y = g(e) (动画的位移相对于动画算子的方程)
动画生命周期:开始、进程中、结束
<script type="text/javascript"> /* 参数1:动画的执行时间 参数2:动画执行的时候的回调函数(动画执行的要干的事情) 参数3:动画算子. 若是没有传入动画算子,则默认使用匀速算子 */ function Animator(duration, progress, easing) { this.duration = duration; this.progress = progress; this.easing = easing || function(p) { return p }; } Animator.prototype = { /*开始动画的方法, 参数:一个布尔值 true表示动画不循环执行。 */ start: function(finished) { /*动画开始时间*/ var startTime = Date.now(); /*动画执行时间*/ var duration = this.duration, self = this; /*定义动画执行函数*/ requestAnimationFrame(function step() { /*获得动画执行进度*/ var p = (Date.now() - startTime) / duration; /*是否执行下一帧动画*/ var next = true; /*判断动画进度是否完成*/ if(p < 1.0) { self.progress(self.easing(p), p); //执行动画回调函数,并传入动画算子的结果和动画进度。 } else { if(finished){ //判断是否中止动画。若是是true表明中止动画。 next = false; }else{ startTime = Date.now(); } } // 若是next是true执行下一帧动画 if(next) requestAnimationFrame(step); }); } }; block.onclick = function () { var self = this; new Animator(2000, function (p) { self.style.top = 500 * p +"px"; },Easing.bounce).start(false); }
有时候,咱们不但要支持元素的运动,还须要改变元素的外观,好比飞翔的小鸟须要扇动翅膀,这类动画咱们能够用逐帧动画来实现:
<style type="text/css"> .sprite {display:inline-block; overflow:hidden; background-repeat: no-repeat;background-image:url(http://res.h5jun.com/matrix/8PQEganHkhynPxk-CUyDcJEk.png);} .bird0 {width:86px; height:60px; background-position: -178px -2px} .bird1 {width:86px; height:60px; background-position: -90px -2px} .bird2 {width:86px; height:60px; background-position: -2px -2px} #bird{ position: absolute; left: 100px; top: 100px; zoom: 0.5; } </style> <div id="bird" class="sprite bird1"></div> <script> var i = 0; setInterval(function(){ bird.className = "sprite " + "bird" + ((i++) % 3); }, 1000/10); </script>