Cacomania: Speeding up canvas drawing by scaling it with CSS3

Cacomania

Speeding up canvas drawing by scaling it with CSS3

Guido Krömer - 9. December 2012 - Tags: , ,

Playing around with HTML5 Canvas is a lot of fun, but drawing is really slow and reminds me on GDI+ or Java 2D. The larger the resolution of the drawing canvas is the worse the performance becomes. Doing a little game using the whole browser given size on a Full HD monitor can even slow down on Chromium with a relatively fast CPU (AMD A8-3870). My idea was drawing the canvas in a smaller resolution, the half of the available for example, and scale it up. This is the same way a game console like XBOX 360 and PS3 rendering Full HD or even HD Ready which sometimes is not HD Ready indeed. Scaling elements can be done using CSS3 transform.

Take a look at the example below, the css property transform: scale scales an html element by the given factor. By default the element gets scaled by their midpoint. This can be bypassed by defining another transform-origin which is 50%, 50% by default. The reset-origin, in the example, class does this by setting the origin to the upper left (transform-origin: 0% 0%;).

Implementation

Since I am a Fan of reusable code, even in examples/tutorials I wrote a GameManager class which controls the update and drawing logic. The GameManager manages a set of GameObject's. The GameObject class itself is very simple and just a skeleton for subclasses inheriting from this class.

Besides some trivial methods for adding, removing and iterating over GameObject subclasses there are some methods, which controls the flow of the game. Methods like draw() calls the GameObject draw(ctx) method for example. timerStart() and timerEnd() calculating the delta time needed for rendering a frame, the delta get passed to the GameObjects update(delta) method.

var timerStart = function() {
  var date = new Date();
  delta = date.getTime() - this.lastTimeStamp;
  delta *= 0.01;
};

var timerEnd = function() {
  this.lastTimeStamp = new Date().getTime();
};

The whole call flow is in the animloop function, which is called by the requestAnimFrame function.

(function animloop(){
  requestAnimFrame(animloop);
  timerStart();
  update();
  draw();
  timerEnd();
})();

If you want to know more about the benefits using the request animation frame instead of an interval you should read this blog post by Paul Irish.

Here is the whole GameManager class:

Testing

For testing my theory I made two examples, the first one is a simple parallax scroller the other one just draws some random circles on the screen. Canvas upscaling with css3 transform demo.
This is the random circle demo running on Mac OSX. Canvas upscaling with css3 transform demo, parallax scrollingand no scale up.
This is the parallax scrolling demo on Linux with no scaling through CSS3. Canvas upscaling with css3 transform demo, parallax scrolling with scale up.
This is the same demo scaled up by factor three.

For determining the FPS there is a Hud GameObject which calculates this. This Hud is used in Both demos.

function Hud() { 
  var tmpFps = 0;
  var lastSecond = -1;
  var fps = 1;

  this.update = function () {
    var seconds = new Date().getSeconds();
    if (lastSecond != seconds) {
      lastSecond = seconds;
      fps = tmpFps;
      tmpFps = 1;
    }
    else {
      tmpFps++;
    }
  }

  this.draw = function (ctx) { 
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, 60, 40);
    ctx.fillStyle = 'black';
    ctx.fillText('FPS: ' + fps, 10, 10);
    ctx.fillText(this.gameManager.width + 'x' + this.gameManager.height, 10, 20);
    ctx.fillText(window.innerWidth + 'x' + window.innerHeight, 10, 30);
  };
}
Hud.prototype = new GameObject ();

The screenshot below is a comparison between the Hud output and the Chromium Task Manager which displays the FPS, too.FPS Hud vs Chromium Task Manager

The balls demo is extremely simple, as you can see at the screen shot provided above, the source code for this demo can be found in the Gist.

The parallax scrolling demo is a little bit more complex, three different images get scrolled horizontally in different speeds for faking some kind of depth. The images used are very small with a resolution of 290x185 pixel so that they have to be scaled up during rendering the canvas element, too.

function Layer(src, depth) { 
  ...
  this.draw = function (ctx) { 
    ctx.drawImage(this.image, 0, 0, this.image.width, this.image.height, this.x - this.gameManager.width, 0, this.gameManager.width, this.gameManager.height);
    ctx.drawImage(this.image, 0, 0, this.image.width, this.image.height, this.x, 0, this.gameManager.width, this.gameManager.height);
    ctx.drawImage(this.image, 0, 0, this.image.width, this.image.height, this.x + this.gameManager.width, 0, this.gameManager.width, this.gameManager.height);
  };
}
Layer.prototype = new GameObject ();

Since the scaling is done by the canvas element and by CSS3 the output should not differs strongly by using different scale factors if the virtually screen (the canvas element) is not smaller than 290x185 pixels. If you want to get the three layer graphics I have drawn, here they are:

Since I am not a designer or pixel artist, please apologize the poor quality :) .

Here is the whole scrolling demo code including the usage of the GameManager. The defined scale factor is two, in the code.

Results

Enough gray theory, let's talk about the results which can be very different dependent on the used browser. I ran the balls demo on my Mac Book Air with Firefox and Safari both are hardware accelerated. Since the Safari did not benefit from the CSS3 scaling the Firefox made a significant improvement when rendering with a scale factor by two.

Balls Demo
Browser/ResolutionScaleFPS
Safari 160
6.0.2 260
BALLS DEMO 360
(1920x934) 460
Mac Book Air 13" 560
Firefox 132
17 269
BALLS DEMO 366
(1920x933) 465
Mac Book Air 13" 569

The scrolling demo had been tested on my desktop which runs Linux with an AMD A8-3870 APU. The Firefox runs fine hardware accelerated, but I got some graphic glitches running Opera with acceleration enabled. The Chromium seems to does not support acceleration in Version 23. Since the Demo is more complex than the balls one the benefits by scaling the canvas up are more significant. The demo runs at poorly 11FPS on the Opera without scaling an at up to 34FPS with scaling by factor three. The Firefox reached 60FPS when scaling by factor five in comparison with 29 FPS when no CSS scaling was used.

Canvas scaled rendering performance on Firefox, Opera and Chromium

Parallax Scrolling Demo
Browser/ResolutionScaleFPS
Chromium 119
23 226
(Software) 331
(1920x969) 434
535
1036
Firefox 129
17 255
(Hardware) 359
(1920x914) 458
560
1060
Opera 111
12.11 224
(Software) 334
(1881x938) 435
537
1034

I think scaling the canvas up by CSS3 has a big benefit on the common browsers since smoother gaming on a larger area makes more fun. That's it folks.