You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

requestAnimationFrame绘制Canvas时间码丢帧,最终时间值不符求助

问题描述

我用requestAnimationFrame替代setTimeout在JavaScript Canvas上绘制时间码动画:动画包含31个图形状态,每个状态实际时长24秒,需以24倍速运行,让整个序列在31秒内预览完成。

为实现加速时间码效果,我通过时间戳计算前后帧的间隔framePeriod,再用subSeconds变量推导seconds等时间值。但动画运行31秒结束后,最终显示的时间值总是短于预期的12:24(即744秒):当subSeconds设为10毫秒时,最终值通常在12:00至12:09之间;若设为50毫秒这类更大的值,最终值甚至低至11:25。

控制台输出的framePeriod日志片段显示,前后时间戳的间隔存在波动,偶尔会出现超过1秒的情况。

控制台日志片段

34 67 100 134 167 200 234 267 344 345 367 400 434 468 501 534 568 601 635 668 710 734 771 803 834 867 901 935 968 1001 
33 66  99 133 167 199 232 267 299 334 367 399 434 467 499 534 566 600 633 667 700 734 766 800 832 868 900 933 967 1000 
34 67 100 132 166 200 233 267 300 333 367 400 433 467 500 533 567 600 633 667 699 733 823 825 833 866 900 933 966 999 1032 
34 67 101 134 167 201 234 267 301 334 367 400 434 467 500 534 568 600 634 667 700 734 767 801 834 867 901 934 968 1000 
34 68 100 135 167 200 234 267 300 334 367 401 435 467 500 534 567 601 634 667 700 734 767 801 834 867 901 934 967 1000 
34 68 100 134 168 200 234 268 301 334 367 400 435 467 501 535 567 601 634 668 701 734 767 800 834 867 901 934 967 1000 

问题演示代码

JavaScript

function animate31States() {

  const cnv = document.getElementById('draw');
  const ctx = cnv.getContext('2d');
        ctx.translate(cnv.width/2, cnv.height/2);

  let currentTime = performance.now();
  let state = 0;
  let timeUnits = 0;
  let totalSeconds = 0;

  const subSecond = 10;
  const second = 1000;

// Animation loop
  function draw(timestamp) {
    if (state >= 31) {
        return;
    }

    const framePeriod = timestamp - currentTime;
    console.log(framePeriod);

    if (framePeriod >= subSecond) {
      timeUnits++;
      totalSeconds = timeUnits * (24/31);
    }
    if (framePeriod >= second) {
        state++;
        currentTime = timestamp;
    }

    const secondsStr = clockify(totalSeconds);

    clearCanvas();

    if (state < 31) {
        drawText(`${state + 1}`, 0, -385, 24);
    } else {
      drawText('should end at 12:24', 0, -385, 16);
    }   
    drawText(secondsStr, 0, -355, 16);
    requestAnimationFrame(draw);
  
  }

  function drawText(text, x, y, size) {
      ctx.textAlign = 'center';
      ctx.font = `${size}px Helvetica Neue, Helvetica, Arial, sans-serif`;
      ctx.fillText(text, x, y);
  }

  function clockify(time) {
      const m = mins(time);
      const s = secs(time);
      return m + s;
  }

  function mins(time) {
      const m = Math.floor(time / 60);
      return `${m.toString().padStart(2, '0')}:`;
  }

  function secs(time) {
      const s = Math.floor(time % 60);
      return `${s.toString().padStart(2, '0')} `;
  }

  function clearCanvas() {
      ctx.clearRect(-cnv.width/2, -cnv.height/2, cnv.width, cnv.height);
  }


  requestAnimationFrame(draw);     // Start the animation loop
  }

  animate31States();

HTML

<canvas id="draw" width=390 height=844></canvas>
解决方案

你的核心问题在于用framePeriod判断是否累加timeUnits的逻辑存在缺陷:每次帧间隔超过subSecond就只加1个单位,但实际帧间隔可能远大于subSecond(比如日志里的823ms),这样会漏掉很多应累加的单位,导致最终时间值不足。

正确的做法是直接基于动画运行的总时长计算加速后的时间码,不受帧间隔波动影响。具体修改如下:

function animate31States() {

  const cnv = document.getElementById('draw');
  const ctx = cnv.getContext('2d');
        ctx.translate(cnv.width/2, cnv.height/2);

  const startTime = performance.now(); // 记录动画启动的初始时间
  let state = 0;

// Animation loop
  function draw(timestamp) {
    if (state >= 31) {
        // 动画结束时强制显示目标时间
        clearCanvas();
        drawText('ended at 12:24', 0, -385, 16);
        drawText('12:24', 0, -355, 16);
        return;
    }

    // 计算动画已运行的毫秒数
    const elapsedMs = timestamp - startTime;
    // 转换为加速后的真实秒数:实际运行秒数 × 24倍速
    const totalSeconds = (elapsedMs / 1000) * 24;

    // 更新状态:每1秒切换一个状态(31状态总耗时31秒)
    const newState = Math.floor(elapsedMs / 1000);
    if (newState !== state) {
        state = newState;
    }

    const secondsStr = clockify(totalSeconds);

    clearCanvas();

    if (state < 31) {
        drawText(`${state + 1}`, 0, -385, 24);
    }   
    drawText(secondsStr, 0, -355, 16);
    requestAnimationFrame(draw);
  
  }

  function drawText(text, x, y, size) {
      ctx.textAlign = 'center';
      ctx.font = `${size}px Helvetica Neue, Helvetica, Arial, sans-serif`;
      ctx.fillText(text, x, y);
  }

  function clockify(time) {
      const m = mins(time);
      const s = secs(time);
      return m + s;
  }

  function mins(time) {
      const m = Math.floor(time / 60);
      return `${m.toString().padStart(2, '0')}:`;
  }

  function secs(time) {
      const s = Math.floor(time % 60);
      return `${s.toString().padStart(2, '0')} `;
  }

  function clearCanvas() {
      ctx.clearRect(-cnv.width/2, -cnv.height/2, cnv.width, cnv.height);
  }


  requestAnimationFrame(draw);     // Start the animation loop
  }

  animate31States();

关键修改说明

  • 用启动时间计算总耗时:elapsedMs = timestamp - startTime确保准确获取动画运行的总时长,完全不受帧间隔波动影响
  • 直接计算加速时间:totalSeconds = (elapsedMs / 1000) * 24,实际运行1秒对应真实时间24秒,31秒运行结束后正好是31×24=744秒(12:24)
  • 状态切换逻辑简化:通过Math.floor(elapsedMs / 1000)直接得到当前状态,无需每帧判断帧间隔是否超过1秒
  • 结束时强制显示目标时间:避免最后一帧的时间差导致显示偏差,确保结束时准确显示12:24

这样修改后,无论帧间隔如何波动,最终时间都会准确停在12:24,完全符合预期。


内容的提问来源于stack exchange,提问作者Greg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 22:07:05