HTML5 Game Scaling

by William Malone
HTML5 Game Resize Example

HTML5 games have the advantage of portability across many different devices. This poses a challenge of how to fit the game on the various different aspect ratios of tablets, phones and desktop browsers. This article will describe scaling techniques, including the use of a game safe area, to best fit an HTML5 game regardless of where it is played.

Resize Event

To be able to dynamically update the dimensions of our game to fit the current device or browser window via JavaScript we need to know when the viewport dimensions change. Fortunately we have the resize event.

The resize event fires when viewport size changes. This can happen when:

  • The browser window size is changed
  • The orientation of a device is changed
  • Browser chrome appears or disappears on a device (e.g. the url bar appears on an iPhone)

To listen for the resize event simply add an event listener to the window like so:

window.addEventListener("resize", resizeGame);

CSS Approach

We can use CSS to simply stretch the game to the size of the viewport.

<article id="gameContainer"></article>
.gameContainer {
    width: 100%;
    height: 100%;
}

As you can see in the following example this approach can cause the game to be stretched when the viewport and game aspect ratios are different.

HTML5 Game Example

We can remove the stretching by changing the CSS height from 100% to auto.

.gameContainer {
    width: 100%;
    height: auto;
}

Or we can use JavaScript to do the same thing:

var game = {
    element: document.getElementById("gameContainer"),
    width: 1280,
    height: 800
  },
  
  resizeGame = function () {
		
    // Get the dimensions of the viewport
    var viewport = {
      width: window.innerWidth,
      height: window.innerHeight
    };
  
    // Determine game size
    newGameWidth = viewport.width;
    newGameHeight = newGameWidth * game.height / game.width;
  
    // Resize game
    game.element.style.width = newGameWidth + "px";
    game.element.style.height = newGameHeight + "px";
  };
  
window.addEventListener("resize", resizeGame);
resizeGame();

Takes a lot more work to do the same thing in JavaScript, but it turns out we will need to use JavaScript. Let's take a moment to see what is going on: First we create a game object that has the game's container element and its dimensions and a resizeGame function which is called on the window's resize event and one time in the beginning.

Each time the viewport's dimensions change the resizeGame function will determine it's new size using the window's innerWidth and innerHeight properties. If the game is inside an element that doesn't fill the entire viewport (like the examples in the article) we could alternatively use that element's offsetWidth and offsetHeight properties.

In this new example the game is no longer stretched. It will respect the game aspect ratio regardless of size of the viewport. This approach however can cause unwanted cropping at certain aspect ratios. In our example game we have can no longer see the bottom of our house and our game character.

HTML5 Game Example

JavaScript Approach

If we want the game to fit inside the viewport without stretching we will need to scale it differently depending on the game and viewport's aspect ratios.

To do this we will need JavaScript, hence why we laid the groundwork in the above approach that could have been done with only CSS. Let's update the part of the code where we determine the new game dimensions to handle two scenarios.

// Determine game size
if (game.height / game.width > viewport.height / viewport.width) {
  newGameHeight = viewport.height;
  newGameWidth = newGameHeight * game.width / game.height;
} else {
  newGameWidth = viewport.width;
  newGameHeight = newGameWidth * game.height / game.width;
}

If the aspect ratio of the game is larger viewport's aspect ratio then we size the game so the heights are the same. In other words, in the case of our landscape game, if the game's aspect ratio is taller than the viewport's then we can fit the game inside the viewport via the height.

If it's wider then we need to scale via the width instead.

This will result in a game that always fits inside our viewport without the cropping and stretching from the previous approaches.

HTML5 Game Example

Let's balance it a bit more by centering the game with a little letterboxing.

newGameX = (viewport.width - newGameWidth) / 2;
newGameY = (viewport.height - newGameHeight) / 2;

// Center the game by setting the padding of the game
game.element.style.padding = newGameY + "px " + newGameX + "px";

We make the mattes on each side of the game by setting the padding of the game element by half of the difference in game and viewport size.

HTML5 Game Example

