htmlcsshtml5-canvasobject-fit

How do I make a canvas fill a div while keeping its aspect ratio?


I have a canvas element inside a flexbox that may be resized. How do I get the canvas to fill the available space without losing its aspect ratio (2/3), and without being cut off?

(I want to change the CSS dimensions, not the canvas resolution).

I've tried using object-fit: contain and clamp(...), but I can't get the results I want. Either the canvas doesn't maintain its aspect ratio, or it grows outside its container.

body {
  margin: 0;
}

#mainContent {
    background: grey;
    height: 100vh;
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
}

#someOtherElem {
    background: red;
    width: 200px;
    height: 200px;
    margin-left: 1rem;
}

#canvasContainer {
    display: flex;
    flex: 1;
    height: 100%;
    justify-content: center;
    align-items: center;
}

canvas {
    width: calc(100% - 2rem);
    height: calc(100% - 2rem);
    background: green;
    object-fit: contain;
}
<div id="mainContent">
  <div id="someOtherElem"></div>
  <div id="canvasContainer">
    <canvas height="300" width="200"></canvas>
  </div>
</div>

Here's a stripped-down version of what I've been trying: https://jsfiddle.net/mwq4502v/.

I'm not sure what the best way to achieve this is, so any help would be greatly appreciated!


Solution

  • You can specify an aspect-ratio of 2 / 3 for canvas and a width of 100% or <parent-height> * 2 / 3, whichever smaller.

    canvas {
      aspect-ratio: 2 / 3;
      width: min(100%, 100vh * 2 / 3);
      /* ...or height: min(100%, calc(100vw - 210px) * 3 / 2). Same spirit. */
    }
    

    Some math:

    Let the width and height of the container be w and h, correspondingly. Since the canvas needs to be as large as possible it will always touch at least two borders of the container (or all 4), which means its size can be either w / (w / (2 / 3)) or (h * 2 / 3) / h, depends on the size of the container.

    w / h > 2 / 3:

    ◄──────────── w ────────────►
    ┌────────┬─────────┬────────┐ ▲
    │        │         │        │ │
    │        │         │        │ │
    │        │         │        │ │
    │        │         │        │ h
    │        │         │        │ │
    │        │         │        │ │
    │        │         │        │ │
    └────────┴─────────┴────────┘ ▼
             ◄ (h*2/3) ►
    

    w / h < 2 / 3:

             ◄─── w ───►
             ┌─────────┐ ▲
             │         │ │
           ▲ ├─────────┤ │
           │ │         │ │
           │ │         │ │
     (w*3/2) │         │ h
           │ │         │ │
           │ │         │ │
           ▼ ├─────────┤ │
             │         │ │
             └─────────┘ ▼
    

    This means the width needs to be min(w, h * 2 / 3) or, in CSS, min(100%, 100vh * 2 / 3).

    Try it:

    canvas {
      aspect-ratio: 2 / 3;
      width: min(100%, 100vh * 2 / 3);
    }
    
    /* Demo only */
    
    #canvasContainer {
      outline: 1px solid #000; /* Just so we know where it is */
    }
    
    body {
      margin: 0;
    }
    
    #mainContent {
      display: flex;
      align-items: center;
      height: 100vh;
    }
    
    #someOtherElem {
      margin-left: 1rem;
      width: 200px;
      height: 200px;
      background: red;
    }
    
    #canvasContainer {
      flex: 1;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100%;
    }
    
    canvas {
      background: green;
    }
    <div id="mainContent">
      <div id="someOtherElem"></div>
      <div id="canvasContainer">
        <canvas></canvas>
      </div>
    </div>