簡體   English   中英

用 requestAnimationFrame 控制 fps?

[英]Controlling fps with requestAnimationFrame?

看起來requestAnimationFrame是事實上的動畫方式。 它在大多數情況下對我來說效果很好,但現在我正在嘗試做一些畫布動畫,我想知道:有沒有辦法確保它以特定的 fps 運行? 我知道 rAF 的目的是為了始終流暢的動畫,我可能會冒着讓我的動畫斷斷續續的風險,但現在它似乎以非常任意的速度運行,我想知道是否有辦法對抗不知何故。

我會使用setInterval但我想要 rAF 提供的優化(尤其是在選項卡處於焦點時自動停止)。

如果有人想查看我的代碼,這幾乎是:

animateFlash: function() {
    ctx_fg.clearRect(0,0,canvasWidth,canvasHeight);
    ctx_fg.fillStyle = 'rgba(177,39,116,1)';
    ctx_fg.strokeStyle = 'none';
    ctx_fg.beginPath();
    for(var i in nodes) {
        nodes[i].drawFlash();
    }
    ctx_fg.fill();
    ctx_fg.closePath();
    var instance = this;
    var rafID = requestAnimationFrame(function(){
        instance.animateFlash();
    })

    var unfinishedNodes = nodes.filter(function(elem){
        return elem.timer < timerMax;
    });

    if(unfinishedNodes.length === 0) {
        console.log("done");
        cancelAnimationFrame(rafID);
        instance.animate();
    }
}

其中 Node.drawFlash() 只是一些根據計數器變量確定半徑然后繪制圓的代碼。

如何將 requestAnimationFrame 限制為特定的幀速率

5 FPS 的演示節流:http: //jsfiddle.net/m1erickson/CtsY3/

此方法通過測試自執行最后一幀循環以來經過的時間來工作。

您的繪圖代碼僅在您指定的 FPS 間隔已過時執行。

代碼的第一部分設置了一些用於計算經過時間的變量。

var stop = false;
var frameCount = 0;
var $results = $("#results");
var fps, fpsInterval, startTime, now, then, elapsed;


// initialize the timer variables and start the animation

function startAnimating(fps) {
    fpsInterval = 1000 / fps;
    then = Date.now();
    startTime = then;
    animate();
}

這段代碼是實際的 requestAnimationFrame 循環,它以您指定的 FPS 進行繪制。

// the animation loop calculates time elapsed since the last loop
// and only draws if your specified fps interval is achieved

function animate() {

    // request another frame

    requestAnimationFrame(animate);

    // calc elapsed time since last loop

    now = Date.now();
    elapsed = now - then;

    // if enough time has elapsed, draw the next frame

    if (elapsed > fpsInterval) {

        // Get ready for next frame by setting then=now, but also adjust for your
        // specified fpsInterval not being a multiple of RAF's interval (16.7ms)
        then = now - (elapsed % fpsInterval);

        // Put your drawing code here

    }
}

2016/6 更新

限制幀速率的問題是屏幕具有恆定的更新速率,通常為 60 FPS。

如果我們想要 24 fps,我們將永遠無法在屏幕上獲得真正的 24 fps,我們可以這樣計時但不顯示它,因為顯示器只能以 15 fps、30 fps 或 60 fps 顯示同步幀(某些顯示器也 120 fps )。

但是,出於計時目的,我們可以在可能的情況下計算和更新。

您可以通過將計算和回調封裝到一個對象中來構建用於控制幀速率的所有邏輯:

function FpsCtrl(fps, callback) {

    var delay = 1000 / fps,                               // calc. time per frame
        time = null,                                      // start time
        frame = -1,                                       // frame count
        tref;                                             // rAF time reference

    function loop(timestamp) {
        if (time === null) time = timestamp;              // init start time
        var seg = Math.floor((timestamp - time) / delay); // calc frame no.
        if (seg > frame) {                                // moved to next frame?
            frame = seg;                                  // update
            callback({                                    // callback function
                time: timestamp,
                frame: frame
            })
        }
        tref = requestAnimationFrame(loop)
    }
}

然后添加一些控制器和配置代碼:

// play status
this.isPlaying = false;

