Viewports and the scaling & positioning of the canvas

Having just spent a couple of hours poring over my code for scaling & positioning the game canvas I thought I’d share the experience.

Despite having made several HTML5 games over the last couple of years I’ve never experienced any real issues with scaling or positioning. Just recently however I’ve started to notice inconsistencies between browsers and OS versions.

In some cases the canvas would scale perfectly but not position properly. In other cases the canvas would not scale properly yet position well in the centre of the screen.

My criteria is simple – I want the games to be played in any orientation and to fit consistently in the centre of the screen.
Where full screen is available in either width (landscape orientation games) or height (portrait) I will also aim for that.

It’s 90% there. On Galaxy S2+ and iPhone4+ portrait games load and scale to fit fine in portrait and landscape games load fine and scale in landscape.
The problem arises on iOS when the games are loaded in the wrong orientation.

e.g. if the game is a portrait game and it loads up in portrait mode then it scales to fit just fine, as you would expect. (illustration 1)

ipadscale-portraitload

illustration 1 – game loads in portrait mode

But if the player then decides to play it in landscape it scales fine but doesn’t centre correctly. Just sits to the left of centre. (illustration 2)

illustration 2 - game loaded in portrait and rotated to landscape

illustration 2 – game loaded in portrait and rotated to landscape – note white space where HTML BODY is simply untouched.

If the game loads up in landscape then it centers beautifully and is playable (illustration 3)

iPad HTML5

illustration 3 – game loaded whilst tablet in landscape mode

If the player then rotates to portrait the game scales just fine on iPhone to fit the screen but on iPad it sits rather awkwardly to the right of the screen with about a third of the content out of sight. (illustration 4)

ipadscale-landscapeload-rotate

illustration 4 – loaded in landscape and rotated to portrait

This has an impact on my regional touchEvent code that I use for driving menus and other in-game features.

I no longer present a banner message instructing the player to rotate. It seems clunky and my clients hate it.

Of course it’s far too easy to suggest that as long as the games load up in their preferred orientation then surely I’ll be OK.
The reality is it needs to work and work properly.

So what’s my code doing?

Well surprisingly little.
I certainly don’t sniff the device. It’s 2013. So I don’t actually know I’m dealing with iOS any more than I know it’s a desktop or mobile browser.

The first thing I do is record the screen’s physical width less any scrollbars.

var w = window.innerWidth;
var h = window.innerHeight;

I also record the game’s intended canvas dimensions.

var rw =320;
var rh = 480;

Then to accurately scale the canvas I use the Math.min() method to first determine how best to scale the canvas to preserve the aspect ratio.

multiplier = Math.min((h / rh), (w / rw));
var actualCanvasWidth = rw * multiplier;
var actualCanvasHeight = rh * multiplier;

Fairly simple stuff there just to keep the aspect ratio of the game’s canvas intact and as previously stipulated aim for full screen in the preferred orientation.

To actually position the canvas on the screen I use CSS.

So first I’ve already got a handle on the canvas and stored it within my global namespace (g):

HTML

<body>
<div id="game">
<canvas class="canvas"></canvas>
</div>
</body>

JavaScript

g.gamecanvas = document.getElementById('game');

CSS

.canvas { width: 100%; height: 100%; }

I then make sure that the canvas parent is displaying as a block level element, assign a width to it and use the margin property to centre it. The canvas fits its parent to the pixel.

g.gamecanvas.style.display = "block";
g.gamecanvas.style.width = actualCanvasWidth + "px";
g.gamecanvas.style.height = actualCanvasHeight + "px";
g.gamecanvas.style.margin = "0px auto 0px auto";

My scaling function is called every time the onresize() or onorientationchange() events are fired. It is also called at the very top of the code before the assets are loaded.

So when we tie all this together we get a perfect presentation the first time the game is loaded.
The canvas scales to fit the orientation and centers perfectly in the centre of the screen.

So why on earth do I get that “dead” area when the tablet is rotated from portrait to landscape? (illustration 2 above)
Why does the canvas shift over to the right when the tablet is rotated from landscape to portrait? (illustration 4 above)
There has to be something else to look at.

The answer lay in the META description for the VIEWPORT.

Here’s what I used to present in the index.html of every game:

<meta name="viewport" content="width=320, height=440, user-scalable=no, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />

What a mess. I’d never touched this in almost two years.
Understanding the viewport is essential to understanding mobile presentation. Generally speaking the rule is if you don’t understand how it works don’t use it. The results can be undesirable.

Rather than go to the trouble of fully understanding what the viewport definitions are and do I decided to look at what I actually want to achieve. The answer here is very simple – a) stretch content to screen dimensions, b) suppress ability to pinch / zoom.

There is no need to specify the physical width or height in pixels of the device screen. There is also no need to restrict user scaling with the user-scalable attribute. Finally there is no need to set a maximum-scale, which I must admit I found surprising.

The only code I need here is:

<meta name="viewport" content="width=device-width, initial-scale=1">

The scaling of the canvas and positioning of the parent element within the document body are now perfect. No more “dead” regions and no more strange horizontal positioning.

There appears to be more sympathy for my lousy meta code within Android browsers than there is with iOS and Safari.

2 Comments

    Mark

    Hi Jon.
    I do all of my CSS with the game DIV and simply scale the CANVAS element to fit at 100% height and width.
    I got better results when I delayed any resizing / positioning code by a second after the orientationchange event had fired.

    window.setTimeout(function() {
    .. do stuff
    }, 1000);

    Sorry this response is late. Let me know if you are still scratching your head!

    Jon

    Hi Mark,
    I’m doing some testing with your code here and I’m not getting good results. Maybe I’m doing something wrong!

    -CSS: I’m assuming you are not using any code for the “game” div.

    -I’m having trouble with the orientation change events, specially with the Iphone. It loads the game OK but once you start changing the orientation it goes completely off and finishes showing only the top left portion of the canvas in the bottom right area of the screen. Do you use some kind of timer in order to regulate the orientation change event calls? I thought it could be some kind of overreaction to too many orientation changes but I really don’t know.

    Thank you for posting this ! Helps a lot.

Leave a reply

%d bloggers like this: