一种canvas动画的帧速控制方法

在使用requestAnimationFrame时,如何以指定的帧速绘制动画?

我能想到的是:假设检测到浏览器允许的requestAnimationFrame帧速为60fps,要实现30fps动画只需在每2个requestAnimationFrame回调中丢弃1个,要实现24fps则在每5个中丢弃3个,要实现59fps呢?当然是在每60个中丢弃1个。

关于帧丢弃的规则,我之前的设想是预设一些帧丢弃的范式例如 60fps转24fps的范式为[1,0,0,1,0,1,0,0,1,0,...](其中的0代表丢弃帧/放弃绘制)。考虑到范式预设得少了可能不够用,而预设得多了又会导致代码臃肿,因此只好写方法来生成帧丢弃的范式。

至此,帧速控制得实现思路算是通了:

  • 检测浏览器requestAnimationFrame的帧速animationFrameRate(典型值是60fps)
  • 根据animationFrameRate和你期望的帧速desiredFrameRate (比如说24fps)构建帧丢弃的范式bitSet
  • 从帧丢弃范式 bitSet 中查找根据,决定当前帧是否该丢弃

接下来就是编写代码和测试功能的事。

经过几番调优,我总算有了份拿得出手的迷你代码库。这份代码库/用法/效果演示已经放在stackoverflow.com Controlling fps with requestAnimationFrame?。考虑到墙内墙外两个世界,本文中也嵌入一份,如下:

<div>
  Animation Frame Rate: <span id="animationFrameRate">--</span>
</div>
<div>
  Desired Frame Rate: <input id="frameRateInput" type="range" min="1" max="60" step="1" list="frameRates" />
  <output id="frameRateOutput"></output>
  <datalist id="frameRates">
    <option>15</option>
    <option>24</option>
    <option>30</option>
    <option>48</option>
    <option>60</option>
  </datalist>
</div>
<div>
  Actual Frame Rate: <span id="actualFrameRate">--</span>
</div>

<canvas id="digitalClock" width="240" height="48"></canvas>
function detectAnimationFrameRate(numIntervals = 6){
  if(typeof numIntervals !== 'number' || !isFinite(numIntervals) || numIntervals < 2){
    throw new RangeError('Argument numIntervals should be a number not less than 2');
  }
  let intervals = Math.floor(numIntervals);
  return new Promise((resolve) => {
    let numFrames = intervals + 1;
    let then;
    let i = 0;
    let tick = () => {
      let now = performance.now();
      i += 1;
      if(i < numFrames){
        requestAnimationFrame(tick);
      }
      if(i === 1){
        then = now;
      }else{
        if(i === numFrames){
          resolve(Math.round(1000 / ((now - then) / intervals)));
        }
      }
    };
    requestAnimationFrame(() => {
      requestAnimationFrame(tick);
    });
  });
}
function buildFrameBitSet(animationFrameRate, desiredFrameRate){
  let bitSet = new Uint8Array(animationFrameRate);
  let ratio = desiredFrameRate / animationFrameRate;
  if(ratio >= 1)
    return bitSet.fill(1);
  for(let i = 0, prev = -1, curr; i < animationFrameRate; i += 1, prev = curr){
    curr = Math.floor(i * ratio);
    bitSet[i] = (curr !== prev) ? 1 : 0;
  }
  return bitSet;
}



let $ = (s, c = document) => c.querySelector(s);
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));

async function main(){
  let canvas = $('#digitalClock');
  let context2d = canvas.getContext('2d');
  await new Promise((resolve) => {
    if(window.requestIdleCallback){
      requestIdleCallback(resolve, {timeout:3000});
    }else{
      setTimeout(resolve, 0, {didTimeout: false});
    }
  });
  let animationFrameRate = await detectAnimationFrameRate(10); // 1. detect animation frame rate
  let desiredFrameRate = 24;
  let frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); // 2. build a bit set
  let handle;
  let i = 0;
  let count = 0, then, actualFrameRate = $('#actualFrameRate'); // debug-only

  let draw = () => {
    if(++i >= animationFrameRate){ // shoud use === if frameBits don't change dynamically
      i = 0;
      /* debug-only */
      let now = performance.now();
      let deltaT = now - then;
      let fps = 1000 / (deltaT / count);
      actualFrameRate.textContent = fps;
      then = now;
      count = 0;
    }
    if(frameBits[i] === 0){ // 3. lookup the bit set
      handle = requestAnimationFrame(draw);
      return;
    }
    count += 1; // debug-only
    let d = new Date();
    let text = d.getHours().toString().padStart(2, '0') + ':' +
        d.getMinutes().toString().padStart(2, '0') + ':' +
        d.getSeconds().toString().padStart(2, '0') + '.' +
        (d.getMilliseconds() / 10).toFixed(0).padStart(2, '0');
    context2d.fillStyle = '#000000';
    context2d.fillRect(0, 0, canvas.width, canvas.height);
    context2d.font = '36px monospace';
    context2d.fillStyle = '#ffffff';
    context2d.fillText(text, 0, 36);
    handle = requestAnimationFrame(draw);
  };
  handle = requestAnimationFrame(() => {
    then = performance.now();
    handle = requestAnimationFrame(draw);
  });

  /* debug-only */
  $('#animationFrameRate').textContent = animationFrameRate;
  let frameRateInput = $('#frameRateInput');
  let frameRateOutput = $('#frameRateOutput');
  frameRateInput.addEventListener('input', (e) => {
    frameRateOutput.value = e.target.value;
  });
  frameRateInput.max = animationFrameRate;
  frameRateOutput.value = frameRateOutput.value = desiredFrameRate;
  frameRateInput.addEventListener('change', (e) => {
    desiredFrameRate = +e.target.value;
    frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate);
  });
}

document.addEventListener('DOMContentLoaded', main);
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
贝塞尔曲线是一种平滑的曲线,可以用于制作动画效果。在canvas中,我们可以使用贝塞尔曲线来绘制路径,并通过动画让路径呈现出流畅的变化。 下面是一个使用贝塞尔曲线实现动画效果的示例: ```html <canvas id="myCanvas" width="500" height="500"></canvas> ``` ```javascript // 获取canvas元素 var canvas = document.getElementById("myCanvas"); var ctx = canvas.getContext("2d"); // 定义起点、终点和控制点 var startPoint = {x: 50, y: 50}; var endPoint = {x: 450, y: 450}; var controlPoint = {x: 250, y: 50}; // 定义动画数和当前数 var frames = 60; var currentFrame = 0; // 绘制贝塞尔曲线路径 function drawBezierPath() { ctx.beginPath(); ctx.moveTo(startPoint.x, startPoint.y); ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); ctx.stroke(); } // 清除canvas function clearCanvas() { ctx.clearRect(0, 0, canvas.width, canvas.height); } // 动画函数 function animate() { clearCanvas(); drawBezierPath(); currentFrame++; // 当前数小于动画数时,继续执行动画 if (currentFrame < frames) { controlPoint.y += 2; requestAnimationFrame(animate); } } // 启动动画 animate(); ``` 在这个示例中,我们定义了起点、终点和控制点的坐标,并使用`quadraticCurveTo()`方法绘制了贝塞尔曲线路径。然后定义了动画数和当前数,以及动画函数`animate()`。在动画函数中,我们清除canvas,重新绘制路径,将控制点的y坐标逐渐增加,实现动画效果。 你可以根据自己的需求修改起点、终点和控制点的坐标,以及动画数和控制点的移动方式,来实现不同的贝塞尔曲线动画效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值