IE9 - Debugging a Canvas Game

A few weeks ago, I discussed one compatibility issue we’d found when running a new HTML5 game. The game’s developers quickly fixed their site to return a proper character set declaration and we were able to get the game running in IE9. However, after playing the game for about 5 seconds, it would invariably hang the browser tab.

What went wrong?

The problem is that the site was relying upon a problematic pattern in the logic that would copy an image to the game board’s <canvas>. The code was similar to this:

 // WARNING: Dangerous pattern. Do not copy!!
imgToDraw = document.getElementById("IMGSpriteToDraw");
if (imgToDraw.height > 0)
{ 
  try
  { 
    canvasGame.drawImage( /* arguments elided */); 
  } 
  catch(e){ SetTimerForRetry(); } 
} 
else
{ 
  SetTimerForRetry(); 
}

Attempting to copy an image to a canvas using drawImage() will fail if that image hasn’t yet finished downloading. Hence, the intent of this code’s imgToDraw.height check is to ensure that the IMG tag containing the desired sprite has completed its download before trying to draw to the canvas. If it hasn’t, the code block calls a function which uses setTimeout() to try again in a short time.

We quickly tracked down the hang to the fact that the SetTimerForRetry() function was being called thousands of times; because it contained no logic to coalesce the timers into a animation frame or “beat”, instead of trying, say, 60 times per second, the code was trying thousands of times, each of which resulted in a failure that queued up another retry.

After we pointed this out, the game developers developed a simple paint queuing system that prevented a large stack of redundant timers from being created, and the game stopped hanging. However, the game board still wasn’t being painted properly, and the root cause of the problem explained why the stacking of timers occurred at all. As we debugged the game further, we realized that the problem was that imgToDraw.height was always 0. A quick look with the F12 Developer Tools showed that the image was successfully downloading and being added to the DOM. The HTML tab yielded a clue however:

 <img id="IMGSpriteToDraw" src="sprite.png" style="display: none" />

When the style attribute was removed, the game began running correctly, albeit with a minor display glitch because a duplicate copy of the sprite appeared next to the canvas.

So, why does the style matter?

The developer was using a standard image tag with display: none styling to allow the browser to download his sprites without needing to write any extra JavaScript. He didn’t want the sprites to appear on the canvas, however, so he styled them to not display in the page.

The problem: In IE, when an image has the style display: none, the image.height and image.width properties will always return 0. The reason is that these values are returned from the layout subsystem, but display: none images do not participate in layout. Had the developer styled these images using visibility: hidden instead of display: none, IE will still calculate a layout size for the element and the code would have worked as expected.  Alternatively, the code could use the naturalHeight or naturalWidth properties, which always return the intrinsic size of the image, even when it is not displayed.

Another alternative would be to forgo the direct size check and instead use one or more onload handlers to detect when content is fully available. Since this sites images were all directly in the markup, an onload handler on the body would work too: IE will fire the onload for the body only after all the images have finished their downloads.

After tweaking the code to correctly detect when the image was available, the game now runs correctly in IE—it makes great use of IE9’s hardware-accelerated HTML5 features, and it’s a lot of fun too!

-Eric