Give it a try in the demo by changing your browser size or rotating your mobile device.

Simple Scale Demo

Game Safe Area

The above approach will allow our game to fit on any size viewport. On devices with dimensions different from the game we will get letterboxing. Although unavoidable at extreme viewport aspect ratios we can minimize the mattes by implementing a game safe area. This will allow us to crop portions of the game that are not integral to gameplay thus allowing the game to fit across devices. Carefully choosing the game and game safe aspect ratios will allow us to fill the screen with no mattes on all modern mobile devices in our chosen orientation.

Before implementing let's first define the game safe area for our example game:

Next we need to once again update the part of the code where we determine the new game dimensions. This time we need to handle four scenarios which we will be calling: A, B, C and D.

// Determine game size
if (game.height / game.width > viewport.height / viewport.width) {
  if (game.safeHeight / game.width > viewport.height / viewport.width) {
    // A
    newGameHeight = viewport.height * game.height / game.safeHeight;
    newGameWidth = newGameHeight * game.width / game.height;
  } else {
    // B
    newGameWidth = viewport.width;
    newGameHeight = newGameWidth * game.height / game.width;
  }
} else {
  if (game.height / game.safeWidth > viewport.height / viewport.width) {
    // C
    newGameHeight = viewport.height;
    newGameWidth = newGameHeight * game.width / game.height;
  } else {
    // D
    newGameWidth = viewport.width * game.width / game.safeWidth;
    newGameHeight = newGameWidth * game.height / game.width;
  }
}

The first two scenarios A and B handle when the game aspect ratio is taller than the viewport aspect ratio. A also has the requirement that the aspect ratio of safe height to game width is taller than the viewport.

B has the requirement that the aspect ratio of safe height to game width is also shorter than the viewport.

The last two scenarios C and D handle when the game aspect ratio is wider than the viewport aspect ratio. C also has the requirement that the aspect ratio of safe width to game height is wider than the viewport.

D has the requirement that the aspect ratio of safe width to game height is also less wide than the viewport.

This is an animation showing how the game reacts to the changing viewport:

And this is our example game in all four scenarios including break points between each scenario:

And finally here is our example game with a game safe area implemented.

HTML5 Game Example

Give it a try in the demo by changing your browser size or rotating your mobile device.

Game Safe Demo

Code

Here is the code to scale our example game with a game safe area. The full source code and assets can be downloaded from GitHub.

<article id="gameContainer"></article>
var game = {
    element: document.getElementById("gameContainer"),
    width: 1280,
    height: 800,
    safeWidth: 1024,
    safeHeight: 720
  },
  
  resizeGame = function () {
	
    var viewport, newGameWidth, newGameHeight, newGameX, newGameY;
					
    // Get the dimensions of the viewport
    viewport = {
      width: window.innerWidth,
      height: window.innerHeight
    };

    // Determine game size
    if (game.height / game.width > viewport.height / viewport.width) {
      if (game.safeHeight / game.width > viewport.height / viewport.width) {
          // A
          newGameHeight = viewport.height * game.height / game.safeHeight;
          newGameWidth = newGameHeight * game.width / game.height;
      } else {
          // B
          newGameWidth = viewport.width;
          newGameHeight = newGameWidth * game.height / game.width;
      }
    } else {
      if (game.height / game.safeWidth > viewport.height / viewport.width) {
        // C
        newGameHeight = viewport.height;
        newGameWidth = newGameHeight * game.width / game.height;
      } else {
        // D
        newGameWidth = viewport.width * game.width / game.safeWidth;
        newGameHeight = newGameWidth * game.height / game.width;
      }
    }
  
    game.element.style.width = newGameWidth + "px";
    game.element.style.height = newGameHeight + "px";
			
    newGameX = (viewport.width - newGameWidth) / 2;
    newGameY = (viewport.height - newGameHeight) / 2;
			
    // Set the new padding of the game so it will be centered
    game.element.style.margin = newGameY + "px " + newGameX + "px";
  };

window.addEventListener("resize", resizeGame);
resizeGame();

Share This Article

Related Articles