Почему requestAnimationFrame лучше, чем setInterval или setTimeout

Почему я должен использовать requestAnimationFrame, а не setTimeout или setInterval?

Этот вопрос с автоответчиком является примером документации.

Ответ 1

Высококачественная анимация.

На этот вопрос просто ответит. requestAnimationFrame создает более качественную анимацию, полностью устраняющую мерцание и сдвиг, которые могут возникать при использовании setTimeout или setInterval, а также уменьшают или полностью удаляют пропуски кадров.

ножницы

когда новый буфер холста отображается в буфер отображения на полпути через сканирование дисплея, что приводит к сдвиговой линии, вызванной несогласованными позициями анимации.

мерцать

вызвано, когда буфер холста отображается в буфер отображения до того, как холст был полностью отображен.

Пропустить рамку

вызвано, когда время между кадрами рендеринга не находится в точной синхронизации с оборудованием дисплея. Каждый так много кадров будет пропущен, создавая непоследовательную анимацию. (Есть способ уменьшить это, но лично я считаю, что эти методы приводят к худшим общим результатам). Поскольку большинство устройств используют 60 кадров в секунду (или несколько), что приводит к новому кадру каждые 16,666... мс, а таймеры setTimeout и setInterval используют целые числа значения, которые они никогда не могут идеально соответствовать частоте кадров (округление до 17 мс, если у вас есть interval = 1000/60)


Демонстрация стоит тысячи слов.

Обновление Ответ на вопрос requestAnimationFrame loop not correct fps показывает, как время цикла setTimeout является непоследовательным и сравнивает его с requestAnimationFrame.

Демонстрация показывает простую анимацию (полосы, перемещающиеся по экрану), нажатие кнопки мыши переключается между используемыми методами обновления рендеринга.

Существует несколько методов обновления. Это будет зависеть от установленной вами аппаратной настройки относительно того, каким будет точный внешний вид артефактов анимации. Вы будете искать небольшие подергивания в движении полос

Заметка. У вас может быть отключена синхронизация дисплея или аппаратное ускорение, которое повлияет на качество всех методов синхронизации. У устройств нижнего уровня также могут быть проблемы с анимацией

  • Таймер Использует setTimeout для анимации. Время: 1000/60
  • Лучшее качество RAF, Использует requestAnimationFrame для анимации
  • Двойной таймер, использует два таймера, один называется каждые 1000/60 очистки, а другой для рендеринга. Размер мерцания, который это произведет, зависит от установленного оборудования. Но это плохо и типично для рендеринга решений, которые включают в себя множество событий, таких как мышь, таймеры и многое другое.
  • RAF с временной анимацией, использует requestAnimationFrame, но анимирует с использованием времени, прошедшего через кадр. Этот метод очень распространен в анимации. Я считаю, что это недостаток, но я оставляю это до зрителя
  • Таймер с временной анимацией. Как "RAF с временной анимацией" и используется в этом случае для преодоления пропущенного кадра в методе "Таймер". Снова я думаю, что это суки, но игровое сообщество ругается, это лучший способ использовать, когда у вас нет доступа к обновлению дисплея

/** SimpleFullCanvasMouse.js begin **/

var backBuff;
var bctx;
const STRIPE_WIDTH = 250;
var textWidth;
const helpText = "Click mouse to change render update method.";
var onResize = function(){
    if(backBuff === undefined){
        backBuff = document.createElement("canvas")    ;
        bctx = backBuff.getContext("2d");
        
    }
    
    backBuff.width = canvas.width;
    backBuff.height = canvas.height;
    bctx.fillStyle = "White"
    bctx.fillRect(0,0,w,h);
    bctx.fillStyle = "Black";
    for(var i = 0;  i < w; i += STRIPE_WIDTH){
        bctx.fillRect(i,0,STRIPE_WIDTH/2,h)   ;
        
    }
    ctx.font = "20px arial";
    ctx.textAlign = "center";
    ctx.font = "20px arial";
    textWidth = ctx.measureText(helpText).width;
    
};
var tick = 0;
var displayMethod = 0;
var methods = "Timer,RAF Best Quality,Dual Timers,RAF with timed animation,Timer with timed animation".split(",");

function display(timeAdvance){  // put code in here

    tick += timeAdvance;
    tick %= w;


    ctx.drawImage(backBuff,tick-w,0);
    ctx.drawImage(backBuff,tick,0);
    if(textWidth !== undefined){
        ctx.fillStyle = "rgba(255,255,255,0.7)";
        ctx.fillRect(w /2 - textWidth/2, 0,textWidth,40);
        ctx.fillStyle = "black";
        ctx.fillText(helpText,w/2, 14);
        ctx.fillText("Display method : " + methods[displayMethod],w/2, 34);
    }
    if(mouse.buttonRaw&1){
        displayMethod += 1;
        displayMethod %= methods.length;
        mouse.buttonRaw = 0;
        lastTime = null;
        tick = 0;
    }
}








//==================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable 
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
const U = undefined;const RESIZE_DEBOUNCE_TIME = 100;
var w,h,cw,ch,canvas,ctx,mouse,createCanvas,resizeCanvas,setGlobals,globalTime=0,resizeCount = 0; 
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () { var c,cs; cs = (c = document.createElement("canvas")).style; cs.position = "absolute"; cs.top = cs.left = "0px"; cs.zIndex = 1000; document.body.appendChild(c); return c;}
resizeCanvas = function () {
    if (canvas === U) { canvas = createCanvas(); } canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx = canvas.getContext("2d"); 
    if (typeof setGlobals === "function") { setGlobals(); } if (typeof onResize === "function"){ resizeCount += 1; setTimeout(debounceResize,RESIZE_DEBOUNCE_TIME);}
}
function debounceResize(){ resizeCount -= 1; if(resizeCount <= 0){ onResize();}}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; mouse.updateBounds(); }
mouse = (function(){
    function preventDefault(e) { e.preventDefault(); }
    var mouse = {
        x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0, over : false, bm : [1, 2, 4, 6, 5, 3], 
        active : false,bounds : null, crashRecover : null, mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
    };
    var m = mouse;
    function mouseMove(e) {
        var t = e.type;
        m.x = e.clientX - m.bounds.left; m.y = e.clientY - m.bounds.top;
        m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
        if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }  
        else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
        else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
        else if (t === "mouseover") { m.over = true; }
        else if (t === "mousewheel") { m.w = e.wheelDelta; }
        else if (t === "DOMMouseScroll") { m.w = -e.detail; }
        if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
        if((m.buttonRaw & 2) && m.crashRecover !== null){ if(typeof m.crashRecover === "function"){ setTimeout(m.crashRecover,0);}}        
        e.preventDefault();
    }
    m.updateBounds = function(){
        if(m.active){
            m.bounds = m.element.getBoundingClientRect();
        }
        
    }
    m.addCallback = function (callback) {
        if (typeof callback === "function") {
            if (m.callbacks === U) { m.callbacks = [callback]; }
            else { m.callbacks.push(callback); }
        } else { throw new TypeError("mouse.addCallback argument must be a function"); }
    }
    m.start = function (element, blockContextMenu) {
        if (m.element !== U) { m.removeMouse(); }        
        m.element = element === U ? document : element;
        m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
        m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
        if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
        m.active = true;
        m.updateBounds();
    }
    m.remove = function () {
        if (m.element !== U) {
            m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
            if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
            m.element = m.callbacks = m.contextMenuBlocked = U;
            m.active = false;
        }
    }
    return mouse;
})();


resizeCanvas(); 
mouse.start(canvas,true); 
onResize()
var lastTime = null;
window.addEventListener("resize",resizeCanvas); 
function clearCTX(){
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h); // though not needed this is here to be fair across methods and demonstrat flicker
}


function dualUpdate(){
    setTimeout(updateMethods[displayMethod],1000/60);
    clearCTX();
    setTimeout(function(){
        display(10);
    },0);    
}
function timerUpdate(){
    timer = performance.now();
    if(!lastTime){
        lastTime = timer;
    }
    var time = (timer-lastTime) / (1000/60);
    lastTime = timer;    
    setTimeout(updateMethods[displayMethod],1000/60);
    clearCTX();
    display(10*time);
}
function updateRAF(){ 
    clearCTX();
    requestAnimationFrame(updateMethods[displayMethod]);
    display(10);  
}
function updateRAFTimer(timer){ // Main update loop
    clearCTX();
    requestAnimationFrame(updateMethods[displayMethod]);
    if(!timer){
        timer = 0;
    }
    if(!lastTime){
        lastTime = timer;
    }
    var time = (timer-lastTime) / (1000/60);
    display(10 * time);  
    lastTime = timer;
}

displayMethod = 1;
var updateMethods = [timerUpdate,updateRAF,dualUpdate,updateRAFTimer,timerUpdate]
updateMethods[displayMethod]();

/** SimpleFullCanvasMouse.js end **/