117 votes

Pourquoi requestAnimationFrame est-il préférable à setInterval ou setTimeout ?

Pourquoi devrais-je utiliser requestAnimationFrame plutôt que setTimeout ou setInterval ?

Cette question auto-répondante est un exemple de documentation.

0 votes

Vous pouvez également vérifier ce

148voto

user3877726 Points 11

Animation de haute qualité.

La réponse à cette question est très simple. requestAnimationFrame produit des animations de meilleure qualité en éliminant complètement le scintillement et le cisaillement qui peuvent se produire lors de l'utilisation de la fonction setTimeout ou setInterval et réduire ou supprimer complètement les sauts de trame.

Cisaillement

se produit lorsqu'un nouveau tampon de toile est présenté au tampon d'affichage au milieu du balayage d'affichage, ce qui entraîne une ligne de cisaillement causée par les positions d'animation non concordantes.

Scintillement

se produit lorsque le tampon du canevas est présenté au tampon d'affichage avant que le canevas n'ait été entièrement rendu.

Saut de cadre

est causée par le fait que le temps entre les images de rendu n'est pas précisément synchronisé avec le matériel d'affichage. Chaque fois qu'une image est sautée, elle produit une animation incohérente. (Il existe des méthodes pour réduire ce phénomène, mais personnellement, je pense que ces méthodes produisent des résultats plus mauvais). Comme la plupart des appareils utilisent 60 images par seconde (ou un multiple de ce nombre), il en résulte une nouvelle image toutes les 16.666...ms et les temporisateurs... setTimeout et setInterval utilisent des valeurs entières, elles ne peuvent jamais correspondre parfaitement au taux de rafraîchissement (en arrondissant à 17ms si vous avez interval = 1000/60 )


Une démo vaut mille mots.

Mise à jour La réponse à la question la boucle requestAnimationFrame ne corrige pas les fps montre comment le temps de trame de setTimeout est incohérent et le compare à requestAnimationFrame.

La démo montre une animation simple (des bandes se déplaçant sur l'écran). Un clic sur le bouton de la souris permet de passer d'une méthode de mise à jour du rendu à l'autre.

Plusieurs méthodes de mise à jour sont utilisées. L'apparence exacte des artefacts d'animation dépendra de la configuration matérielle que vous utilisez. Vous recherchez de petites variations dans le mouvement des bandes.

Note. Il se peut que la synchronisation de l'affichage soit désactivée, ou que l'accélération matérielle soit désactivée, ce qui affectera la qualité de toutes les méthodes de synchronisation. Les appareils bas de gamme peuvent également avoir des problèmes avec l'animation.

  • Minuterie Utilise setTimeout pour animer. Le temps est de 1000/60

  • RAF Best Quality Utilise requestAnimationFrame pour l'animation.

  • Minuteurs doubles , Utilise deux timers, un appelé tous les 1000/60 effacements et un autre pour le rendu.

    MISE À JOUR OCTOBRE 2019 Des changements ont été apportés à la présentation du contenu des minuteries. Pour montrer que setInterval n'est pas correctement synchronisé avec le rafraîchissement de l'écran. J'ai modifié l'exemple de la double minuterie pour montrer que l'utilisation de plus d'une minuterie est nécessaire. setInterval peut néanmoins provoquer un scintillement important L'ampleur du scintillement produit dépend de la configuration du matériel.

  • RAF avec animation chronométrée Utilise requestAnimationFrame mais anime en utilisant le temps écoulé de la trame. Cette technique est très courante dans les animations. Je pense qu'elle est imparfaite, mais je laisse le soin au spectateur d'en juger.

  • Minuterie avec animation programmée . Comme "RAF with timed animation" et est utilisé dans ce cas pour surmonter le saut d'image vu dans la méthode "Timer". Encore une fois, je pense que c'est nul, mais la communauté des joueurs affirme que c'est la meilleure méthode à utiliser lorsque vous n'avez pas accès au rafraîchissement de l'écran.

    / 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(","); var dualTimersActive = false; var hdl1, hdl2

    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;
        if(dualTimersActive) {
             dualTimersActive = false;
             clearInterval(hdl1);
             clearInterval(hdl2);
             updateMethods[displayMethod]()             
        }
    }

    }

    //================================================================================================== // 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(){ if(!dualTimersActive) { dualTimersActive = true; hdl1 = setInterval( clearCTX, 1000/60); hdl2 = setInterval(() => display(10), 1000/60); } } 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 /

2 votes

Bonne réponse. Il faut noter que le navigateur fait automatiquement un double tampon pour vous, donc il n'y a jamais de risque de cisaillement avec la toile normale, de même pour le scintillement.

4 votes

@Cristy Oui DOM double buffers. Cependant, une fonction (y compris le délai d'attente et l'intervalle) à la sortie (exécution idle, c'est-à-dire pile d'appels vide) a rendu les tampons immédiatement présentés à la RAM d'affichage, ce qui pourrait être un balayage intermédiaire. Cela provoquera un cisaillement de l'animation si vous effectuez le rendu avec une seule fonction, et un scintillement si vous effectuez le rendu avec deux fonctions, qui sortent toutes deux de l'idle. requestAnimationFrames est spécial, à la sortie, les tampons arrière sont maintenus jusqu'à Vsync (aucun pixel ne se déplace vers l'écran). Cela empêche le cisaillement et le scintillement.

0 votes

Je vois du sheer dans tous les exemples. Les lignes ressemblent à des escaliers. L'animation est fluide et il ne semble pas y avoir d'images sautées, cependant. Y a-t-il un moyen de corriger cela sans réduire la fréquence d'images ? (Chrome v84, vieux Lenovo merdique avec une carte graphique encore pire).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X