// set frame-rate
this.frameRate = function(newfps) {
    if (!arguments.length) return fps;
    fps = newfps;
    delay = 1000 / fps;
    frame = -1;
    time = null;
};

// enable starting/pausing of the object
this.start = function() {
    if (!this.isPlaying) {
        this.isPlaying = true;
        tref = requestAnimationFrame(loop);
    }
};

this.pause = function() {
    if (this.isPlaying) {
        cancelAnimationFrame(tref);
        this.isPlaying = false;
        time = null;
        frame = -1;
    }
};

用法

它變得非常簡單 - 現在,我們所要做的就是通過設置回調函數和所需的幀速率來創建一個實例,如下所示:

var fc = new FpsCtrl(24, function(e) {
     // render each frame here
  });

然后開始(如果需要,這可能是默認行為):

fc.start();

就是這樣,所有的邏輯都在內部處理。

演示

 var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0; ctx.font = "20px sans-serif"; // update canvas with some information and animation var fps = new FpsCtrl(12, function(e) { ctx.clearRect(0, 0, c.width, c.height); ctx.fillText("FPS: " + fps.frameRate() + " Frame: " + e.frame + " Time: " + (e.time - pTime).toFixed(1), 4, 30); pTime = e.time; var x = (pTime - mTime) * 0.1; if (x > c.width) mTime = pTime; ctx.fillRect(x, 50, 10, 10) }) // start the loop fps.start(); // UI bState.onclick = function() { fps.isPlaying ? fps.pause() : fps.start(); }; sFPS.onchange = function() { fps.frameRate(+this.value) }; function FpsCtrl(fps, callback) { var delay = 1000 / fps, time = null, frame = -1, tref; function loop(timestamp) { if (time === null) time = timestamp; var seg = Math.floor((timestamp - time) / delay); if (seg > frame) { frame = seg; callback({ time: timestamp, frame: frame }) } tref = requestAnimationFrame(loop) } this.isPlaying = false; this.frameRate = function(newfps) { if (!arguments.length) return fps; fps = newfps; delay = 1000 / fps; frame = -1; time = null; }; this.start = function() { if (!this.isPlaying) { this.isPlaying = true; tref = requestAnimationFrame(loop); } }; this.pause = function() { if (this.isPlaying) { cancelAnimationFrame(tref); this.isPlaying = false; time = null; frame = -1; } }; }
 body {font:16px sans-serif}
 <label>Framerate: <select id=sFPS> <option>12</option> <option>15</option> <option>24</option> <option>25</option> <option>29.97</option> <option>30</option> <option>60</option> </select></label><br> <canvas id=c height=60></canvas><br> <button id=bState>Start/Stop</button>

舊答案

requestAnimationFrame的主要目的是同步更新到監視器的刷新率。 這將要求您以顯示器的 FPS 或它的一個因素(即 60、30、15 FPS 的典型刷新率 @ 60 Hz)進行動畫處理。

如果您想要更隨意的 FPS,那么使用 rAF 毫無意義,因為幀速率無論如何都不會匹配顯示器的更新頻率(只是這里和那里的一幀),這根本無法為您提供流暢的動畫(與所有幀重新計時一樣) ) 你也可以使用setTimeoutsetInterval代替。

當您想以不同的 FPS 播放視頻,然后顯示它的設備刷新時,這也是專業視頻行業的一個眾所周知的問題。 已經使用了許多技術,例如幀混合和基於運動矢量的復雜重新定時重新構建中間幀,但是對於畫布,這些技術不可用,結果將始終是生澀的視頻。

var FPS = 24;  /// "silver screen"
var isPlaying = true;

function loop() {
    if (isPlaying) setTimeout(loop, 1000 / FPS);

    ... code for frame here
}

我們將setTimeout放在首位(以及為什么在使用 poly-fill 時將rAF放在首位)的原因是這樣會更准確,因為setTimeout會在循環開始時立即將事件排隊,這樣無論剩余多少時間代碼將使用(只要它不超過超時間隔)下一次調用將在它表示的間隔內(對於純 rAF 這不是必需的,因為 rAF 在任何情況下都會嘗試跳到下一幀)。

同樣值得注意的是,將它放在第一位也會像setInterval一樣冒着調用堆疊的風險。 對於這種用途, setInterval可能會更准確一些。

您可以在循環之外使用setInterval來做同樣的事情。

