Modernize your HTML5 Canvas game

Part 1: Hardware scaling and CSS3

By David Rousset

Modern browsers like Internet Explorer 10 are implementing stable versions of some interesting HTML5 features, including offline application programming interfaces (API), drag and drop, and file API. These features are bringing us to a new era of web applications and fresh, quickly emerging gaming scenarios.

In this two-part article, I’ll show how I’ve used these new features to modernize my last HTML5 game, HTML5 Platformer. Hopefully you’ll get some great new ideas for your own games!

Part 1: Hardware scaling and CSS3 (this article)
Part 2: Offline , file, and drag-and-drop API (next article)

Note: The URL demo is at the end of this article. Feel free to play using your favorite browser, and check out the IE10 gameplay video. The source code will be available for download in Part 2.

Scaling across devices

If you’re building an HTML5 game, you’re probably interested in the cross-platform nature of this standard programming language. But compatibility with a broad variety of devices means you have to take into account a huge number of resolutions. Compared to SVG, Canvas—at first—seems ill-prepared to handle this.

However, with a casual game based on sprites, there is a simple solution to implement. David Catuhe has done a great job of describing this on his blog, Unleash the power of HTML 5 Canvas for gaming – Part 1 (see the section called “Using the hardware scaling feature” for specifics).

The idea is as simple as it is smart. You’re working inside a canvas at a fixed, predictable resolution, and you’re stretching it to the current displayed resolution using the canvas.style properties.

Step 1: Stretch

In the case of my little HTML5 Platformer game, the assets and level logic have been set to 800x480. So if I want to fill a 1080p screen or a 1366x768 tablet, I need to build higher resolution assets to match those specs.

Before building all those assets, I can try a scaling operation—along with proper anti-aliasing—to increase image quality. Let’s give it a try.

The scaling operation simply requires this code:

window.addEventListener("resize", OnResizeCalled, false);
function OnResizeCalled() {
    canvas.style.width = window.innerWidth + 'px';
    canvas.style.height = window.innerHeight + 'px';
}

And that’s it!

With hardware-accelerated browsers, this operation will be done by your GPU at no cost. Anti-aliasing can even be enabled. That’s why David Catuhe thought it was worth mentioning it in his Canvas performance article.

This trick is not specific to HTML5, by the way. Most modern console games are not internally computed in 720p or 1080p; almost all of them render in lower resolutions (like 1024x600) and let the GPU handle the scaling/anti-aliasing process. In most cases, the method described here can help you boost the number of frames per second (FPS).

But this action, by itself, raises a ratio problem. Indeed, when the canvas had a fixed size of 800x480, the ratio was controlled. Now, I get this strange result when resizing the browser window:

The game is still playable, but also clearly far from optimal.

Step 2: Control your ratio

The idea here is to control how the screen is filled when the browser window is resized. Rather than stretching anything further, I’m going to add some empty space on the right if the window is too large, or at the bottom if the window is too high. Here’s that code:

var gameWidth = window.innerWidth;
var gameHeight = window.innerHeight;
var scaleToFitX = gameWidth / 800;
var scaleToFitY = gameHeight / 480;
var currentScreenRatio = gameWidth / gameHeight;
var optimalRatio = Math.min(scaleToFitX, scaleToFitY);
if (currentScreenRatio >= 1.77 && currentScreenRatio <= 1.79) {
    canvas.style.width = gameWidth + "px";
    canvas.style.height = gameHeight + "px";
}
else {
    canvas.style.width = 800 * optimalRatio + "px";
    canvas.style.height = 480 * optimalRatio + "px";
}

The “if” statement creates an exception: If you hit F11 in your browser to switch to full-screen viewing, and you’ve got a 16:9 screen (like my 1920x1080 Sony VAIO Z screen or the 1366x768 Samsung BUILD tablet), the game will be completely stretched. This experience was quite awesome.

Without this exception, here is the type of output you’ll see:

Notice the black areas below and to the right of the game, controlling the ratio.

It would be even better if the game was centered, giving it a widescreen movie effect, right? Let’s do it.

Step 3: Center the game with CSS3 Grid Layout

Centering an HTML element can sometimes be painful. There are several ways to do it—and there are plenty of resources on the web to help.

I like to use a new specification named CSS Grid Layout (currently only supported by IE10), which is the base of our Windows Store app layout in Windows 8.

Centering an element with CSS Grid Layout is straightforward:

  • Switch the display of the container to display:grid.
  • Define 1 column and 1 row.
  • Center the inner element with the column-align and row-align properties.

Here’s the CSS used in my case:

.canvasHolder {
    width: 100%;
    height: 100%;
    display: -ms-grid;
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr;
}
#platformerCanvas {
    -ms-grid-column: 1;
    -ms-grid-row: 1;
    -ms-grid-column-align: center;
    -ms-grid-row-align: center;
}

You’ll notice the prefix is “-ms” for IE10. Mozilla has recently annonced they will also support the CSS Grid Layout specification for Firefox in 2012, which is excellent news. In the meantime, this centering trick only works with IE10. Here’s what it looks like:

IE10 windows will display vertical or horizontal black bars, similar to what you might see on a television screen. In other browsers, the results will match those of Step 2, because the CSS3 Grid Layout specification will be ignored.

Using smooth animations

Now that we’re handling multiple resolutions with an easy scaling operation, it would be nice to play a smooth transition when the user is resizing the window. It would also be great to play a cool animation while each level loads. For that, we’re going to use the CSS3 Transitions and CSS3 3D Transforms tools. On most platforms, hardware acceleration is provided by the GPU.

Animating every change made to Canvas style properties

