Javascript 使用 requestAnimationFrame 实现动画

lixj 7年前
   <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>&nbsp;</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>