Javascript 使用 requestAnimationFrame 实现动画
lixj
8年前
<p>requestAnimationFrame优于setTimeout/setInterval的地方在于它是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销,这篇文章给大家详细介绍使用requestAnimationFrame实现js动画:仪表盘效果。</p> <p>废话不多说,先看看一个效果:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/5cc49a87a8c67ac65aa2fe8e45a53dcb.gif"></p> <p>直接上代码:</p> <pre> <code class="language-javascript"><!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>canvas仪表盘动画效果</title> <style type="text/css"> html, body { width: 100%; height: 100%; margin: 0; } canvas { display: none; border: 1px solid red; display: block; margin: 0 auto; background: -webkit-linear-gradient(top, #0e83f5 0%, #21bdf6 100%); } </style> <script type="text/javascript"> window.onload = function() { window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60); }; })(); var canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'), cWidth = canvas.width, cHeight = canvas.height, score = canvas.attributes['data-score'].value, radius = 100, //圆的半径 deg0 = Math.PI / 9, //每一格20度 mum = 100, //数字步长 /* * 要求:圆弧走完,数字得自加完,就得确定圆弧走的次数和数字走的次数相等! 数字最大10000,对应的度数是11*PI/9,那每个步长mum对应的度数如下: */ deg1 = mum * Math.PI * 11 / 9 / 10000; // 每mum对应的度数 var angle = 0, //初始角度 credit = 0; //数字默认值开始数 var drawFrame = function() { if(score < 0 || score > 10000) { alert('额度只能是0--10000') score = 10000; } ctx.save(); ctx.clearRect(0, 0, cWidth, cHeight); ctx.translate(cWidth / 2, cHeight / 2); ctx.rotate(8 * deg0); //160度 var aim = score * deg1 / mum; //数字对应的弧度数,先确定要走几次,除以mum,然后计算对应的弧度数 if(angle < aim) { angle += deg1; } if(credit < score) { credit += mum; //默认数字间隔是mum } else if(credit >= 10000) { credit = 10000; } //信用额度 ctx.save(); ctx.rotate(10 * deg0); ctx.fillStyle = 'white'; ctx.font = '28px Microsoft yahei'; ctx.textAlign = 'center'; ctx.fillText('信用额度', 0, 50); ctx.restore(); // text(credit); ctx.save(); ctx.beginPath(); ctx.lineWidth = 5; ctx.strokeStyle = 'rgba(255, 255, 255, 1)'; ctx.arc(0, 0, radius, 0, angle, false); //动画圆环 ctx.stroke(); ctx.restore(); ctx.save(); ctx.rotate(10 * deg0); //200度 ctx.restore(); ctx.beginPath(); ctx.strokeStyle = 'rgba(255, 0, 0, .1)'; ctx.lineWidth = 5; ctx.arc(0, 0, radius, 0, 11 * deg0, false); //设置外圆环220度 ctx.stroke(); ctx.restore(); window.requestAnimFrame(drawFrame); } function text(process) { ctx.save(); ctx.rotate(10 * deg0); //200度 ctx.fillStyle = 'red'; ctx.font = '40px Microsoft yahei'; ctx.textAlign = 'center'; ctx.textBaseLine = 'top'; ctx.fillText("¥:" + process, 0, 10); ctx.restore(); } setTimeout(function() { document.getElementById("canvas").style.display = "block"; drawFrame(); }, 10) } </script> </head> <body> <canvas id="canvas" width="300" height="300" data-score='8100'></canvas> </body> </html></code></pre> <p>使用requestAnimationFrame实现js动画性能好。先给大家简单介绍下requestAnimationFrame比起setTimeout、setInterval有哪些优势?</p> <p>requestAnimationFrame 比起 setTimeout、setInterval的优势主要有:</p> <p>1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。</p> <p>2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。</p> <p>3.浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果。比如,通过 requestAnimationFrame() ,JS动画能够和CSS动画/变换或SVG SMIL动画同步发生。另外,如果在一个浏览器标签页里运行一个动画,当这个标签页不可见时,浏览器会暂停它,这会减少CPU,内存的压力,节省电池电量。</p> <h2><strong>基本用法与区别:</strong></h2> <ul> <li>setTimeout(code, millseconds) 用于延时执行参数指定的代码,如果在指定的延迟时间之前,你想取消这个执行,那么直接用 clearTimeout(timeoutId) 来清除任务, timeoutID 是 setTimeout 时返回的;</li> <li>setInterval(code, millseconds) 用于每隔一段时间执行指定的代码,永无停歇,除非你反悔了,想清除它,可以使用 clearInterval(intervalId) ,这样从调用 clearInterval 开始,就不会在有重复执行的任务, intervalId 是 setInterval 时返回的;</li> <li>requestAnimationFrame(code) ,一般用于动画,与 setTimeout 方法类似,区别是 setTimeout 是用户指定的,而 requestAnimationFrame 是浏览器刷新频率决定的,一般遵循 W3C 标准,它在浏览器每次刷新页面之前执行。</li> </ul> <p>先看实现思路:</p> <p><strong>最简单:</strong></p> <pre> <code class="language-javascript">var FPS = 60; setInterval(draw, 1000/FPS); </code></pre> <p>这个简单做法,如果draw带有大量逻辑计算,导致计算时间超过帧等待时间时,将会出现丢帧。除外,如果FPS太高,超过了当时浏览器的重绘频率,将会造成计算浪费,例如浏览器实际才重绘2帧,但却计算了3帧,那么有1帧的计算就浪费了。</p> <p><strong>成熟做法:</strong></p> <p>引入requestAnimationFrame,这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数,以满足开发者操作动画的需求。</p> <p>这个函数类似setTimeout,只调用一次。</p> <pre> <code class="language-javascript">function draw() { requestAnimationFrame(draw); // ... Code for Drawing the Frame ... } </code></pre> <p>递归调用,就可以实现定时器。</p> <p>但是,这样完全跟浏览器帧频同步了,无法自定义动画的帧频,是无法满足需求的。</p> <p><strong>接下来需要考虑如何控制帧频。</strong></p> <p><strong>简单做法:</strong></p> <pre> <code class="language-javascript"> var fps = 30; function tick() { setTimeout(function() { requestAnimationFrame(tick); draw(); // ... Code for Drawing the Frame ... }, 1000 / fps); } tick(); </code></pre> <p>这种做法,比较直观的可以发现,每一次setTimeout执行的时候,都还要再等到下一个requestAnimationFrame事件到达,累积下去会造成动画变慢。</p> <p><strong>自行控制时间跨度:</strong></p> <pre> <code class="language-javascript"> var fps = 30; var now; var then = Date.now(); var interval = 1000/fps; var delta; function tick() { requestAnimationFrame(tick); now = Date.now(); delta = now - then; if (delta > interval) { // 这里不能简单then=now,否则还会出现上边简单做法的细微时间差问题。例如fps=10,每帧100ms,而现在每16ms(60fps)执行一次draw。16*7=112>100,需要7次才实际绘制一次。这个情况下,实际10帧需要112*10=1120ms>1000ms才绘制完成。 then = now - (delta % interval); draw(); // ... Code for Drawing the Frame ... } } tick(); </code></pre> <p><strong>针对低版本浏览器再优化:</strong></p> <p>如果浏览器没有requestAnimationFrame函数,实际底层还只能用setTimeout模拟,上边做的都是无用功。那么可以再改进一下。</p> <pre> <code class="language-javascript"> var fps = 30; var now; var then = Date.now(); var interval = 1000/fps; var delta; window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; function tick() { if(window.requestAnimationFrame) { requestAnimationFrame(tick); now = Date.now(); delta = now - then; if (delta > interval) { // 这里不能简单then=now,否则还会出现上边简单做法的细微时间差问题。例如fps=10,每帧100ms,而现在每16ms(60fps)执行一次draw。16*7=112>100,需要7次才实际绘制一次。这个情况下,实际10帧需要112*10=1120ms>1000ms才绘制完成。 then = now - (delta % interval); draw(); // ... Code for Drawing the Frame ... } } else { setTimeout(tick, interval); draw(); } } tick(); </code></pre> <p> </p> <p><strong>最后,还可以加上暂停。</strong></p> <pre> <code class="language-javascript"> var fps = 30; var pause = false; var now; var then = Date.now(); var interval = 1000/fps; var delta; window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; function tick() { if(pause) return; if(window.requestAnimationFrame) { ... } else { ... } } tick(); </code></pre> <h2><strong>requestAnimationFrame的用法</strong></h2> <pre> <code class="language-javascript">// shim layer with setTimeout fallback window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })(); // usage: // instead of setInterval(render, 16) .... (function animloop(){ requestAnimFrame(animloop); render(); })(); // place the rAF *before* the render() to assure as close to // 60fps with the setTimeout fallback. </code></pre> <h2><strong>对requestAnimationFrame更牢靠的封装</strong></h2> <p>Opera浏览器的技术师Erik Möller 把这个函数进行了封装 ,使得它能更好的兼容各种浏览器。你可以读一读这篇文章,但基本上他的代码就是判断使用 4ms 还是 16ms 的延迟,来最佳匹配60fps。下面就是这段代码,你可以使用它,但请注意,这段代码里使用的是标准函数,我给它加上了兼容各种 浏览器引擎前缀 。</p> <pre> <code class="language-javascript">(function() { var lastTime = 0; var vendors = ['webkit', 'moz']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }()); </code></pre> <h2><strong>我来看看使用requestAnimationFrame的效果</strong></h2> <pre> <code class="language-javascript">// requestAnim shim layer by Paul Irish window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function */ callback, /* DOMElement */ element){ window.setTimeout(callback, 1000 / 60); }; })(); // example code from mr doob : http://mrdoob.com/lab/javascript/requestanimationframe/ var canvas, context, toggle; init(); animate(); function init() { canvas = document.createElement( 'canvas' ); canvas.width = 512; canvas.height = 512; context = canvas.getContext( '2d' ); document.body.appendChild( canvas ); } function animate() { requestAnimFrame( animate ); draw(); } function draw() { var time = new Date().getTime() * 0.002; var x = Math.sin( time ) * 192 + 256; var y = Math.cos( time * 0.9 ) * 192 + 256; toggle = !toggle; context.fillStyle = toggle ? 'rgb(200,200,20)' : 'rgb(20,20,200)'; context.beginPath(); context.arc( x, y, 10, 0, Math.PI * 2, true ); context.closePath(); context.fill(); }</code></pre> <h2><strong>requestAnimationFrame API</strong></h2> <pre> <code class="language-javascript">window.requestAnimationFrame(function(/* time */ time){ // time ~= +new Date // the unix time }); </code></pre> <p>回调函数里的参数可以传入时间。</p> <h2><strong>各种浏览器对requestAnimationFrame的支持情况</strong></h2> <p>谷歌浏览器,火狐浏览器,IE10+都实现了这个函数,即使你的浏览器很古老,上面的对requestAnimationFrame封装也能让这个方法在IE8/9上不出错。</p> <p><strong>参考文章:</strong></p> <p>http://www.jb51.net/article/70678.htm</p> <p>http://www.webhek.com/requestanimationframe/</p> <p>http://www.cnblogs.com/kenkofox/p/3849067.html</p> <p>http://blog.csdn.net/qingyafan/article/details/52335753</p> <p> </p> <p>来自:http://www.cnblogs.com/libin-1/p/6096067.html</p> <p> </p>