79

I'm using Three.js with the WebGL renderer to make a game which fullscreens when a play link is clicked. For animation, I use requestAnimationFrame.

I initiate it like this:

self.animate = function()
{
    self.camera.lookAt(self.scene.position);

    self.renderer.render(self.scene, self.camera);

    if (self.willAnimate)
        window.requestAnimationFrame(self.animate, self.renderer.domElement);
}

self.startAnimating = function()
{
    self.willAnimate = true;
    self.animate();
}

self.stopAnimating = function()
{
    self.willAnimate = false;
}

When I want to, I call the startAnimating method, and yes, it does work as intended. But, when I call the stopAnimating function, things break! There are no reported errors, though...

The setup is basically like this:

  • There is a play link on the page
  • Once the user clicks the link, a renderer's domElement should fullscreen, and it does
  • The startAnimating method is called and the renderer starts rendering stuff
  • Once escape is clicked, I register an fullscreenchange event and execute the stopAnimating method
  • The page tries to exit fullscreen, it does, but the entire document is completely blank

I'm pretty sure my other code is OK, and that I'm somehow stopping requestAnimationFrame in a wrong way. My explanation probably sucked, so I uploaded the code to my website, you can see it happening here: http://banehq.com/Placeholdername/main.html.

Here is the version where I don't try to call the animation methods, and fullscreening in and out works: http://banehq.com/Correct/Placeholdername/main.html.

Once play is clicked the first time, the game initializes and it's start method is executed. Once the fullscreen exits, the game's stop method is executed. Every other time that play has been clicked, the game only executes it's start method, because there is no need for it to be initialized again.

Here's how it looks:

var playLinkHasBeenClicked = function()
{
    if (!started)
    {
        started = true;

        game = new Game(container); //"container" is an empty div
    }

    game.start();
}

And here's how the start and stop methods look like:

self.start = function()
{
    self.container.appendChild(game.renderer.domElement); //Add the renderer's domElement to an empty div
    THREEx.FullScreen.request(self.container);  //Request fullscreen on the div
    self.renderer.setSize(screen.width, screen.height); //Adjust screensize

    self.startAnimating();
}

self.stop = function()
{
    self.container.removeChild(game.renderer.domElement); //Remove the renderer from the div
    self.renderer.setSize(0, 0); //I guess this isn't needed, but welp

    self.stopAnimating();
}

The only difference between this and the working version is that startAnimating and stopAnimating method calls in start and stop methods are commented out.

corazza
  • 31,222
  • 37
  • 115
  • 186

6 Answers6

144

One way to start/stop is like this

var requestId;

function loop(time) {
    requestId = undefined;

    ...
    // do stuff
    ...

    start();
}

function start() {
    if (!requestId) {
       requestId = window.requestAnimationFrame(loop);
    }
}

function stop() {
    if (requestId) {
       window.cancelAnimationFrame(requestId);
       requestId = undefined;
    }
}

Working example:

const timeElem = document.querySelector("#time");
var requestId;

function loop(time) {
    requestId = undefined;
    
    doStuff(time)
    start();
}

function start() {
    if (!requestId) {
       requestId = window.requestAnimationFrame(loop);
    }
}

function stop() {
    if (requestId) {
       window.cancelAnimationFrame(requestId);
       requestId = undefined;
    }
}

function doStuff(time) {
  timeElem.textContent = (time * 0.001).toFixed(2);
}
  

document.querySelector("#start").addEventListener('click', function() {
  start();
});

