
HTML5-enabled browsers and the HTML5 platform for Windows 8 Metro are today becoming serious candidates for developing modern games.
Thanks to canvas, you have access to a hardware-accelerated surface on which you can display the content of your game and with the help of some tricks and tricks you can achieve excellent rendering performance up to 60 frames per second. Such continuity is really important in games, because the smoother the game (animation), the better the player feels.
')
The purpose of this article is to give you some tips on how to get the most out of HTML5 Canvas. The article consists of two main parts [you read the first].
David Rousset will soon publish the second part.
In the article, I will show the key ideas on the same example - this is the effect of a 2D tunnel that I wrote for the
Coding4Fun session at TechDays 2012 in France.
I was inspired to write this effect by my code for Commodore AMIGA, which I wrote when I was a young author of the demoscene in the distant 80s :). Today it uses only
canvas and
Javascript , although the original code was based only on 68000 assembler:
Example on JSFiddle (note that the texture is wired into the code).
The full code is available for download here:
http://www.catuhe.com/msdn/canvas/tunnel.zip (
copy to J.Narod ).
The purpose of this article is to explain not how the tunnel is programmed, but how you can take the existing code and optimize it in order to achieve the best real-time performance.
Using hidden canvas to read image data
The first thing I want to talk about is how you can use canvas to optimize the reading of image data. Indeed, in almost every game you need graphics for sprites or background images. Canvas has a very convenient method for drawing images:
drawImage . This function can be used to display a sprite to the canvas element, since you can specify the source and destination areas for drawing.
But sometimes this is not enough. For example, when you want to apply some effects to the original image. Or when the original image is not just a picture, but a more complex resource for your game (for example, the map from which you need to read data).
In such cases, you need access to internal image data. But the
image tag does not provide a way to read the contents. This is where canvas comes to the rescue!
In essence, every time you need to read the contents of an image, you can use an invisible (non-displayable) canvas. The key idea is to load the image, and when it is loaded, you just have to map it to the canvas element that is not inserted into the DOM. You now have access to [copied] pixels of the original image through the corresponding pixels of the canvas (which is very simple).
The code for this technique is as follows (used in the 2D tunnel effect to read the tunnel texture):
var loadTexture = function (name, then) { var texture = new Image(); var textureData; var textureWidth; var textureHeight; var result = {}; // on load texture.addEventListener('load', function () { var textureCanvas = document.createElement('canvas'); // off-screen canvas // Setting the canvas to right size textureCanvas.width = this.width; // <-- "this" is the image textureCanvas.height = this.height; result.width = this.width; result.height = this.height; var textureContext = textureCanvas.getContext('2d'); textureContext.drawImage(this, 0, 0); result.data = textureContext.getImageData(0, 0, this.width, this.height).data; then(); }, false); // Loading texture.src = name; return result; };
When using this code, keep in mind that the texture is loaded asynchronously, so you need to pass a function through the
then parameter to continue working your code:
// Texture var texture = loadTexture("soft.png", function () { // Launching the render QueueNewFrame(); });
Using the hardware scaling feature
Modern browsers and Windows 8 support canvas
hardware acceleration . This means, in particular, that you can use the
GPU to scale content on canvas.
In the case of the 2D tunnel effect, the algorithm requires processing each pixel of the canvas. For example, for canvas with a size of 1024x768, it is necessary to process 786432 pixels. To ensure the continuity of the display, it must be done 60 times per second, which corresponds to the processing of
471,85920 pixels per second!
Obviously, any solution that helps you
reduce the number of pixels will result in a noticeable performance
improvement .
I repeat, canvas provides such a tool! The following code shows how to use hardware acceleration to scale the internal canvas buffer to the external size of the DOM object:
// Setting hardware scaling canvas.width = 300; canvas.style.width = window.innerWidth + 'px'; canvas.height = 200; canvas.style.height = window.innerHeight + 'px';
Note the difference between the size of the DOM object (
canvas.style.width and
canvas.style.height ) and the size of the canvas working
buffer (
canvas.width and
canvas.height ).
If there is a difference between these two sizes, the capabilities of iron are used to scale the working buffer — in our case, this is just a great function: we can work at a lower resolution and allow the GPU to scale the result to fill the DOM object (using the excellent free blur filter to smooth the result ).
In this example, the rendering is in the 300x200 area, and the GPU scales to fit your window.
This feature is widely supported by all modern browsers, so you can count on it.
Rendering cycle optimization
When you develop a game, you probably should have a rendering cycle in which you draw all the components of the game (background, sprites, glasses, etc.). This loop is a bottleneck in your code and should be as optimized as possible to make sure your game runs quickly and smoothly.
RequestAnimationFrame
One of the interesting features that came with HTML5 is the
window.requestAnimationFrame function. Instead of using
window.setInterval to create a timer that calls your draw cycle every (1000/16) milliseconds (to achieve the cherished 60fps), you can delegate this responsibility to the browser using
requestAnimationFrame . Calling this method says that you want the browser to call your code as soon as it is possible to update the graphical representation.
The browser will turn on your request inside its drawing schedule and synchronize you with its drawing and animation code (CSS, transitions, etc.). This solution is also interesting due to the fact that your code will not be called when the window is not visible (minimized, completely blocked, etc.).
This can help with performance, since the browser can optimize parallel rendering (for example, if your rendering cycle is too slow) and still produce smooth animations.
The code is pretty obvious (note the use of browser prefixes):
var intervalID = -1; var QueueNewFrame = function () { if (window.requestAnimationFrame) window.requestAnimationFrame(renderingLoop); else if (window.msRequestAnimationFrame) window.msRequestAnimationFrame(renderingLoop); else if (window.webkitRequestAnimationFrame) window.webkitRequestAnimationFrame(renderingLoop); else if (window.mozRequestAnimationFrame) window.mozRequestAnimationFrame(renderingLoop); else if (window.oRequestAnimationFrame) window.oRequestAnimationFrame(renderingLoop); else { QueueNewFrame = function () { }; intervalID = window.setInterval(renderingLoop, 16.7); } };
To use this function, you just need to call it at the end of your draw cycle to subscribe to the next frame:
var renderingLoop = function () { ... QueueNewFrame(); };
DOM Access (Document Object Model)
To optimize your rendering cycle, you should use at least one golden rule: DO NOT TAKE A DOM. Although modern browsers are specifically optimized in this place, reading the properties of DOM objects is still too slow for a fast rendering cycle.
For example, in my code, I used the Internet Explorer 10 profiler (
available in the F12 developer tools ) and the results are obvious:

As you can see, accessing the width and height of the canvas takes a very long time in the rendering loop!
The original code looked like this:
var renderingLoop = function () { for (var y = -canvas.height / 2; y < canvas.height / 2; y++) { for (var x = -canvas.width / 2; x < canvas.width / 2; x++) { ... } } };
You can replace the canvas.width and canvas.height properties with two variables with the correct values set in advance:
var renderingLoop = function () { var index = 0; for (var y = -<b>canvasHeight</b> / 2; y < <b>canvasHeight</b> / 2; y++) { for (var x = -<b>canvasWidth</b> / 2; x < <b>canvasWidth</b> / 2; x++) { ... } } };
Simple, isn't it? It may not be very easy to understand, but believe me, it is worth a try!
Preliminary calculations
According to the profiler, the
Math.atan2 function is somewhat slow. In fact, this operation is not embedded inside the CPU, so the JavaScript runtime must do some operations to calculate the result.
[Lane. Although, from a technical point of view, it can be assumed that a specific implementation of the JS-runtime can rely on the hardware instruction fpatan, no one guarantees a priori. The ECMAScript5 specification for mathematical functions suggests that they are (obviously) approximations and recommends using the algorithms of the Sun Microsystems mathematical library (http://www.netlib.org/fdlibm). In any case, whatever the implementation of the atan2 function, it does not make it elementary and fast.]
In general, if you can arrange a preliminary calculation for some long-running code, it is always a good idea. Here, before starting my rendering cycle, I calculate the values for
Math.atan2 :
// precompute arctangent var atans = []; var index = 0; for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) { for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) { atans[index++] = Math.atan2(y, x) / Math.PI; } }
The
atans array
can then be used inside the draw loop for an obvious increase in performance.
Avoid using Math.round, Math.floor and parseInt
The point about using
parseInt makes sense in our case:

When you work with canvas, you need to use integer coordinates (x and y) to point to the pixels. However, all your calculations are made using floating point numbers and sooner or later you will need to convert them to integers.
JavaScript provides
Math.round ,
Math.floor, or even
parseInt functions for converting numbers to integers. But these functions do some extra work (in particular, they check ranges or check that values really are numbers; parseInt generally converts its parameter to a string in the first place!). So within my drawing cycle, I need a faster way to convert numbers.
Recalling my old assembler code, I decided to use a small trick: instead of using
parseInt, it is enough just to shift the number to the right by 0. The runtime will move the floating-point number from the corresponding register to the integer one and apply a hardware conversion. Shifting the number to the right by 0 will leave the number unchanged and return you an integer value.
The source code was:
u = parseInt((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u)
The new code looks like this:
u = ((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u) >> 0
Of course, this solution requires you to be sure of the correctness of the transmitted number :)
Final result
Applying all the described optimizations results in the following report:

As you can see, now the code looks well optimized using only key functions.
We started with this original, non-optimized tunnel:
JSFiddle exampleAnd they came to this result after optimization:
JSFiddle example[lane: these screenshots are made on the translator’s computer, on which fps does not reach the cherished 60fps, but is spinning pretty close to this - about 50fps in IE10.]We can estimate the contribution of each optimization by the following diagram showing the frame rate on my computer:

Moving on
With these key points in mind, you are now ready to develop fast and smooth games for modern browsers and Windows 8!