20

Is there a way I can create a canvas inside a dynamic re-sizing flex-box container?

Preferably a CSS only solution but I think JavaScript is required for redraw?

I think one solution could be to listen to the re-sizing event and then scale the canvas to meet the size of the flex box parent then force a redraw but I would preferably like to use as much CSS as possible or a more clean/less code solution.

The current approach is CSS based where the canvas is re-sized according to the parent flex box element. The graphics are blurred, re positioned and overflowed from the canvas in the below screenshot.

Stretched image/Graphics overflowing canvas

CSS:

html,body{
            margin:0;
            width:100%;
            height:100%;
        }
        body{
            display:flex;
            flex-direction:column;
        }
header{
            width:100%;
            height:40px;
            background-color:red;
        }
        main{
            display:flex;
            flex: 1 1 auto;
            border: 1px solid blue;
            width:80vw;
        }
        canvas{
            flex: 1 1 auto;
            background-color:black;
        }

HTML:

<header>
</header>
<main>
    <canvas id="stage"></canvas>
</main>

Javascript:

$( document ).ready(function() {
    var ctx = $("#stage")[0].getContext("2d");
    ctx.strokeStyle="#FFFFFF";
    ctx.beginPath();
    ctx.arc(100,100,50,0,2*Math.PI);
    ctx.stroke();
});

JS fiddle showing the issue: https://jsfiddle.net/h2v1w0a1/

Xela
  • 2,322
  • 1
  • 17
  • 32
  • canvas is taking width of its parent i.e 80vw. with 100vw it may solve your problem – Neel Shah May 14 '15 at 05:02
  • no.. that's purposeful, a menu is going to later be on the right of it. The graphic scaling is the issue, may need to do an edit so others don't have the same assumption. – Xela May 14 '15 at 05:04
  • @Dima could you clarify a bit what you call "the original question"? As I read it, it's perfectly answered by the currently accepted one (except that it could win from an update using a ResizeObserver). In order to make a canvas "fit" in a dynamic container it has to be resized, or are you thinking of an other behavior? If so which one? – Kaiido Aug 05 '20 at 14:24
  • Maybe you are right, maybe I can't read :) I guess, ultimately, I'd love to have both options: canvas following container size, with and without changing the canvas (internal) pixel size. – Dima Tisnek Aug 06 '20 at 03:08
  • i think you are looking for `object-fit` ofr a pure `CSS` solution. Here is one https://jsfiddle.net/vfsgpm3q/. –  Aug 11 '20 at 21:07
  • if you want the object inside canvas to be resized then `object-fit` set to `contain` will do. If not then `object-fit` set to `none` does the work. Either way your pure `CSS` solution is to add one more line in your `CSS` (most efficient). It would be ideal also to define if you want a resize effect or not since from what is written in the question it is a bit vague. –  Aug 12 '20 at 13:25

5 Answers5

26

Think of canvas as an image. If you scale it to a different size than the original it will appear blurry at some point, and perhaps early on depending on interpolation algorithm chosen by the browser. For canvas the browser tend to chose bi-linear over bi-cubic so there is higher risk of blur than with an actual image.

You will want to have things in canvas rendered as sharp as possible and the only way to this is to adapt the size to the parent using JavaScript, and avoid CSS (the latter is good for things like printing, or when you need "retina resolutions").

To get the parent container size in pixels you can use getComputedStyle() (see update below):

var parent = canvas.parentNode,
    styles = getComputedStyle(parent),
    w = parseInt(styles.getPropertyValue("width"), 10),
    h = parseInt(styles.getPropertyValue("height"), 10);

canvas.width = w;
canvas.height = h;

Fiddle

(Note: Chrome seem to have some issues with computed flex at the moment)

Update

Here's a simpler way:

var rect = canvas.parentNode.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;

Fiddle

  • 3
    Adding style="position:absolute" on canvas made it also work on edge for me. – Glenn Sep 22 '18 at 03:55
  • 2
    The second fiddle link is broken. – 0xcaff Dec 01 '18 at 17:26
  • 1
    @Glenn - this also helped me in chrome. I was already getting the size of the canvas's parent and setting the canvas to that size, but when I then put that parent in a flexbox the canvas was breaking out. Setting position: absolute on the canvas fixed it. – gsn1074 Mar 12 '20 at 06:07
5

The only CSS solution I can think about is the use of object-fit:none (related: How does object-fit work with canvas element?)

$(document).ready(function() {
  var ctx = $("#stage")[0].getContext("2d");
  ctx.strokeStyle = "#FFFFFF";
  ctx.beginPath();
  ctx.arc(100, 100, 50, 0, 2 * Math.PI);
  ctx.stroke();
});
html,
body {
  margin: 0;
  width: 100%;
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
}

header {
  width: 100%;
  height: 40px;
  background-color: red;
}

main {
  display: flex;
  flex: 1 1 auto;
  border: 1px solid blue;
  width: 80vw;
}

canvas {
  flex: 1 1 auto;
  background-color: black;
  object-fit:none;
  object-position:top left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<header>
</header>
<main>
  <canvas id="stage"></canvas>
</main>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • object-fit if set to none will work for sure but it does not resize the canvas. Object-fit set to contain seems more appropriate in this situation than set to none. –  Aug 12 '20 at 12:25
  • @PeterDarmis check the code of the accepted answer and you will see that the goal is *not* to resize the canvas content. Contain and cover will resize the content which is not the purpose here – Temani Afif Aug 12 '20 at 12:55
  • Most probably i must have not read correctly this part in the question... **I think one solution could be to listen to the re-sizing event and then scale the canvas to meet the size of the flex box parent then force a redraw but I would preferably like to use as much CSS as possible or a more clean/less code solution.** –  Aug 12 '20 at 12:57
  • @PeterDarmis *scale the canvas to meet the size of the flex box parent* --> adjust the width/height of the canvas to be the one of the parent AND this will distort the content that's why : * then force a redraw* --> we redraw again considering the new width/height of the canvas to get the exact result. See the difference between the jsfiddle of the question (where the content of the canvas is distored/changed) and the jsfiddle of the accepted answer where the content is kept the same. This is what I am doing here and this is the use of object-fit:none; – Temani Afif Aug 12 '20 at 13:03
  • i don't know when i answered i did not read the other answers or comments, i read only the question and saw the fact that the circle was distorted in the jsfiddle provided. That for me did not mean that the question did not want the canvas to resize accordingly. If i am wrong in my initial reading of the question then there is not case `object-fit` set to none does the work. But i am really troubled by the phrase mentioned in my comment above. I really do think that it is meant to be resized and i have not seen the question holder to define this. Best regards, sorry for my comment. –  Aug 12 '20 at 13:22
3

Using object-fit provides a pure CSS solution. I think you need to check for yourself which value is better to set to object-fit between cover and contain, there are two examples below.

$(document).ready(function() {
  var ctx = $("#stage")[0].getContext("2d");
  ctx.strokeStyle = "#FFFFFF";
  ctx.beginPath();
  ctx.arc(100, 100, 50, 0, 2 * Math.PI);
  ctx.stroke();
});
html,
body {
  margin: 0;
  width: 100%;
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
}

header {
  width: 100%;
  height: 40px;
  background-color: red;
}

main {
  display: flex;
  flex: 1 1 auto;
  border: 1px solid blue;
  width: 80vw;
}

canvas {
  object-fit: cover;
  background-color: black;
  width: 100%;
  height: auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<header>
</header>
<main>
  <canvas id="stage"></canvas>
</main>

$(document).ready(function() {
  var ctx = $("#stage")[0].getContext("2d");
  ctx.strokeStyle = "#FFFFFF";
  ctx.beginPath();
  ctx.arc(100, 100, 50, 0, 2 * Math.PI);
  ctx.stroke();
});
html,
body {
  margin: 0;
  width: 100%;
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
}

header {
  width: 100%;
  height: 40px;
  background-color: red;
}

main {
  display: flex;
  flex: 1 1 auto;
  border: 1px solid blue;
  width: 80vw;
}

canvas {
  object-fit: contain;
  background-color: black;
  width: 100%;
  height: auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<header>
</header>
<main>
  <canvas id="stage"></canvas>
</main>

You will also find both examples in JSFiddle https://jsfiddle.net/vfsgpm3q/, https://jsfiddle.net/vfsgpm3q/1/

0

One other solution is to create a new containing block, and make it follow the formatting context:

const Canvase = styled.canvas`
  flex: auto;
  position: relative;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
`;

It is somewhat complimentary/orthogonal to the object-fit: ... suggested by Temani and Peter

Dima Tisnek
  • 11,241
  • 4
  • 68
  • 120
0

$(document).ready(function() {
  var ctx = $("#stage")[0].getContext("2d");
  ctx.strokeStyle = "#FFFFFF";
  ctx.beginPath();
  ctx.arc(100, 100, 50, 0, 2 * Math.PI);
  ctx.stroke();
});
html,
body {
  margin: 0;
  width: 100%;
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
}

header {
  width: 100%;
  height: 40px;
  background-color: red;
}

main {
  display: flex;
  flex: 1 1 auto;
  border: 1px solid blue;
  width: 80vw;
}

canvas {
  flex: 1 1 auto;
  background-color: black;
  object-fit:none;
  object-position:top left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<header>
</header>
<main>
  <canvas id="stage"></canvas>
</main>
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 04 '23 at 09:58