在网页上创建动画一般有两种方式:css和javascript。它们在创建动画的时间和性能上是不一样的,各有利弊。选择哪种方法实际上取决于项目,以及想要实现什么类型的动画。
一般使用css动画来实现比较简单的“一次性转换”,为UI元素转换比较小的独立状态。例如从侧面引入导航栏菜单,模太框弹出等。 要实现高级效果时,例如弹跳,加速,减速等比较复杂的动画,则使用Javascript动画。现在有很多比较好的JS动画框架,例如TweenMax,Velocity,animo.js,jquery。
不管是css还是javascript来创建动画,我们都会听到一个词“缓动”。自然界中没有东西从一点呈线性的移动到另一点,一般可能需要加速或减速。在经典动画中,经常会出现“缓入”,“缓出”,“缓入缓出”效果。缓动使动画不再那么尖锐或生硬。 css中,我们要想达到这些效果,只需要使用一些关键字: * linear * ease-in * ease-out * ease-in-out
那这么关键字的背后到底是什么原理呢?如何用javascript来实现这些缓动效果。
动画是关于时间的函数,本质就是利用浏览器和GPU的渲染过程定时改变元素的属性。
使用javascript实现动画时一般是使用requestAnimationFrame,我们可能经常也会用setInterval和setTimeout来实现动画,但是它们实现的动画都不会与屏幕的刷新率同步,并且很可能出现抖动和跳帧,例如jQuery就是采用setInterval来实现动画,所以jQuery动画的帧率会偏低(jQuery为甚么不采用RAF)。
“Talk is cheap, show me the code”。下面学习实现下月影总结的动画原理。原理简单,实现经典。
匀速运动
让小球在2s内向右匀速移动200px
时间: t = T * p位移: St = S * p = v * t速度: v = St / t = S / T加速度: a = 0 circle.on('click', function() { var self = this; var startTime = Date.now(); var distance = 200; var T = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / T); self.style.transform = 'translateX(' + (distance * p) + 'px)'; if(p < 1.0){ requestAnimationFrame(step); } }) })2.匀加速运动
让小球在2s内向右匀加速向右移动200px, 速度从0开始 * 时间: t = T * p * 位移: St = S * p^2 = (S * t^2) / T^2 * 速度: v = (2*S / T^2) * t = 2Sp/T * 加速度 a = 2*S / T^2
circle.on('click', function() { var self = this; var startTime = Date.now(); var distance = 200; var T = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / T); self.style.transform = 'translateX(' + (distance * p * p) + 'px)'; if(p < 1.0){ requestAnimationFrame(step); } }) })3.匀减速运动
让小球在2s内向右匀减速向右移动200px, 速度从最大减为0 * 时间: t = T * p * 位移: St = (2*S / T) * t - (S / T^2) * t^2 = Sp * (2 - p) * 速度: v = 2*S / T - 2*S / t^2 * t * 加速度 a = -2*S / T^2
circle.on('click', function() { var self = this; var startTime = Date.now(); var distance = 200; var T = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / T); self.style.transform = 'translateX(' + (distance * p * (2 - p)) + 'px)'; if(p < 1.0){ requestAnimationFrame(step); } }) })4.抛物线运动
circle.on('click', function() { var self = this; var startTime = Date.now(); var disX = 200, disY = 200; var T = 1000 * Math.sqrt(2 * disY / 98); requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / T); var tx = disX * p; var ty = disY * p * p; self.style.transform = 'translate(' + tx + 'px, ' +ty + 'px)'; if(p < 1.0){ requestAnimationFrame(step); } })5.简谐摆动
circle.on('click', function() { var self = this; var startTime = Date.now(); var distance = 100; var T = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / T); var tx = distance * Math.sin(2 * Math.PI * p); self.style.transform = 'translateX(' + tx + 'px)'; if(p < 1.0){ requestAnimationFrame(step); } }) })6.正弦线
circle.on('click', function() { var self = this; var startTime = Date.now(); var distance = 100; var T = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / T); 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); } }) })7.圆周运动
circle.on('click', function() { var self = this; var startTime = Date.now(); var distance = 100; var T = 2000; var r = 100; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / T); var rotation = p * 360; self.style.transformOrigin = 'r' + 'px ' + r + 'px'; self.style.transform = 'rotate(' + rotation +'deg)'; if(p < 1.0){ requestAnimationFrame(step); } })可以将动画封装成通用的对象,每次只需要实例定义单帧执行函数就可以了。
参数:duration-动画持续时间,progress每一帧执行的函数,easing-缓动效果(可传可不传) function Animator(duration, progress, easing) { this.duration = duration; this.progress = progress; this.easing = easing || function(p){return p}; } //调用start函数时可传入参数,如果参数为false或者传入的函数返回false,那么就说明进行循环动画,相当于将css animation中的animation-iteration-count:infinite Animator.prototype.start = function(finished) { var self = this; var startTime = Date.now(); var duration = self.duration; 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; } //如果调用start时传递的参数为false或函数返回false,next便为true说明进行循环运动 if(!next) { self.progress(self.easing(1.0), 1.0); }else { startTime += duration; //如果这里不加的话那么小球不会循环,而是一直向前运动 self.progress(self.easing(p), p); } } //next为true,动画继续执行 if(next) { requestAnimationFrame(step); } }) }目前这个函数存在的缺点就是虽然可以设置动画执行为一次或一直循环,但是并不能设置动画执行次数为2,3,4..,也不能设置动画循环的方向,即不能像css animation设置animation-direction
折线运动 让小球先向右运动再向下运动 var a1 = new Animator(1000, function(p) { var tx = 100 * p; circle.style.transform = 'translateX(' + tx + 'px)'; }) var a2 = new Animator(1000, function(p) { var ty = 100 * p; circle.style.transform = 'translate(100px,' + ty + 'px)'; }) 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(); //如果数组中的对象是Animator的实例,则直接调用 if(animator instanceof Animator) { animator.start(function() { if(self.animators.length) { play(); } }) }else { //数组中的对象不是Animator的实例,调用Animator的方法,可以在这里将之前的对象再次添加到animators数组中 animator.apply(self);//通过这里可以进行循环运动 if(self.animators.length) { play(); } } } play(); } } } 弹跳的小球 设20px为1米,下落距离为200px, 为10米g = 10m/sS = 1/2 * g * T^2 = 10T = 1.414s = 1414ms下落阶段 St = S * p^2上升阶段 St = S - Sp * (2 - p) var c1 = new Animator(1000, function(p) { var ty = 200 * p * p; circle.style.transform = 'translateY(' + ty + 'px)'; }) var c2 = new Animator(1414, function(p) { var ty = 200 - 200 * p * (2 - p); circle.style.transform = 'translateY(' + ty + 'px)'; }) circle.addEventListener('click', function() { var animators = new AnimationQueue(); animators.append(c1, c2, function execute() { this.append(c1,c2, execute); }); animators.flush(); }) 弹跳幅度逐渐减小的小球 上升时间:T = 0.7 * T上升距离:S = 0.49 * S circle.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); circle.style.transform = 'translateY(' + ty + 'px)'; }) var a2 = new Animator(T, function(p) { var s = this.duration * 200 / T; var ty = - s * p * (2 - p); circle.style.transform = 'translateY(' + ty + 'px)'; }) var animators = new AnimationQueue(); function foo() { a2.duration *= 0.7; if(a2.duration <= 0.0001) { animators.animators.length = 0; } } animators.append(a1, foo, a2, function b() { a1.duration *= 0.7; this.append(a1, foo, a2, b); }); animators.flush(); }) 滚动的小球 小球直径:d = 50px圆周长: l = π * d;周期: T = 2s滚动时间: t = 4s滚动距离:S = π * d * t / T = 314px circle3.addEventListener('click', function() { var a1 = new Animator(4000, function(p) { var rotation = 'rotate(' + 720 * p + 'deg)'; var x = 50 + 314 * p + 'px'; circle3.style.transform = rotation; circle3.style.left = x; }); var animators = new AnimationQueue(); animators.append(a1, function b() { animators.append(a1, b); }) animators.flush(); })以上就是利用我们学过的常见物理公式实现的js动画效果。当然还可以有更多复杂的效果,如何实现各种优美的动画也是值得深入学习的。