CSS3 Transitions is easy to use and produces smooth, efficient animations. To discover how to use them, you can read my colleague’s excellent article, Introduction to CSS3 Transitions, or play around on our Internet Explorer test-drive site, Hands On: transitions.

I’ve set up a global transition for all my canvas properties, thanks to these rules:

#platformerCanvas {
    -ms-grid-column: 1;
    -ms-grid-row: 1;
    -ms-grid-column-align: center;
    -ms-grid-row-align: center;
    -ms-transition-property: all;
    -ms-transition-duration: 1s;
    -ms-transition-timing-function: ease;
}

The canvas with the “platformerCanvas” ID will now automatically reflect any change made to its style properties in a one-second animation generated by an easing function.

Thanks to this new rule, resizing the browser window reduces/increases the size of the canvas with a smooth animation. I love the effect it produces—all with only 3 lines of CSS.

Note: I also add the different prefixes (-moz, -webkit, and -o for Mozilla, WebKit, and Opera, respectively) for compatibility. You’ll want to remember to do the same.

Building a cool animation between each level

Now I’d like to use CSS3 3D Transforms to temporarily make the canvas disappear. This will be done with an animated 90-degree rotation on the Y axis. I’ll load the next level once the animation rotates 90 degrees and comes back to its initial position (rotated zero degrees). To better understand the effect, you can play with our Hands On: 3D Transforms on the Internet Explorer test-drive site:

We’re also going to play with the scale and rotateY properties to build a fun animation. For that, I’m adding two CSS rules and targeting two classes:

.moveRotation
{
    -ms-transform: perspective(500px) rotateY(-90deg) scale(0.1);
    -webkit-transform: perspective(500px) scale(0);
    -moz-transform: perspective(500px) rotateY(-90deg) scale(0.1);
}
.initialRotation
{
    -ms-transform: perspective(500px) rotateY(0deg) scale(1);
    -webkit-transform: perspective(500px) scale(1);
    -moz-transform: perspective(500px) rotateY(0deg) scale(1);
}

First, my canvas has the initialRotation class set:

<canvas id="platformerCanvas" width="800" height="480" class="initialRotation"></canvas>

Now the idea is to wait until the player beats the current level. Once that’s done, we’re changing the class of the canvas from initialRotation to moveRotation. This will automatically trigger the CSS3 Transitions we set before to generate the animation.

In order to know when the animation is finished, an event is raised. It’s named differently in each browser. Here is the code I use for registering the event and targeting IE10, Firefox, WebKit, and Opera:

// Registering to the various browsers vendors transition end event
PlatformerGame.prototype.registerTransitionEndEvents = function () {
    // IE10, Firefox, Chrome & Safari, Opera
    this.platformerGameStage.canvas.addEventListener("MSTransitionEnd", onTransitionEnd(this));
    this.platformerGameStage.canvas.addEventListener("transitionend", onTransitionEnd(this));
    this.platformerGameStage.canvas.addEventListener("webkitTransitionEnd", onTransitionEnd(this));
    this.platformerGameStage.canvas.addEventListener("OTransitionEnd", onTransitionEnd(this));
};

And here is the code that will be called back:

// Function called when the transition has ended
// We're then loading the next level
function onTransitionEnd(instance) {
    return function () {
        if (instance.loadNextLevel === true) {
            instance.LoadNextLevel();
        }
    }
};

At last, here is the code for my game that set the moveRotation class on the canvas:

// Perform the appropriate action to advance the game and
// to get the player back to playing.
PlatformerGame.prototype.HandleInput = function () {
    if (!this.wasContinuePressed && this.continuePressed) {
        if (!this.level.Hero.IsAlive) {
            this.level.StartNewLife();
        }
        else if (this.level.TimeRemaining == 0) {
            if (this.level.ReachedExit) {
                // If CSS3 Transitions is supported
                // We're using smooth & nice effects between each levels
                if (Modernizr.csstransitions) {
                    this.loadNextLevel = true;
                    // Setting the moveRotation class will trigger the css transition
                    this.platformerGameStage.canvas.className = "moveRotation";
                }
                // If CSS3 Transition is not supported, we're jumping directly
                // to the next level
                else {
                    this.LoadNextLevel();
                }
            }
            else
                this.ReloadCurrentLevel();
        }
        this.platformerGameStage.removeChild(statusBitmap);
        overlayDisplayed = false;
    }
    this.wasContinuePressed = this.continuePressed;
};

Notice I’m using Modernizr to do a feature detection of CSS Transitions. We’re finally setting back the initialRotation class inside the LoadNextLevel function:

// Loading the next level 
PlatformerGame.prototype.LoadNextLevel = function () {
    this.loadNextLevel = false;
    // Setting back the initialRotation class will trigger the transition
    this.platformerGameStage.canvas.className = "initialRotation";
    // ... loadNextLevel logic stuff...
};

Note: You may have noticed that I’m not setting the same transformation (and animation) for WebKit as for IE10 and Firefox in my previous CSS block. This is because, in my case, Chrome won’t behave the same way as IE10 and Firefox 11. I’ve set up a simple reproduction case on jsFiddle here: https://jsfiddle.net/5H8wg/2/.

Demo video and URL

Here is a short video demonstrating the IE10 features covered in this article:

You can also play with this demo in IE10 or your favorite browser here: Modern HTML5 Platformer.

In the next part of this article, I’ll show how to implement offline API to make my game work without network connections, and how drag-and-drop API can create another cool feature.

About the Author

David Rousset

David Rousset is a Developer Evangelist at Microsoft, specializing in HTML5 and web development. Read his blog on MSDN or follow him @davrous on Twitter.