3

I have a question for a html5 game using canvas in javascript.

I would like that the canvas showed to the player contains either 1920 of canvas.width, either 1080 of canvas.height in order to not see the padding. That means that I don't want to keep the (optimal) game ratio 16/9 if the player resize his screen with an other ratio, but I want to keep the viewport ratio 16/9 to avoid stretching (using canvas.style.***)

Let me take an example of a player with a resizable screen (window.innerWidth = 500 and window.innerHeight = 1600), as 1600/500 > 1920 / 1080, I would like that the player can see 1920 of the game width and "less than 1080" of the game height, preventing viewport stretching.

Agar.io is an other good example.

Thank you very much !

zbeyens
  • 321
  • 1
  • 5
  • 16
  • So if I understand clearly, you want the "xMidYMid meet" of [this function](http://stackoverflow.com/a/34429774) ? – Kaiido Aug 11 '16 at 14:03
  • Can't you just use CSS to fill the space and use JS to reassign width and height? https://jsfiddle.net/4rhdef0w/ – Domino Aug 11 '16 at 18:06

1 Answers1

3

To fit but will leave empty areas on the sides or top bottom if aspects do not match.

var scale = Math.min(innerWidth / canvas.width, innerHeight / canvas.height);

or to fill but will clip canvas if aspects do not match

var scale = Math.max(innerWidth / canvas.width, innerHeight / canvas.height);

for display 1600 by 500 and canvas 1920 by 1080

1600 / 1920 = 0.8333;
500 / 1080 = 0.463;

thus to fit canvas will be 0.463 * (1920,1080) = (889,500) empty on sides

and to fill canvas will be 0.8333 * (1920,1080) = (1600,900) clipped top and bottom.

More info on scale and fit can be found below.

If you are scaling to fill the canvas you will need to account for the clipped area and find the offset to the top left corner of the canvas (this will be off the page).

var leftOffset = 0;
var topOffset = 0;
var canW = scale * canvas.width;
var canH = scale * canvas.height;
if(canW > innerWidth ){
    leftOffset = ((canW - innerWidth) / canW) * 1920 / 2;
}else
if(canH > innerHeight ){
    topOffset = ((canH - innerHeight) / canH) * 1080 / 2;
}

Your canvas will fill the page innerWidth and innerHeight but you will need to offset all rendering. This can be done by setting the transform to the correct offsets

ctx.setTransform(1,0,0,1,-leftOffset, -topOffset);

The canvas display size will be

canvas.style.width = innerWidth + "px"; 
canvas.style.height = innerHeight + "px";

and the canvas resolution will be

canvas.width = 1920 - (leftOffset * 2);
canvas.height = 1080 - (topOffset * 2);

Below is an example of the code in action. Very lazy coding, just to show it working should really not create and destroy canvas on resizes. Also as fiddle because it has resizable panels to test with.

 var canvas;
var createCanvas = function(){
    if(canvas !== undefined){
         document.body.removeChild(canvas);
    
    }
    var canWidth = 1920;
    var canHeight = 1080;

    var scale = Math.max(innerWidth /canWidth, innerHeight / canHeight);
    var leftOffset = 0;
    var topOffset = 0;
    var canW = scale * canWidth;
    var canH = scale * canHeight;
    if(canW > innerWidth ){
        leftOffset = ((canW - innerWidth) / canW) * canWidth / 2;
    }else
    if(canH > innerHeight ){
        topOffset = ((canH - innerHeight) / canH) * canHeight / 2;
    }

    canvas = document.createElement("canvas");
    canvas.style.position = "absolute";
    canvas.style.top = "0px";
    canvas.style.left = "0px";
    canvas.style.width = innerWidth + "px"; 
    canvas.style.height = innerHeight + "px";
    canvas.width = canWidth - (leftOffset * 2);
    canvas.height = canHeight - (topOffset * 2);
    document.body.appendChild(canvas);
    var ctx = canvas.getContext("2d");


    ctx.setTransform(1,0,0,1,-leftOffset, -topOffset);
    ctx.beginPath();
    ctx.arc(canWidth / 2, canHeight/2, 400,0,Math.PI * 2);
    ctx.stroke();
}

window.addEventListener('resize',createCanvas);
createCanvas();

Scaling to fit

Means that the whole image will be visible but there may be some empty space on the sides or top and bottom if the image is not the same aspect as the canvas. The example shows the image scaled to fit. The blue on the sides is due to the fact that the image is not the same aspect as the canvas.

enter image description here

Scaling to fill

Means that the image is scaled so that all the canvas pixels will be covered by the image. If the image aspect is not the same as the canvas then some parts of the image will be clipped. The example shows the image scaled to fill. Note how the top and bottom of the image are no longer visible.

enter image description here

Example Scale to fit

var image = new Image();
image.src = "imgURL";
image.onload = function(){
    scaleToFit(this);
}

function scaleToFit(img){
    // get the scale
    var scale = Math.min(canvas.width / img.width, canvas.height / img.height);
    // get the top left position of the image
    var x = (canvas.width / 2) - (img.width / 2) * scale;
    var y = (canvas.height / 2) - (img.height / 2) * scale;
    ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
}

Example Scale to fill

var image = new Image();
image.src = "imgURL";
image.onload = function(){
    scaleToFill(this);
}

function scaleToFill(img){
    // get the scale
    var scale = Math.max(canvas.width / img.width, canvas.height / img.height);
    // get the top left position of the image
    var x = (canvas.width / 2) - (img.width / 2) * scale;
    var y = (canvas.height / 2) - (img.height / 2) * scale;
    ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
}

The only differance between the two functions is getting the scale. The fit uses the min fitting scale will the fill uses the max fitting scale.

Graham
  • 7,431
  • 18
  • 59
  • 84
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Thanks ! I "scale to fill" successfully but now I can't center the image (x, y)... Can you help me ? Here is the actual code on resize : `canvas.height = 1080; canvas.width = 1920; var scale = Math.max(window.innerWidth / canvas.width; window.innerHeight / canvas.height); canvas.style.width = 1920 * scale + "px"; canvas.style.height = 1080 * scale + "px";` – zbeyens Aug 11 '16 at 17:12
  • @Miyud I will add more to the answer so that you can get the correct render region – Blindman67 Aug 11 '16 at 17:19
  • @Miyud Ok that was from the top of my head will have to just test it out. Will update when I find out what is wrong – Blindman67 Aug 11 '16 at 17:51
  • Well your answer works with : `canvas.style.width = 1920 * scale + "px"; canvas.style.height = 1080 * scale + "px";` and keeping `canvas.width = 1920; canvas.height = 1080;`. Thanks a lot for your quick replies :) – zbeyens Aug 11 '16 at 17:55
  • Ow sorry I forgot to remove one of my line code by testing your code. Your answer is right then. Thanks again :) – zbeyens Aug 11 '16 at 17:59
  • @Miyud All good then.. Could not workout what was wrong as fiddle worked.. Happy coding. – Blindman67 Aug 11 '16 at 18:05
  • What about what's called on Android as "fit-center" ? It means : scales the image to as much as it can, to the width or height of device (or as much as possible), while also maintaining aspect ratio – android developer Sep 25 '18 at 22:32
  • 1
    @androiddeveloper What you call "fit-center" is call scaleToFit. Which means that the Image is as large as it can possibly be and can see all pixels. – Blindman67 Sep 24 '20 at 19:01
  • @Blindman67 I wrote this a long time ago. Thanks. I don't even remember why I asked it – android developer Sep 25 '20 at 11:29