var FPS = 29.97;   /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);

function loop() {

    ... code for frame here
}

並停止循環:

clearInterval(rememberMe);

為了在選項卡變得模糊時降低幀速率,您可以添加如下因素:

var isFocus = 1;
var FPS = 25;

function loop() {
    setTimeout(loop, 1000 / (isFocus * FPS)); /// note the change here

    ... code for frame here
}

window.onblur = function() {
    isFocus = 0.5; /// reduce FPS to half   
}

window.onfocus = function() {
    isFocus = 1; /// full FPS
}

這樣您可以將 FPS 降低到 1/4 等。

我建議將您對requestAnimationFrame的調用包裝在setTimeout中:

const fps = 25;
function animate() {
  // perform some animation task here

  setTimeout(() => {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
animate();

您需要從setTimeout中調用requestAnimationFrame ,而不是相反,因為requestAnimationFrame會安排您的函數在下一次重繪之前運行,如果您使用setTimeout進一步延遲更新,您將錯過那個時間窗口。 但是,反向操作是合理的,因為您只是在發出請求之前等待一段時間。

這些都是理論上的好主意,直​​到你深入為止。 問題是你不能在不去同步的情況下限制 RAF,破壞它的存在目的。 所以你讓它全速運行,並在一個單獨的循環甚至一個單獨的線程中更新你的數據!

是的,我說過。 可以在瀏覽器中執行多線程 JavaScript!

我知道有兩種方法在沒有卡頓的情況下效果非常好,使用更少的果汁並產生更少的熱量。 准確的人工計時和機器效率是最終結果。

抱歉,如果這有點羅嗦,但這里有......


方法一:通過setInterval更新數據,通過RAF更新圖形。

使用單獨的 setInterval 更新平移和旋轉值、物理、碰撞等。將這些值保存在每個動畫元素的對象中。 將轉換字符串分配給每個 setInterval 'frame' 對象中的一個變量。 將這些對象保存在一個數組中。 以毫秒為單位將間隔設置為所需的 fps:ms=(1000/fps)。 這可以保持穩定的時鍾,在任何設備上都允許相同的 fps,無論 RAF 速度如何。 不要將變換分配給這里的元素!

在 requestAnimationFrame 循環中,使用老式的 for 循環遍歷您的數組——不要在這里使用較新的形式,它們很慢!

for(var i=0; i<sprite.length-1; i++){  rafUpdate(sprite[i]);  }

在您的 rafUpdate 函數中,從數組中的 js 對象獲取轉換字符串及其元素 id。 您應該已經將您的“精靈”元素附加到變量或通過其他方式輕松訪問,這樣您就不會浪費時間在 RAF 中“獲取”它們。 將它們保存在以它們的 html id 命名的對象中效果很好。 在它進入您的 SI 或 RAF 之前設置該部分。

僅使用 RAF 更新您的變換,使用 3D 變換(即使是 2d),並設置 css "will-change: transform;" 關於會改變的元素。 這使您的轉換盡可能地與本機刷新率同步,啟動 GPU,並告訴瀏覽器最集中的位置。

所以你應該有這樣的偽代碼......

// refs to elements to be transformed, kept in an array
var element = [
   mario: document.getElementById('mario'),
   luigi: document.getElementById('luigi')
   //...etc.
]

var sprite = [  // read/write this with SI.  read-only from RAF
   mario: { id: mario  ....physics data, id, and updated transform string (from SI) here  },
   luigi: {  id: luigi  .....same  }
   //...and so forth
] // also kept in an array (for efficient iteration)

//update one sprite js object
//data manipulation, CPU tasks for each sprite object
//(physics, collisions, and transform-string updates here.)
//pass the object (by reference).
var SIupdate = function(object){
  // get pos/rot and update with movement
  object.pos.x += object.mov.pos.x;  // example, motion along x axis
  // and so on for y and z movement
  // and xyz rotational motion, scripted scaling etc

  // build transform string ie
  object.transform =
   'translate3d('+
     object.pos.x+','+
     object.pos.y+','+
     object.pos.z+
   ') '+

   // assign rotations, order depends on purpose and set-up. 
   'rotationZ('+object.rot.z+') '+
   'rotationY('+object.rot.y+') '+
   'rotationX('+object.rot.x+') '+

   'scale3d('.... if desired
  ;  //...etc.  include 
}


var fps = 30; //desired controlled frame-rate


// CPU TASKS - SI psuedo-frame data manipulation
setInterval(function(){
  // update each objects data
  for(var i=0; i<sprite.length-1; i++){  SIupdate(sprite[i]);  }
},1000/fps); //  note ms = 1000/fps


// GPU TASKS - RAF callback, real frame graphics updates only
var rAf = function(){
  // update each objects graphics
  for(var i=0; i<sprite.length-1; i++){  rAF.update(sprite[i])  }
  window.requestAnimationFrame(rAF); // loop
}

// assign new transform to sprite's element, only if it's transform has changed.
rAF.update = function(object){     
  if(object.old_transform !== object.transform){
    element[object.id].style.transform = transform;
    object.old_transform = object.transform;
  }
} 

window.requestAnimationFrame(rAF); // begin RAF

這使您對數據對象和變換字符串的更新保持同步到 SI 中所需的“幀”速率,並且 RAF 中的實際變換分配同步到 GPU 刷新率。 因此,實際的圖形更新僅在 RAF 中,但對數據的更改和構建轉換字符串在 SI 中,因此沒有 jankies,而是“時間”以所需的幀速率流動。


流動:

[setup js sprite objects and html element object references]

[setup RAF and SI single-object update functions]

[start SI at percieved/ideal frame-rate]
  [iterate through js objects, update data transform string for each]
  [loop back to SI]

[start RAF loop]
  [iterate through js objects, read object's transform string and assign it to it's html element]
  [loop back to RAF]

方法 2. 將 SI 放在 web-worker 中。 這一款非常流暢!

與方法 1 相同,但將 SI 放在 web-worker 中。 然后它將在一個完全獨立的線程上運行,讓頁面只處理 RAF 和 UI。 將精靈數組作為“可轉移對象”來回傳遞。 這是 buko 快。 克隆或序列化不需要時間,但它不像通過引用傳遞,因為來自另一側的引用被破壞了,所以你需要讓雙方都傳遞到另一側,並且只在存在時更新它們,排序就像在高中時和你的女朋友來回傳遞一張紙條。

一次只有一個人可以讀寫。 這很好,只要他們檢查它是否未定義以避免錯誤。 RAF 速度很快,會立即將其踢回,然后通過一堆 GPU 幀檢查它是否已被發回。 web-worker 中的 SI 將大部分時間擁有 sprite 數組,並將更新位置、運動和物理數據,以及創建新的轉換字符串,然后將其傳遞回頁面中的 RAF。

這是我所知道的通過腳本為元素設置動畫的最快方式。 這兩個函數將作為兩個單獨的程序在兩個單獨的線程上運行,以單個 js 腳本所不具備的方式利用多核 CPU。 多線程javascript動畫。

並且它會在沒有卡頓的情況下順利進行,但在實際指定的幀速率下,幾乎沒有分歧。


結果:

這兩種方法中的任何一種都可以確保您的腳本在任何 PC、手機、平板電腦等上以相同的速度運行(當然,在設備和瀏覽器的能力范圍內)。

如何輕松限制到特定的 FPS:

// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
    maxFPS = 30,
    timestep = 1000 / maxFPS; // ms for each frame

function main(timestamp) {
    window.requestAnimationFrame(main);

    // skip if timestep ms hasn't passed since last frame
    if (timestamp - lastTimestamp < timestep) return;

    lastTimestamp = timestamp;

    // draw frame here
}

window.requestAnimationFrame(main);

來源: Isaac Sukin 對 JavaScript 游戲循環和時序的詳細解釋

var time = 0;
var time_framerate = 1000; //in milliseconds

function animate(timestamp) {
  if(timestamp > time + time_framerate) {
    time = timestamp;    

    //your code
  }

  window.requestAnimationFrame(animate);
}

最簡單的方法

note :它在具有不同幀速率的不同屏幕上可能表現不同。


const FPS = 30;
let lastTimestamp = 0;


function update(timestamp) {
  requestAnimationFrame(update);
  if (timestamp - lastTimestamp < 1000 / FPS) return;
  
  
   /* <<< PUT YOUR CODE HERE >>>  */

 
  lastTimestamp = timestamp;
}


update();

這個問題的一個簡單解決方案是在不需要渲染幀的情況下從渲染循環返回:

const FPS = 60;
let prevTick = 0;    

function render() 
{
    requestAnimationFrame(render);

    // clamp to fixed framerate
    let now = Math.round(FPS * Date.now() / 1000);
    if (now == prevTick) return;
    prevTick = now;

    // otherwise, do your stuff ...
}

重要的是要知道 requestAnimationFrame 取決於用戶監視器刷新率 (vsync)。 因此,如果您在模擬中沒有使用單獨的計時器機制,例如依靠 requestAnimationFrame 來獲得游戲速度將使其無法在 200Hz 顯示器上播放。

跳過requestAnimationFrame會導致自定義 fps 的動畫不流暢

 // Input/output DOM elements var $results = $("#results"); var $fps = $("#fps"); var $period = $("#period"); // Array of FPS samples for graphing // Animation state/parameters var fpsInterval, lastDrawTime, frameCount_timed, frameCount, lastSampleTime, currentFps=0, currentFps_timed=0; var intervalID, requestID; // Setup canvas being animated var canvas = document.getElementById("c"); var canvas_timed = document.getElementById("c2"); canvas_timed.width = canvas.width = 300; canvas_timed.height = canvas.height = 300; var ctx = canvas.getContext("2d"); var ctx2 = canvas_timed.getContext("2d"); // Setup input event handlers $fps.on('click change keyup', function() { if (this.value > 0) { fpsInterval = 1000 / +this.value; } }); $period.on('click change keyup', function() { if (this.value > 0) { if (intervalID) { clearInterval(intervalID); } intervalID = setInterval(sampleFps, +this.value); } }); function startAnimating(fps, sampleFreq) { ctx.fillStyle = ctx2.fillStyle = "#000"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx2.fillRect(0, 0, canvas.width, canvas.height); ctx2.font = ctx.font = "32px sans"; fpsInterval = 1000 / fps; lastDrawTime = performance.now(); lastSampleTime = lastDrawTime; frameCount = 0; frameCount_timed = 0; animate(); intervalID = setInterval(sampleFps, sampleFreq); animate_timed() } function sampleFps() { // sample FPS var now = performance.now(); if (frameCount > 0) { currentFps = (frameCount / (now - lastSampleTime) * 1000).toFixed(2); currentFps_timed = (frameCount_timed / (now - lastSampleTime) * 1000).toFixed(2); $results.text(currentFps + " | " + currentFps_timed); frameCount = 0; frameCount_timed = 0; } lastSampleTime = now; } function drawNextFrame(now, canvas, ctx, fpsCount) { // Just draw an oscillating seconds-hand var length = Math.min(canvas.width, canvas.height) / 2.1; var step = 15000; var theta = (now % step) / step * 2 * Math.PI; var xCenter = canvas.width / 2; var yCenter = canvas.height / 2; var x = xCenter + length * Math.cos(theta); var y = yCenter + length * Math.sin(theta); ctx.beginPath(); ctx.moveTo(xCenter, yCenter); ctx.lineTo(x, y); ctx.fillStyle = ctx.strokeStyle = 'white'; ctx.stroke(); var theta2 = theta + 3.14/6; ctx.beginPath(); ctx.moveTo(xCenter, yCenter); ctx.lineTo(x, y); ctx.arc(xCenter, yCenter, length*2, theta, theta2); ctx.fillStyle = "rgba(0,0,0,.1)" ctx.fill(); ctx.fillStyle = "#000"; ctx.fillRect(0,0,100,30); ctx.fillStyle = "#080"; ctx.fillText(fpsCount,10,30); } // redraw second canvas each fpsInterval (1000/fps) function animate_timed() { frameCount_timed++; drawNextFrame( performance.now(), canvas_timed, ctx2, currentFps_timed); setTimeout(animate_timed, fpsInterval); } function animate(now) { // request another frame requestAnimationFrame(animate); // calc elapsed time since last loop var elapsed = now - lastDrawTime; // if enough time has elapsed, draw the next frame if (elapsed > fpsInterval) { // Get ready for next frame by setting lastDrawTime=now, but... // Also, adjust for fpsInterval not being multiple of 16.67 lastDrawTime = now - (elapsed % fpsInterval); frameCount++; drawNextFrame(now, canvas, ctx, currentFps); } } startAnimating(+$fps.val(), +$period.val());
 input{ width:100px; } #tvs{ color:red; padding:0px 25px; } H3{ font-weight:400; }
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <h3>requestAnimationFrame skipping <span id="tvs">vs.</span> setTimeout() redraw</h3> <div> <input id="fps" type="number" value="33"/> FPS: <span id="results"></span> </div> <div> <input id="period" type="number" value="1000"/> Sample period (fps, ms) </div> <canvas id="c"></canvas><canvas id="c2"></canvas>

@tavnab 的原始代碼。

我總是以非常簡單的方式做到這一點,而不會弄亂時間戳:

let fps, eachNthFrame, frameCount;

fps = 30;

//This variable specifies how many frames should be skipped.
//If it is 1 then no frames are skipped. If it is 2, one frame 
//is skipped so "eachSecondFrame" is renderd.
eachNthFrame = Math.round((1000 / fps) / 16.66);

//This variable is the number of the current frame. It is set to eachNthFrame so that the 
//first frame will be renderd.
frameCount = eachNthFrame;

requestAnimationFrame(frame);

//I think the rest is self-explanatory
function frame() {
  if (frameCount === eachNthFrame) {
    frameCount = 0;
    animate();
  }
  frameCount++;
  requestAnimationFrame(frame);
}

這是user1693593代碼重構

class FpsCtrl {
  constructor(callback, fps) {
    let 
    _fps = fps, delay = 1000/_fps, 
    time = null, frame = -1, tref, run = false,
    loop = timestamp => {
      if(time === null) time = timestamp;
      let seg = Math.floor((timestamp - time) / delay);
      if(frame < seg ) {
        frame = seg;
        callback({ time: timestamp, frame });
      }
      tref = requestAnimationFrame(loop);
    };

    Object.defineProperties(this, {
      fps: {
        get() { return _fps; },
        set(v) {
          if(this.run) {this.run = false;}
          _fps = v; delay = 1000/v;
          this.run = true;
        }
      },
      run: {
        get() { return run; },
        set(v) {
          if(run && v) { return; }
          if(v) {
            tref = requestAnimationFrame(loop);
          }
          else {
            cancelAnimationFrame(tref);
            time = null;
            frame = -1;
          }
          run = v;
        }
      }
    });
  }
}

let fc = new FpsCtrl(function() {
  console.log(10);
}, 1);
fc.run = true;
// fc.fps = 10;
// fc.run = false; // It is frame stop

謝謝user1693593。

如需將 FPS 限制為任何值,請參閱jdmayfields answer 但是,對於將幀速率減半的非常快速和簡單的解決方案,您可以通過以下方式僅每隔 2 幀進行一次計算:

requestAnimationFrame(render);
function render() {
  // ... computations ...
  requestAnimationFrame(skipFrame);
}
function skipFrame() { requestAnimationFrame(render); }

同樣,您始終可以調用render但使用變量來控制這次是否進行計算,從而使您還可以將 FPS 減少到三分之一或四分之一(在我的情況下,對於示意圖 webgl-animation 20fps 仍然足夠,同時大大降低客戶端的計算負載)

我嘗試了針對此問題提供的多種解決方案。 盡管解決方案按預期工作,但它們導致的輸出並不那么專業。

根據我的個人經驗,我強烈建議不要在瀏覽器端控制 FPS,尤其是使用 requestAnimationFrame。 因為,當你這樣做時,它會使幀渲染體驗非常不穩定,用戶會清楚地看到幀跳躍,最后看起來一點也不真實或專業。

因此,我的建議是在發送自身時從服務器端控制 FPS,並在瀏覽器端收到幀后立即渲染幀。

注意:如果您仍想在客戶端進行控制,請盡量避免在控制 fps 的邏輯中使用 setTimeout 或 Date 對象。 因為,當 FPS 很高時,它們會在事件循環或對象創建方面引入它們自己的延遲。

這是達到所需fps的想法:

  1. 檢測瀏覽器的animationFrameRate (通常為60fps)
  2. 根據animationFrameRate和你的disiredFrameRate構建一個bitSet (比如 24fps)
  3. 查找bitSet並有條件地“繼續”動畫幀循環

它使用requestAnimationFrame因此實際幀速率不會大於animationFrameRate 你可以根據animationFrameRate調整disiredFrameRate

我寫了一個迷你庫和一個畫布動畫演示。

 function filterNums(nums, jitter = 0.2, downJitter = 1 - 1 / (1 + jitter)) { let len = nums.length; let mid = Math.floor(len % 2 === 0 ? len / 2 : (len - 1) / 2), low = mid, high = mid; let lower = true, higher = true; let sum = nums[mid], count = 1; for (let i = 1, j, num; i <= mid; i += 1) { if (higher) { j = mid + i; if (j === len) break; num = nums[j]; if (num < (sum / count) * (1 + jitter)) { sum += num; count += 1; high = j; } else { higher = false; } } if (lower) { j = mid - i; num = nums[j]; if (num > (sum / count) * (1 - downJitter)) { sum += num; count += 1; low = j; } else { lower = false; } } } return nums.slice(low, high + 1); } function snapToOrRound(n, values, distance = 3) { for (let i = 0, v; i < values.length; i += 1) { v = values[i]; if (n >= v - distance && n <= v + distance) { return v; } } return Math.round(n); } 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'); } return new Promise((resolve) => { let num = Math.floor(numIntervals); let numFrames = num + 1; let last; let intervals = []; let i = 0; let tick = () => { let now = performance.now(); i += 1; if (i < numFrames) { requestAnimationFrame(tick); } if (i === 1) { last = now; } else { intervals.push(now - last); last = now; if (i === numFrames) { let compareFn = (a, b) => a < b ? -1 : a > b ? 1 : 0; let sortedIntervals = intervals.slice().sort(compareFn); let selectedIntervals = filterNums(sortedIntervals, 0.2, 0.1); let selectedDuration = selectedIntervals.reduce((s, n) => s + n, 0); let seletedFrameRate = 1000 / (selectedDuration / selectedIntervals.length); let finalFrameRate = snapToOrRound(seletedFrameRate, [60, 120, 90, 30], 5); resolve(finalFrameRate); } } }; 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);
 <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>

對先前答案的簡化解釋。 至少如果您想要實時、准確的節流而不會出現卡頓,或者像炸彈一樣丟幀。 GPU和CPU友好。

setInterval 和 setTimeout 都是面向 CPU 的,而不是 GPU。

requestAnimationFrame 純粹是面向 GPU 的。

分別運行它們 這很簡單而且不笨拙。 在您的 setInterval 中,更新您的數學並在字符串中創建一個小的 CSS 腳本。 使用您的 RAF 循環,僅使用該腳本來更新元素的新坐標。 不要在 RAF 循環中執行任何其他操作。

RAF 本質上與 GPU 相關聯。 只要腳本沒有改變(即因為 SI 運行速度慢了無數倍),基於 Chromium 的瀏覽器就知道它們不需要做任何事情,因為沒有任何改變。 因此,動態腳本創建每個“幀”,例如每秒 60 次,對於 1000 個 RAF GPU 幀仍然相同,但它知道什么都沒有改變,最終結果是它不會在這方面浪費任何能量。 如果您檢查DevTools ,您將看到您的 GPU 幀速率寄存器以 setInterval 描述的速率

真的,就是這么簡單。 分開他們,他們就會合作。

沒有詹基斯。

這是我找到的一個很好的解釋: CreativeJS.com ,在傳遞給 requestAnimationFrame 的函數中包裝 setTimeou) 調用。 我對“普通” requestionAnimationFrame 的擔憂是,“如果我只希望它每秒動畫三次怎么辦?” 即使使用 requestAnimationFrame(而不是 setTimeout),它仍然會浪費(一些)“能量”(意味着瀏覽器代碼正在做某事,並且可能會減慢系統速度)每秒 60 或 120 次或任何次數,如與每秒只有兩次或三次(如您所願)相反。

大多數時候,出於這個原因,我故意使用 JavaScript運行我的瀏覽器。 但是,我使用的是 Yosemite 10.10.3,我認為它存在某種計時器問題 - 至少在我的舊系統上(相對較舊 - 意思是 2011 年)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM