Search code examples
javascriptcsshtml5-canvascss-position

Canvas element spilling out past its container div


I am trying to make a canvas element fit to its container div using Javascript rather than CSS, because this way it doesn't stretch anything drawn inside the canvas.

Currently, if I refresh the page, sometimes the canvas element fits perfectly inside. Other times, when I refresh, its width is greater than the container, and its height is lesser than the container. I have no idea why I get this different behavior upon refreshing the page, even without resizing the window.

Here's how I set up the canvas size:

const ctxX = canvasParent.offsetWidth - 4;
const ctxY = canvasParent.offsetHeight - 4;
canvasEl.width = ctxX;
canvasEl.height = ctxY;
const ctx = canvasEl.getContext("2d");

I read that setting size like this only works if the container is positioned relative, so this is the CSS for the container:

display: block;
position: relative;
height: 60%;
margin-top: 2%;
border: 2px solid v.$border;

Solution

  • The root cause is that offsetHeight and offsetWidth values are rounded and setting canvas dimensions to them will usually result in an inexact fit, either resulting in under or over-flow of width or height. I am not ruling out other causes due to calculations performed by layout engines, but this cause is crucial.

    One solution is to use element.getBoundingClientRect to get the dimensions of the parent (div) element exactly, take the integers below height and width values and use them to set the dimensions of both the canvas and its parent element. This could result in the div element being up to 1px less than percentage value conversions to CSS pixel width and height, but that's inevitable given canvas dimension attributes take integer values.

    Here's an example demonstrating this specific technique with warnings: changing canvas dimensions due to resize will destroy existing canvas content, and a resize handler would need to remove style settings for width and height on the canvas parent to find out what the resized dimensions would be.

    Note: relative positioning is not required and not included below.

    "use strict";
    let rect = canvasParent.getBoundingClientRect();
    const ctxX = Math.floor(rect.width -4);
    const ctxY = Math.floor(rect.height -4);
    canvasEl.width = ctxX;
    canvasEl.height = ctxY;
    canvasParent.style.width = ctxX + 'px';
    canvasParent.style.height = ctxY + 'px';
    const ctx = canvasEl.getContext("2d");
    #canvasParent {
        height: 60%;
        margin-top: 2%;
        border: 2px solid orange;
        box-sizing: content-box;
    }
    canvas {
      background-color: forestgreen;
    
    }
    
    /* to see margins: */
    body   { background-color: grey; }
    .outer { height: 80vh; background-color: #ddd; }
    <div class="outer"> <!-- outer div -->
       <div id="canvasParent"><canvas id="canvasEl"></canvas></div>
    </div>

    Update

    1. In the code snippet, an outer container of the canvas container needs to have an intrinsic height from which to calculate percentage height values for #canvasParent.

    2. Percentage vertical margin values are weird in CSS in that they refer to horizontal width percentages of their containing block (ref MDN and this question). To make matters worse, if child elements within a container have a top margin in percentage units, and are not preceded by content in their container block, the generated width of the container when the child element is encountered is taken as zero - resulting in no top margin on the child.

      This is why running the snippet doesn't show any top margin applied to #containerEl. If you put some text at the start of .outer, #containerEl suddenly gains a top margin with respect to the text inserted (not shown in the snippet).

      My conclusion: while this may not affect your project, don't use percentage units for vertical margins without very good reason :-)