这是我今年为新人设计的一门课程的文字精简版,完整的PPT可参考: http://matrix.h5jun.com/slide/show?id=117
在浏览器里,动画实现的基本原理非常简单明了,其实就是 采用定时器改变显示元素的一些属性的过程 。不管是JavaScript操作DOM的动画,还是CSS3动画,还是Canvas动画,或者SVG动画,区别只是使用的API、何种定时器,影响什么环境(DOM/Canvas/SVG/WebGL)。
基本动画
var deg = 0; block.addEventListener("click", function(){ var self = this; requestAnimationFrame(function change(){ self.style.transform = "rotate(" + (deg++) +"deg)"; requestAnimationFrame(change); }); });
上面的例子里,我们使用了定时器 requestAnimationFrame ,requestAnimationFrame 是浏览器专为渲染刷新设计的定时器接口,在早期版本的浏览器里,我们可以用 setTimeout 或者 setInterval 来代替它。定时器改变了方块元素的角度,每一次定时器触发我们就刷新并增加一次它的角度值,这样就产生了方块不断旋转的动态效果。
这就是我们需要的动画,几行原生JS代码就够了,是不是很简单呢?
事实上,上一节的动画不是最佳的实现方法。它存在着几个明显的改进点。
首先,requestAnimationFrame(或者setTimeout、setInterval等其他定时器)并不能保证严格在某个时间点被触发。还记得JavaScript的单线程非阻塞模型吧?如果requestAnimationFrame被其他任务给阻塞了,那么动画就会变慢:
“变慢”的动画
var deg = 0; block.addEventListener("click", function(){ setInterval(function(){ var i = 0; var t = Date.now(); while(++i < 200000000); //模拟耗时操作 console.log(Date.now() - t); }, 100); var self = this; requestAnimationFrame(function change(){ self.style.transform = "rotate(" + (deg++) +"deg)"; requestAnimationFrame(change); }); });
上面的动画,因为有其他的定时器耗时的操作,导致动画变慢。
其次,一个更加麻烦的问题是,上面的动画我们通过定时器给旋转角度 增量 的方式,或者说得更泛一点(暂时忽略前面那个定时器触发时间不确定的问题),我们通过 定义速度 的方式来改变动画,这会导致我们很难精确控制动画时间和动画的幅度。像前面这种匀速运动其实还好,如果做一些复杂的变速运动,按照我们的定义方式,我们本该设置的元素属性值将会类似于求积分,然而时间又不连贯。
正弦曲线运动
var x = 0, y = 0; block.addEventListener("click", function(){ var self = this; requestAnimationFrame(function change(){ self.style.transform = "translate(" + (x++) + "px," + 100 * Math.cos(Math.PI * (y++/180)) + "px)"; requestAnimationFrame(change); }); });
动画,是 位移关于时间的函数 :/(s = f(t)/)
所以,我们 不该 采用增量的方式来执行动画,为了更精确地控制动画,更合适的方式是将动画与时间联系起来:
动画与时间关联
function startAnimation(){ var startTime = Date.now(); requestAnimationFrame(function change(){ var current = Date.now() - startTime; console.log("动画已执行时间: %fms", current); requestAnimationFrame(change); }); }
动画通常情况下有终止时间,如果是循环动画,我们也可以看做特殊的——当动画达到终止时间之后,重新开始动画。因此,我们可以将动画时间 归一(Normalize) 表示:
动画时间归一化表示
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; } } console.log("动画已执行进度: %f", p); if(p < 1.0){ requestAnimationFrame(change); } }); }
我们可以用 时间 来控制动画:
用时间来控制动画周期精确在1秒
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), duration = 1000; setInterval(function(){ var p = (Date.now() - startTime) / duration; self.style.transform = "rotate(" + (360 * p) +"deg)"; }, 1000/60); });
让滑块在 2秒 内向右匀速移动200px
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 200, 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); }); });
时间V.S. 增量
时间 | 增量 | |
---|---|---|
幅度控制 | √ | √ |
时间控制 | √ | X |
幅度控制 | √ | √ |
不延迟 | √ | X |
不掉帧 | X | √ |
滑块在2秒内向右匀加速移动200px,速度从0开始
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 200, 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); }); });
匀速、匀加速运动对比
让滑块在2秒内向右匀减速移动200px,速度从最大减为0
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 200, 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 = 200, disY = 200, duration = 1000 * Math.sqrt(2 * disY / 98); //假设10px是1米,disY = 20米 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); }); });
正弦线运动
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 100, 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); }); });
圆周运动
圆的代数方程涉及到开根号后的正负号问题,因此一般不使用
圆周运动 - 参数方程
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)"; if(p < 1.0) requestAnimationFrame(step); }); });
圆周运动 - 极坐标方程
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 rotation = -360 * p; self.style.transformOrigin = "0 " + r + "px"; self.style.transform = "rotate(" + rotation + "deg)"; if(p < 1.0) requestAnimationFrame(step); }); });
我们总结一下上面的各类动画,发现它们是非常相似的,匀速运动、匀加速运动、匀减速运动、圆周运动唯一的区别仅仅在于位移方程:
我们把共同的部分 S 去掉,得到一个关于 p 的方程 /(e_p = E(p)/),这个方程我们称为动画的 算子 (easing),它决定了动画的性质。
为了实现更加复杂的动画,我们可以将动画进行简易的封装,要进行封装,我们先要抽象出动画相关的 要素 :
动画的简易封装
function Animator(duration, progress, easing){ this.duration = duration; this.progress = progress; this.easing = easing || function(p){return p}; } Animator.prototype = { 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(typeof finished === "function"){ next = finished() === false; }else{ next = finished === false; } if(!next){ self.progress(self.easing(1.0), 1.0); }else{ startTime += duration; self.progress(self.easing(p), p); } } if(next) requestAnimationFrame(step); }); } };
在上面的代码里,我们封装出一个简易的动画类 Animator, 这个类的构造器接收三个参数,分别是 duration
, process
和 easing
。它产生一个对象,包含一个 start
方法,这个方法用指定 duration
、 process
和 easing
执行动画。
有趣的是, start
方法包含一个参数,这个参数是一个布尔类型或者回调函数,当动画结束的时候,如果这个参数是回调函数,将执行这个函数,它的返回值如果不是 false
那么结束动画,否则循环播放动画。如果这个参数是布尔值 flase,那么也循环播放动画。
后续的例子里我们会看到这个类的用法。
我们尝试使用上面设计的动画类来构造连续播放的动画:
让滑块先向右然后再向下运动
var a1 = new Animator(1000, function(p){ var tx = 100 * p; block.style.transform = "translateX(" + tx + "px)"; }); var a2 = new Animator(1000, function(p){ var ty = 100 * p; block.style.transform = "translate(100px," + ty + "px)"; }); block.addEventListener("click", function(){ a1.start(function(){ a2.start(); }); });
在构造更复杂的动画的时候,为了更方便使用,避免 回调嵌套 ,我们可以再实现一个 动画队列 类:
function AnimationQueue(animators){ this.animators = animators || []; } AnimationQueue.prototype = { append: function(){ var args = [].slice.call(arguments); this.animators.push.apply(this.animators, args); }, flush: function(){ if(this.animators.length){ var self = this; function play(){ var animator = self.animators.shift(); if(animator instanceof Animator){ animator.start(function(){ if(self.animators.length){ play(); } }); }else{ animator.apply(self); if(self.animators.length){ play(); } } } play(); } } };
有了动画队列,我们就可以轻松做更复杂一点的动画,比如:
让滑块沿一个矩形边界运动
var a1 = new Animator(1000, function(p){ var tx = 100 * p; block.style.transform = "translateX(" + tx + "px)"; }); var a2 = new Animator(1000, function(p){ var ty = 100 * p; block.style.transform = "translate(100px," + ty + "px)"; }); var a3 = new Animator(1000, function(p){ var tx = 100 * (1-p); block.style.transform = "translate(" + tx + "px, 100px)"; }); var a4 = new Animator(1000, function(p){ var ty = 100 * (1-p); block.style.transform = "translateY(" + ty + "px)"; }); block.addEventListener("click", function(){ var animators = new AnimationQueue(); animators.append(a1, a2, a3, a4); animators.flush(); });
注意到我们的动画队列除了支持Animator对象外,还支持普通的函数,因此我们可以组合起来做一些复杂的运动:
弹跳的小球
var a1 = new Animator(1414, function(p){ var ty = 200 * p * p; block.style.transform = "translateY(" + ty + "px)"; }); var a2 = new Animator(1414, function(p){ var ty = 200 - 200 * p * (2-p); block.style.transform = "translateY(" + ty + "px)"; }); block.addEventListener("click", function(){ var animators = new AnimationQueue(); animators.append(a1,a2, function(){ this.append(a1, a2, arguments.callee); }); animators.flush(); });
还可以再加入更复杂的效果:
弹跳的小球 - 带阻尼效果
block.addEventListener("click", function(){ var T = 1414; var a1 = new Animator(T, function(p){ var s = this.duration * 200 / T; var ty = s * (p * p - 1); block.style.transform = "translateY(" + ty + "px)"; }); var a2 = new Animator(T, function(p){ var s = this.duration * 200 / T; var ty = - s * p * (2-p); block.style.transform = "translateY(" + ty + "px)"; }); var animators = new AnimationQueue(); function foo(){ a2.duration *= 0.7; if(a2.duration <= 0.0001){ console.log("done"); animators.animators.length = 0; } } animators.append(a1 ,foo, a2, function b(){ a1.duration *= 0.7; this.append(a1, foo, a2, b); }); animators.flush(); });
有时候我们也需要一些高级的数学技巧:
模拟从圆周甩出小球
var a1 = new Animator(2800, function(p){ var x = -100 * Math.sin(2.8 * Math.PI * p); var y = 100 - 100 * Math.cos(2.8 * Math.PI * p); block.style.transform = "translate(" + x + "px," + y + "px)"; }); var a2 = new Animator(5000, function(p){ var x = -100 * Math.sin(2.8 * Math.PI) -100 * Math.cos(2.8 * Math.PI) * Math.PI * 5 * p; var y = 100 - 100 * Math.cos(2.8 * Math.PI) + 100 * Math.sin(2.8 * Math.PI) * Math.PI * 5 * p; block.style.transform = "translate(" + x + "px," + y + "px)"; }); block.addEventListener("click", function(){ a1.start(function(){ a2.start(); }); });
贝塞尔曲线可以用来构造平滑动画。
我们可以引入 bezier-easing 库 了来支持贝塞尔曲线的JS动画:
贝塞尔动画 - easeInOutQuint
var easing = BezierEasing(0.86, 0, 0.07, 1); //easeInOutQuint var a1 = new Animator(2000, function(ep,p){ var x = 200 * ep; block.style.transform = "translateX(" + x + "px)"; }, easing); block.addEventListener("click", function(){ a1.start(); });
我们可以通过 cubic-bezier.com 和 easings.net 来定制我们想要的动画效果。
有时候,我们不但要支持元素的运动,还需要改变元素的外观,比如飞翔的小鸟需要扇动翅膀,这类动画我们可以用逐帧动画来实现:
小鸟扇翅膀逐帧动画
<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>
var i = 0; setInterval(function(){ bird.className = "sprite " + "bird" + ((i++) % 3); }, 1000/10);
看上面的代码,其实逐帧动画比之前的动画还要简单,直接用 setInterval 修改元素样式即可,需要注意的是,如果用图片的话,最好是将图片提前预加载了,这样不会出现因为图片还在加载中而显示不出动画的情况。
CSS3 支持两种动画,一种是 Transition ,一种是 Animation 。
Transition 是过渡动画,它只定义在样式的 class 切换的时候发生的动画,因此 Transition 动画相对比较简单,没有循环,也没有事件,它触发的时机只在元素的 className 发生变化的时候。
CSS3 动画支持的浏览器包括:
Transition 和 Animation 共同支持的属性:
Transition 和 Animation 支持同样的 Timing functions:
这其实和我们前面的JS动画里的算子概念是一致的,贝塞尔曲线也是一致的:
Transition 圆周运动
<style> #block{ position:absolute; left: 200px; top: 100px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; transform-origin: 0 100px; transform: rotate(0deg); } #block.play { transform: rotate(360deg); transition: transform 2.0s linear; } </style> <div id="block"></div>
block.addEventListener("click", function(){ block.className = "play"; });
Transition 使用贝塞尔曲线
#block.play { transform: translateX(200px); transition: transform 2.0s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
Transition 没有优先级,后面的样式会覆盖掉前面的样式中的某些 Transition 属性,因此当两个 class 都有 Transition 的时候,相互覆盖会导致奇怪的行为:
Transition 样式覆盖
#block.play { border-radius: 0; transform: scale(2.0); background: #c80; transition: all 2.0s cubic-bezier(0.68, -0.55, 0.265, 1.55) 3s; } #block.play2 { /* transition 覆盖*/ background: #c8f; transition: all 2.0s linear 0.5s; transform: scale(2.0) rotate(360deg); }
block.addEventListener("click", function(){ block.className = "play play2"; });
Animation 动画支持一些更高级的特性:
Animation - 往复圆周运动
#block{ position:absolute; left: 200px; top: 100px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; animation: roll 2.0s linear 0s infinite alternate; transform-origin: 0 100px; } @keyframes roll{ 0%{transform:rotate(0deg)} 100%{transform:rotate(360deg)} }
复杂的动画效果可以将 JS 和 CSS3 动画组合使用:
动画组合
#block{ position:absolute; left: 150px; top: 200px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; animation: anim 2.0s linear 0s forwards; } @keyframes anim{ 0%{border-radius: 50%} 50%{border-radius: 0; background: #c80;} 100%{border-radius: 20%; transform:scale(2.0); background: #08c;} }
var easing = BezierEasing(0.68, -0.55, 0.265, 1.55); var a1 = new Animator(2000, function(ep,p){ var x = 150 + 200 * ep; block.style.left = x + "px"; }, easing); block.addEventListener("webkitAnimationEnd", function(){ a1.start(); });