Zanon Zanon - 2 months ago 9
AngularJS Question

Why this canvas animation hangs when loaded for a second time with ngRoute?

Problem



I'm using a space invaders canvas animation as my 404 not found page. It is working pretty fine when the user is redirected to it for the first time, but the animation hangs if the user is redirected a second time.




Demo



I've created a Plunker example to show the issue. The page starts at the
/
location loading the home.html. If you click on "Redirect to /404", the location will be changed to
/404
and 404.html will be loaded. The game animation will be running without problems.

However, if you go back to
/
(clicking in the link) and back again to
/404
, the animation will be hanged. Also, the frames-per-second will drop from 60fps to 10fps.




Code



This demo was built with AngularJS and I switch pages using ngRoute like the following: (nothing special in this)

$routeProvider.
when('/', {
templateUrl: 'partials/home.html',
controller: 'HomeCtrl'
}).
when('/404', {
templateUrl: 'partials/404.html',
controller: '404Ctrl'
});


The home.html contains just instructions and 404.html contains the canvas element:

<canvas id="game-canvas" width="600" height="400"></canvas>


The Angular Controller starts the animation calling
initInvaders404();
. This function is defined inside handle-game.js. The game.js completes the canvas animation code that is executed as soon as the 404.html page is loaded.




What I've already tried



First guess: the issue may be related with the canvas being started using its ID. When I recreate the 404.html, the animation is attached with the previous ID but not with the newer ID. To test this, I've created another Plunker and tested another animation (blue rectangle moving) with a similar approach. It is working fine, so maybe that's not the problem.

Second guess: the animation code that is attached to the canvas is executed only once, that's why it does not work a second time. To test this, I've placed the game.js and handle-game.js code inside the controller. Unfortunately, same issue. Plunker

Third guess: ngRoute is messing with the canvas element. To test this, I've placed the canvas element out of the ngView and controlled the ngShow to hide/show the canvas. With this approach, the problem was solved. The
initInvaders404();
can be called multiple times, but it will always focus in the same element and it will never be recreated. Plunker.




Question



Why this animation can't be executed two times?

Note1: I do not want a workaround solution (I've already found one in my third guess). I would like to keep the canvas element inside the 404.html file.

Note2: this question is not a "please, debug my code" (since game.js have 2000 lines of code). What I want to understand is if there is a canvas property or ngRoute behavior that may cause this.

Answer

I've found that it is possible to execute the animation for a second time, but only if the first animation has finished. In this case, after the game finished with an onWin() or onLoose() event.

When ngRoute changes the view, the canvas element is destroyed, but the game loop still runs in background (as I've confirmed using some console.log()). When I try to attach the same game loop to the same canvas id, it hangs.

The workaround used was to catch the $locationChangeStart event and stop the animation before the canvas destruction. When I need to execute the game for a second time, it will run: Plunker.

Getting the $locationChangeStart event:

ctrls.controller(
  'MyRootController',
  function($location, $rootScope) {
    $rootScope.$on("$locationChangeStart", function(event, next, current) { 
        var contains404 = current.includes("404");

        if (contains404) { // if current is 404, next will be something else
          stopInvaders404(); // code to stop the game loop before destroying the canvas
        }
    });
  }
);

However, the simplest solution for a third-party animation, which does not involve changing the animation code (as I did with the stopInvaders404()) is to place the canvas out of the ngView and control its visibility: Plunker.