user3011902
user3011902

Reputation:

Scaling a Javascript Canvas Game Properly

I am attempting to scale my canvas game dynamically based on screensize. I understand how to resize the canvas based on screensize, but I want to resize the contents as well. Basically I want the game to look the same on every device. he problem I am having currently is that when people with a 4k screen play the game, they can easily see the whole map. And when someone has a really small screen, they can barely see anything. Is there an easy and efficient way to do it? I have tried to do the following:

var maxWidth = 1920;
var maxHeight = 1080;

...

function resize() {
    c.width = screenWidth = window.innerWidth;
    c.height = screenHeight = window.innerHeight;
    widthToHeight = screenWidth / screenHeight; // ASPECT RATIO

    var scalerAmnt = (maxWidth + maxHeight )
            / (screenWidth + screenHeight);
    context.scale(scalerAmnt, scalerAmnt); 
}

If I scale the canvas like this, it has undesired results. It goes off center, and is far too large. As you can see I have set a max width and height. Basically I am trying to make the game look the same on every device as it does on a machine with that resolution.

LIVE DEMO: www.vertix.io

UPDATE: Here is an Image of what it currently looks like: enter image description here

Upvotes: 8

Views: 6520

Answers (2)

Milan Ray
Milan Ray

Reputation: 31

After trying to implement the method by Blindman67, I found a faster and shorter way to achieve this same result using CSS instead of drawing each image in a different way with a modified drawImage() function.

Here is the simpler method, please tell me if I am overlooking anything that the method above does address, although I believe all the problems are being addressed.

->Given a canvas defined as:

canvas

which equals a canvas element in the html page.

Desired native resolutions to scale to (see Blindman67's answer for clarification, if needed) defined as:

resW, resH

Device dimensions defined as:

devW, devH 
//Noting that these values usually correspond to window.innerWidth, window.innerHeight, respectively. 

(1) For the scale filling the screen, cropping away the excess do this:

let f = Math.max(window.innerWidth / resW, window.innerHeight / resH);
//f is the factor by which we redefine our canvas size as show in the code in the following lines:
//Change the canvas elements 'direct' width and height properties
canvas.width = Math.floor(devW / f);
canvas.height = Math.floor(devH / f);
//Change the canvas elements 'css' width and height properties to fill
// the entire page.
canvas.style.width = '100%';
canvas.style.height = '100%';

(2) For fitting the canvas content to the screen, it is much simpler by simply:

Set:

//Changing the 'css' canvas width size, making sure the width is always fitting.
canvas.style.width = '100%';

Then:

//Making the direct canvas width and height the desired native resolution's size
canvas.width = resW;
canvas.height = resH;

Now the CSS automatically takes care of scaling instead of relying on the drawImage's complex function.

Although for a game (the predominant function for canvas) the scale to fill may be more desirable due to the space that will appear on the screen if the user resized his or her window. On the other hand, if you use the scale to fill, the player can get a more immersive view, without blank space or the when there is no scaling and the problem where other players with larger screens have an advantage arises.

I understand that this was very late, but after searching for months for a solution less complex, I finally figured it out. So for others who may need this for reference I would like to keep this answer here.

Thank you very much for reading.

Upvotes: 3

Blindman67
Blindman67

Reputation: 54026

You will be faced with some problems.

High res displays will show bluring if you do not set ctx.imageSmoothingEnabled = false and low res displays will show aliasing effects if you do not set ctx.imageSmoothingEnabled = true.

The best is to not to go over a max resolution. This is arbitrary, and my option is to use the screen resolution most people use. An example of Screen stats there are many sites that provide this information. Pick one that closest reports your demographic target.

For displays larger than that do not increase the resolution just increase the size of the DOM element.

canvas.width and canvas.height set the size of the canvas. canvas.style.width and canvas.style.height set the resolution.

For displays with lower resolutions reduce the canvas resolution to fit the screen res. No point in giving the device extra pixels to render that will not be seen. I personally rerender all the graphics to correct res for lower res displays as the images are loaded. Again this is to stop the device from rendering more pixels than needed (mobile devices can not handle much) and also as the smoothing method leaves a lot to be desired (namely it is completely wrong) See this video to explain why Computer colour is broken

Be careful with reducing resolution. if you have a 100by100 pixel image and need to reduce it to 93by93 it will not look at all good. Pick the graphics sizes carefully for each render element to allow for the best possible reduction to the most common set of resolutions you will be displaying in.

Also allow for some wiggle room in the play field size, they don't all have to be precisely the same size.

Better still is to store graphics as vector based images (such as SVG or a custom format for vector rendering direct to canvas) and then render the images at the correct resolution on the device at load time.

Thus you are left with the aspect. There is nothing you can do about that and some screens will show more of the game playfield than others. You will have to decide what is the minimum size playfield that can be playable in both width and height and make sure that it does not go under that.

But with all that said and most of the work done you can't go back and redo all that workm so here is the simple solution.

var nativeWidth = 1024;  // the resolution the games is designed to look best in
var nativeHeight = 768;

// the resolution of the device that is being used to play
var deviceWidth = window.innerWidth;  // please check for browser compatibility
var deviceHeight = window.innerHeight;

You have two scaling options. Scale to fit, or scale to fill.

// A: This will ensure that the scale fills the device but will crop
// some of the top and bottom, or left and right of the play field.
// use the max scale.
var scaleFillNative = Math.max(deviceWidth / nativeWidth, deviceHeight / nativeHeight);

// B: this will ensure that all screens will see all of the native playscreen size
// but some displays will have extra playfield on the sides or top and bottom.
// use the min scale
var scaleFitNative = Math.min(deviceWidth / nativeWidth, deviceHeight / nativeHeight);

Set the canvas resolution and size to

canvas.style.width = deviceWidth + "px";
canvas.style.height = deviceHeight + "px";
canvas.width = deviceWidth;
canvas.height = deviceHeight;

Now you need to set the rendering scale

// ctx is the canvas 2d context 
ctx.setTransform(
    scaleFitNative,0, // or use scaleFillNative 
    0,scaleFitNative,
    Math.floor(deviceWidth/2),
    Math.floor(deviceHeight/2)
);

This has set the center of the screen as the origin.

UPDATE

There was an error in my original answer.

To get the offset to the native res top left is

var offSetToNativeTop = -nativeHeight/2; // incorrect
var offSetToNativeleft = -nativeWidth/2; // incorrect

SHOULD BE...

var offSetToNativeTop = (-nativeHeight/2)*scaleFitNative; // correct
var offSetToNativeleft = (-nativeWidth/2)*scaleFitNative; // correct

or

var offSetToNativeTop = (-nativeHeight/2)*scaleFillNative; // correct
var offSetToNativeleft = (-nativeWidth/2)*scaleFillNative; // correct

Sorry for any inconvenience this has caused.

To get the offset to the device top left

var offsetDeviceTop = -(deviceHeight/scaleFitNative)/2;
var offsetDeviceLeft = -(deviceWidth/scaleFitNative)/2;

to get the device playfield display size

var deviceDisplayWidth = deviceWidth/scaleFitNative;
var deviceDisplayHeight = deviceHeight/scaleFitNative;

Don't forget smoothing.

if(scaleFitNative < 1){
    ctx.imageSmoothingEnabled = true; // turn it on for low res screens
}else{
    ctx.imageSmoothingEnabled = false; // turn it off for high res screens.
}

You could also do it on individual render items depending on there scale and your personal preference as to how they look smoothed and reduced and smoothed expanded or not smoothed big small.

That should give you all you need to render your graphics for all devices

Upvotes: 9

Related Questions