web-dev-qa-db-fra.com

Contrôle de fps avec requestAnimationFrame?

Il semble que requestAnimationFrame soit le moyen de facto d’animer les choses maintenant. Cela a plutôt bien fonctionné pour moi la plupart du temps, mais pour l’instant, j’essaie de faire des animations sur toile et je me demandais: est-il possible de s’assurer qu’il fonctionne à une certaine vitesse par seconde? Je comprends que l'objectif de rAF est de créer des animations toujours fluides et que je pourrais courir le risque de rendre mon animation saccadée, mais pour l'instant, elle semble fonctionner à des vitesses radicalement différentes, assez arbitrairement, et je me demande s'il existe un moyen de lutter contre ce problème. cela en quelque sorte.

Je voudrais utiliser setInterval mais je veux les optimisations offertes par rAF (en particulier l'arrêt automatique lorsque l'onglet est actif).

Au cas où quelqu'un voudrait regarder mon code, c'est à peu près:

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();
    }
}

Où Node.drawFlash () est juste un code qui détermine le rayon basé sur une variable de compteur, puis trace un cercle.

107
robert.vinluan

Comment limiter requestAnimationFrame à une fréquence d'images spécifique

Limitation de la démo à 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Cette méthode teste le temps écoulé depuis l'exécution de la dernière boucle de trame.

Votre code de dessin ne s'exécute que lorsque l'intervalle FPS spécifié est écoulé.

La première partie du code définit certaines variables utilisées pour calculer le temps écoulé.

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();
}

Et ce code est la boucle réelle requestAnimationFrame qui tire à votre FPS spécifié.

// 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

    }
}
151
markE

Mise à jour 2016/6

Le problème qui limite le taux de trame est que l'écran a un taux de mise à jour constant, généralement 60 FPS.

Si nous voulons 24 images par seconde, nous n’obtiendrons jamais les 24 images par seconde à l’écran. Nous pouvons la chronométrer en tant que telle, mais ne pas l’afficher, car le moniteur ne peut afficher que les images synchronisées à 15, 30 ou 60 images par seconde. ).

Cependant, à des fins de synchronisation, nous pouvons calculer et mettre à jour lorsque cela est possible.

Vous pouvez construire toute la logique de contrôle de la cadence en encapsulant les calculs et les rappels dans un objet:

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)
    }
}

Ajoutez ensuite un code de contrôleur et de configuration:

// 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;
    }
};

Usage

Cela devient très simple - maintenant, tout ce que nous avons à faire est de créer une instance en définissant la fonction de rappel et la cadence souhaitée, comme ceci:

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

Puis démarrez (ce qui pourrait être le comportement par défaut si vous le souhaitez):

fc.start();

Ça y est, toute la logique est gérée en interne.

Démo

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>

Ancienne réponse

Le but principal de requestAnimationFrame est de synchroniser les mises à jour avec le taux de rafraîchissement du moniteur. Pour ce faire, vous devrez animer le FPS du moniteur ou un facteur de celui-ci (c'est-à-dire 60, 30, 15 FPS pour un taux de rafraîchissement typique à 60 Hz).

Si vous voulez un FPS plus arbitraire, inutile d'utiliser rAF, car la cadence ne correspondra jamais à la fréquence de mise à jour du moniteur (juste une image ici et là), ce qui ne peut tout simplement pas vous donner une animation fluide (comme avec tous les re-timings des images). ) et vous pouvez aussi bien utiliser setTimeout ou setInterval à la place.

Il s’agit également d’un problème bien connu dans l’industrie de la vidéo professionnelle lorsque vous souhaitez lire une vidéo à un autre FPS que le périphérique sur lequel elle est actualisée. De nombreuses techniques ont été utilisées, telles que le mélange d'images et la recomposition complexe de la reconstruction d'images intermédiaires en fonction de vecteurs de mouvement, mais avec la technique de canevas, ces techniques ne sont pas disponibles et le résultat sera toujours une vidéo saccadée.

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

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

    ... code for frame here
}

La raison pour laquelle nous plaçons setTimeout en premier (et pourquoi certains placent rAF en premier lorsqu'un remplissage multiple est utilisé) est que ceci sera plus précis car la setTimeout mettra en file d'attente un événement immédiatement au début de la boucle. Ainsi, quel que soit le temps d'utilisation du code restant (à condition qu'il ne dépasse pas l'intervalle de temporisation), le prochain appel aura lieu. l'intervalle qu'il représente (pour rAF pur, cela n'est pas essentiel car rAF essaiera de passer à la trame suivante dans tous les cas).

Il convient également de noter que le placer en premier risque également de provoquer une accumulation d'appels comme avec setInterval. setInterval peut être légèrement plus précis pour cet usage.

Et vous pouvez utiliser setInterval à la place à l'extérieur de la boucle pour faire la même chose.

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

function loop() {

    ... code for frame here
}

Et pour arrêter la boucle:

clearInterval(rememberMe);

Afin de réduire la fréquence d'images lorsque l'onglet devient flou, vous pouvez ajouter un facteur comme celui-ci:

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
}

De cette façon, vous pouvez réduire le FPS à 1/4, etc.

37
user1693593

Je suggère d'envelopper votre appel à requestAnimationFrame dans un setTimeout. Si vous appelez setTimeout à partir de la fonction à partir de laquelle vous avez demandé le cadre d'animation, vous annulez l'objectif de requestAnimationFrame. Mais si vous appelez requestAnimationFrame depuis setTimeout, tout se passe bien:

var fps = 25
function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
27
Luke Taylor

Ce sont toutes de bonnes idées en théorie, jusqu'à ce que vous approfondissiez. Le problème est que vous ne pouvez pas étrangler une RAF sans la désynchroniser, ce qui l'empêcherait d'exister. Vous la laissez donc tourner à toute vitesse, et mettre à jour vos données dans une boucle séparée , ou même un fil séparé!

Oui je l'ai dit Vous pouvez faire du JavaScript multi-thread dans le navigateur!

Je connais deux méthodes qui fonctionnent extrêmement bien sans jank, utilisant beaucoup moins de jus et produisant moins de chaleur. Le résultat final est le timing précis à l’échelle humaine et l’efficacité de la machine.

Toutes mes excuses si cela est un peu verbeux, mais voici ...


Méthode 1: Mettre à jour les données via setInterval et les graphiques via RAF.

Utilisez un setInterval distinct pour mettre à jour les valeurs de translation et de rotation, la physique, les collisions, etc. Conservez ces valeurs dans un objet pour chaque élément animé. Attribuez la chaîne de transformation à une variable de l'objet chaque setInterval 'frame'. Gardez ces objets dans un tableau. Définissez votre intervalle en fps de votre choix en ms: ms = (1000/fps). Cela permet de conserver une horloge constante qui permet le même nombre d’images par seconde sur tous les appareils, quelle que soit la vitesse de rotation. N'attribuez pas ici les transformations aux éléments!

Dans une boucle requestAnimationFrame, parcourez votre tableau avec une boucle old-school for loop. N'utilisez pas les formulaires les plus récents ici, ils sont lents!

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

Dans votre fonction rafUpdate, obtenez la chaîne de transformation de votre objet js dans le tableau et son identifiant d'éléments. Vous devriez déjà avoir vos éléments 'Sprite' attachés à une variable ou facilement accessibles par d'autres moyens afin de ne pas perdre de temps à les récupérer dans la RAF. Les garder dans un objet nommé d'après leur identifiant HTML fonctionne plutôt bien. Configurez cette partie avant même qu’elle n’entre dans votre SI ou votre RAF.

Utilisez la RAF pour mettre à jour vos transformations uniquement , utilisez uniquement des transformations 3D (même pour 2d) et définissez css "will-change: transform;" sur des éléments qui vont changer. Ainsi, vos transformations sont synchronisées avec le taux de rafraîchissement natif autant que possible, lancez le GPU et indique au navigateur où se concentrer le plus.

Donc, vous devriez avoir quelque chose comme ce pseudo-code ...

// 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

Ainsi, vos mises à jour des objets de données et des chaînes de transformation sont synchronisées au taux de trame souhaité dans le SI, ainsi que les affectations de transformation réelles dans la RAF synchronisées avec le taux de rafraîchissement du processeur graphique. Ainsi, les mises à jour graphiques réelles ne concernent que la RAF, mais les modifications apportées aux données et la construction de la chaîne de transformation se trouvent dans le SI. Il n'y a donc pas de jankies, mais le "temps" s'écoule à la cadence souhaitée.


Couler:

[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]

Méthode 2. Mettez le SI dans un Web-worker. Celui-ci est sans faille et lisse!

Identique à la méthode 1, mais mettez le SI dans Web-worker. Il fonctionnera alors sur un thread totalement séparé, laissant la page ne traiter que de la RAF et de l'interface utilisateur. Passez le tableau Sprite dans les deux sens en tant qu '"objet transférable". C'est buko rapide. Il ne faut pas du temps pour cloner ou sérialiser, mais ce n'est pas comme passer par référence car la référence de l'autre côté est détruite. Vous devrez donc faire passer les deux côtés de l'autre côté et ne les mettre à jour que s'ils sont présents. comme de passer une note avec votre petite amie au lycée.

Un seul peut lire et écrire à la fois. C'est bien tant qu'ils vérifient si ce n'est pas indéfini pour éviter une erreur. La RAF est rapide et la relance immédiatement, puis passe en revue une série de trames GPU en vérifiant si elle a déjà été renvoyée. Le système d’information dans le travailleur Web utilise le tableau Sprite la plupart du temps, met à jour les données de position, de mouvement et de physique, crée la nouvelle chaîne de transformation, puis la renvoie à la RAF dans la page.

C’est le moyen le plus rapide que je connaisse pour animer des éléments via un script. Les deux fonctions seront exécutées en tant que deux programmes distincts, sur deux threads distincts, tirant parti des processeurs multicœurs de la même manière qu'un script js unique. Animation javascript multithread.

Et cela se fera sans à-coups, mais au taux de trame spécifié, avec très peu de divergence.


Résultat:

L'une ou l'autre de ces deux méthodes garantira que votre script s'exécutera à la même vitesse sur n'importe quel PC, téléphone, tablette, etc. (dans les limites des capacités de l'appareil et du navigateur, bien entendu).

10
jdmayfield

Comment limiter facilement un FPS spécifique:

// 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);

Source: Explication détaillée des boucles et du minutage de jeux JavaScript par Isaac Sukin

3
Rustem Kakimov

Saut requestAnimationFrame cause pas lisse (désiré) animation à la vitesse personnalisée.

// 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>

Code original de @tavnab.

2
befzz

Je le fais toujours de cette manière très simple sans me gêner avec les horodatages:

var 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
fucntion frame() {
  if (frameCount == eachNthFrame) {
    frameCount = 0;
    animate();
  }
  frameCount++;
  requestAnimationFrame(frame);
}
1
Samer Alkhabbaz

Voici une bonne explication que j'ai trouvée: CreativeJS.com , pour envelopper un appel setTimeou) dans la fonction transmise à requestAnimationFrame. Mon problème avec un "simple" requestionAnimationFrame serait, "et si je seulement veux le faire animer trois fois par seconde?" Même avec requestAnimationFrame (par opposition à setTimeout), cela signifie que encore gaspille (une certaine quantité) "d'énergie" (ce qui signifie que le code du navigateur est en train de faire quelque chose et peut-être ralentir le système ) 60 ou 120 ou autant de fois par seconde, par opposition à seulement deux ou trois fois par seconde (comme vous le souhaitez).

La plupart du temps, je lance mon navigateur avec JavaScript de manière intensive uniquement pour cette raison. Mais j'utilise Yosemite 10.10.3 et je pense que cela pose un problème de minuterie - du moins sur mon ancien système (relativement vieux - signifiant 2011).

0
Jim Witte