document.querySelector("#stop").addEventListener('click', function() {
  stop();
});
<button id="start">start</button>
<button id="stop">stop</button>
<div id="time"></div>
gman
  • 100,619
  • 31
  • 269
  • 393
  • 7
    should this be `cancelAnimationFrame` instead of `cancelRequestAnimationFrame`? – leech Mar 07 '14 at 05:28
  • 8
    changed, although in my defense it was originally called `cancelRequestAnimationFrame`. You can verify this by opening a console in Chrome and typing `webkitCancel` and you'll see `webkitCancelRequestAnimationFrame` still in there as of Chrome 33 – gman Mar 07 '14 at 16:03
  • Documentation for both functions can be found on MDN at: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame (thanks for the awesome info!) –  Apr 30 '16 at 14:47
  • 1
    On thing I found out, you cannot stop the loop from inside the function: Here a [demo](http://jsbin.com/seqixag/1/edit?html,js,console,output). Only from outside :-) – Legends Feb 22 '17 at 18:59
  • 3
    Actually you can cancel the loop from inside the function, like this: `setTimeout(function(){window.cancelAnimationFrame(requestId);},0);` – Legends Feb 22 '17 at 19:19
  • It only seems to loop once? – Howard Nov 30 '17 at 22:41
  • This might be a simple question but I was just wondering, where did the `time` variable of `loop` function came from if it wasn't included on the function call of `requestAnimationFrame`? – Gellie Ann Jul 25 '18 at 23:34
  • see [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) – gman Jul 26 '18 at 03:58
  • My 2 cents here @Legends - You can stop the loop from the inside, but you must first queue up the next frame. if the first thing you do inside RAF is queue another RAF, now you can cancel and it'll work - Just move your next RAF to the top of the function – Patrick Oct 29 '19 at 17:52
23

Stopping is as simple as not calling requestAnimationFrame anymore, and restarting is to call it it again. ex)

        var pause = false;
        function loop(){
                //... your stuff;
                if(pause) return;
                window.requestionAnimationFrame(loop);
        }
       loop(); //to start it off
       pause = true; //to stop it
       loop(); //to restart it
user3363398
  • 1,110
  • 1
  • 9
  • 5
  • Yes. That's actually the simplest way. – Roko C. Buljan Aug 11 '16 at 17:01
  • 5
    But it's not efficient as you would have to perform a check inside each animation frame that way. I found cancelAnimationFrame() performing more efficient on heavy animation loops. – Sergey Lukin Aug 30 '16 at 07:54
  • I don't get why this solution by user3363398 is less efficient than gman's. In gmans solution there's instead a function call inside the loop and that function call in turn then performs the check. Isn't that heavier? One check versus function call + check. – Oortone Jun 04 '21 at 07:37
5
var myAnim //your requestId
function anim()
       {
       //bla bla bla
       //it's important to update the requestId each time you're calling reuestAnimationFrame
       myAnim=requestAnimationFrame(anim)
       }

Let's start it

myAnim=requestAnimationFrame(anim)

Let's stop it

//the cancelation uses the last requestId
cancelAnimationFrame(myAnim)

Reference

PYK
  • 3,674
  • 29
  • 17
2

I played around with the tutorial of a 2D Breakout Game where they also used requestAnimationFrame and I stopped it with a simple return. The return statement ends function execution if the value of return is omitted.

if(!lives) {
    alert("GAME OVER");
    return;
}

// looping the draw()
requestAnimationFrame(draw);
SaikaVA
  • 127
  • 10
  • This would fall into the "just don't initiate the next cycle" approach to stopping a animation frame loop that @user3363398 proposed, the alternative approach being to use `cancelAnimationFrame()`. – Magnus Lind Oxlund Nov 18 '18 at 15:13
1

I would suggest having a look at the requestAnimationFrame polyfill gibhub page. There are discussions about how this is implemented.

Neil
  • 7,861
  • 4
  • 53
  • 74
1

So, after doing some more testing, I've found out that it was, indeed, my other code that posed a problem, not the animation stopping (it was a simple recursion after all). The problem was in dynamically adding and removing the renderer's domElement from the page. After I've stopped doing that, for there was really no reason to do so, and included it once where the initialization was happening, everything started working fine.

corazza
  • 31,222
  • 37
  • 115
  • 186