这是我今年为新人设计的一门课程的文字精简版,完整的PPT可参考:http://matrix.h5jun.com/slide/show?id=117
简单的 JS 动画
在浏览器里,动画实现的基本原理非常简单明了,其实就是采用定时器改变显示元素的一些属性的过程。不管是JavaScript操作DOM的动画,还是CSS3动画,还是Canvas动画,或者SVG动画,区别只是使用的API、何种定时器,影响什么环境(DOM/Canvas/SVG/WebGL)。
1 2 3 4 5 6 7 8 |
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被其他任务给阻塞了,那么动画就会变慢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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); }); }); |
上面的动画,因为有其他的定时器耗时的操作,导致动画变慢。
其次,一个更加麻烦的问题是,上面的动画我们通过定时器给旋转角度增量的方式,或者说得更泛一点(暂时忽略前面那个定时器触发时间不确定的问题),我们通过定义速度的方式来改变动画,这会导致我们很难精确控制动画时间和动画的幅度。像前面这种匀速运动其实还好,如果做一些复杂的变速运动,按照我们的定义方式,我们本该设置的元素属性值将会类似于求积分,然而时间又不连贯。
1 2 3 4 5 6 7 8 9 |
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))
所以,我们不该采用增量的方式来执行动画,为了更精确地控制动画,更合适的方式是将动画与时间联系起来:
1 2 3 4 5 6 7 8 9 10 11 |
function startAnimation(){ var startTime = Date.now(); requestAnimationFrame(function change(){ var current = Date.now() - startTime; console.log("动画已执行时间: %fms", current); requestAnimationFrame(change); }); } |
动画通常情况下有终止时间,如果是循环动画,我们也可以看做特殊的——当动画达到终止时间之后,重新开始动画。因此,我们可以将动画时间归一(Normalize)表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 2 3 4 5 6 7 8 |
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); }); |
1 2 3 4 5 6 7 8 9 10 |
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 | √ |
变速运动
变速运动可以模拟一些物理效果、曲线运动,以及其他的一些非均匀变化的特效。
匀加速运动
加速度恒定,速度从0开始随时间增加而均匀增加。
- (t = T cdot p)
-
(s_t = S cdot p ^ {2} = (frac{S}{T^2}) t^2 )
- (v = frac{2S}{T^2} cdot t = frac{2Sp}{T})
- (a = frac{2S}{T^2} )
通过推导可以得到匀减速运动的位移时间公式:(s_t = Sp^2)
1 2 3 4 5 6 7 8 9 |
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(" |