I have a canvas that is the only thing on a page I want really want to see. I'm trying to make it such that when I resize my window, I always have the entire canvas viewable with no scroll bars and maintain the aspect ratio.
MCVE
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let w = canvas.width;
let h = canvas.height;
ctx.fillStyle = 'teal';
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = 'red';
ctx.lineWidth = 10;
ctx.rect(5, 5, w - 5, h - 5);
ctx.stroke();
* {
background-color: white;
max-height: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
div#canvasDiv {}
canvas#canvas {
display: block;
margin: auto;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<div id="canvasDiv">
<canvas id="canvas" width="500" height="500" />
</div>
</body>
</html>
All here at jsfiddle
With this attempt, it rescales just fine when I resize the width. However, when I resize the height, I get overflow.
Resize Width with appropriate rescaling
Resize Height with unwanted overflow
You may like to read this article: [Centering in CSS: A Complete Guide](https://css-tricks.com/centering-css-complete-guide/
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let w = canvas.width;
let h = canvas.height;
ctx.fillStyle = 'teal';
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = 'red';
ctx.lineWidth = 10;
ctx.rect(5, 5, w - 5, h - 5);
ctx.stroke();
* {
background-color: white;
max-height: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
canvas#canvas {
display: block;
margin: auto;
position:absolute;
top:0;bottom:0;
left:0;
right:0;
}
<div id="canvasDiv">
<canvas id="canvas" width="500" height="500"/>
</div>
Related
This question already has answers here:
Canvas is stretched when using CSS but normal with "width" / "height" properties
(10 answers)
Closed 5 months ago.
Hello I have a simple question. I am trying to draw a circle with ctx.arc(), but the output is always an oval. May I know what is my mistake here?
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(300, 130, 100, 0, Math.PI * 2);
ctx.fill();
#canvas {
border: 1px solid black;
width: 640px;
height: 640px;
}
<canvas id="canvas">My Canvas</canvas>
The canvas needs to know the number of logical pixels in both height and width to draw the shapes in the right dimensions.
Explicitly set the width and height property of the canvas to fix the drawing.
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
canvas.width = 640;
canvas.height = 640;
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(300, 130, 100, 0, Math.PI * 2);
ctx.fill();
#canvas {
border: 1px solid black;
width: 640px;
height: 640px;
}
<canvas id="canvas">My Canvas</canvas>
Alternatively you could also set the properties as attributes in your HTML.
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(300, 130, 100, 0, Math.PI * 2);
ctx.fill();
#canvas {
border: 1px solid black;
width: 640px;
height: 640px;
}
<canvas id="canvas" width="640" height="640">My Canvas</canvas>
I have a canvas like so:
But when resizing the window and redrawing, you can get anything in between.
I keep the aspect ratio with this code:
function setCanvasSize(){
ctx.canvas.width = (window.innerWidth)*0.8;
ctx.canvas.height= (window.innerWidth)*0.5;
}
setCanvasSize()
$(window).resize(function(){
setCanvasSize()
})
and that keeps it nice and clean, the same-looking size, but how many pixels are in there changes, so when my program draws, it's always in a different spot, although the aspect is the same, so it looks really weird.
Any way that I can somehow tweak how I draw things, or a way to keep how many pixels there are?
Edit: code snippet to reproduce
const ctx = document.getElementById('game').getContext("2d");
$('#game').css('width', '100%');
$(window).resize(function(){
$('#game').height($('#game').width() / 2.031);
});
const imageAt=((href,x,y,sizex,sizey)=>{
var img = new Image();
img.addEventListener('load', function() {
ctx.drawImage(img, x, y, sizex, sizey);
}, false);
img.src = href
})
imageAt('https://b.thumbs.redditmedia.com/bZedgr0gq7RQBBnVYVc-Nmzdr-5vEUg4Dj8nTrMb7yA.png',0,0,50,50)
#game{
padding-left: 0;
padding-right: 0;
display: block;
margin: 0;
border:5px solid green;
padding:0;
}
#vertical-center {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: black;
padding:0;
}
body{
background-color: black;
padding:0;
}
<html>
<head>
<title>Tiled Canvas Engine Test 1</title>
<meta name="viewport" content="width=device-width, initial-scale=0.75">
<link rel="stylesheet" href="./index.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="engine.js"></script>
<script defer src="script.js"></script>
</head>
<body>
<div id="vertical-center">
<canvas id="game"></canvas>
</div>
</body>
</html>
In order to render properly the canvas element needs to have its width and height attributes set explicitly. This will define the viewport into which graphics will be draw. The style attributes for width and height which is what jQuery sets via the width() and height() functions will only stretch the viewport to fit. (Note: for high DPI displays, i.e. Retina, you will want to double the dimensions when specifying the attributes).
The critical change is this:
// Updates the actual width and height attributes
canvas.width = $('#game').width() * window.devicePixelRatio;
canvas.height = $('#game').height() * window.devicePixelRatio;
The snippet below also handles clearing the canvas, redrawing the image, and saving the Image once loaded:
const ctx = document.getElementById('game').getContext("2d");
$('#game').css('width', '100%');
$(document).ready(handleResize);
$(window).resize(handleResize);
function handleResize() {
// sets the style.height and style.width for the canvas
$('#game').height($('#game').width() / 2.031);
let canvas = $('#game').get(0);
// Updates the actual width and height attributes
canvas.width = $('#game').width() * window.devicePixelRatio;
canvas.height = $('#game').height() * window.devicePixelRatio;
// redraw
if (img) {
redraw();
}
}
var img;
const imageAt = ((href, x, y, sizex, sizey) => {
img = new Image();
img.addEventListener('load', function() {
ctx.drawImage(img, x, y, sizex, sizey);
}, false);
img.src = href
})
imageAt('https://b.thumbs.redditmedia.com/bZedgr0gq7RQBBnVYVc-Nmzdr-5vEUg4Dj8nTrMb7yA.png', 0, 0, 50 * window.devicePixelRatio, 50 * window.devicePixelRatio)
function redraw() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0, 50 * window.devicePixelRatio, 50 * window.devicePixelRatio);
}
#game {
padding-left: 0;
padding-right: 0;
display: block;
margin: 0;
border: 5px solid green;
padding: 0;
}
#vertical-center {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: black;
padding: 0;
}
body {
background-color: black;
padding: 0;
}
<html>
<head>
<title>Tiled Canvas Engine Test 1</title>
<meta name="viewport" content="width=device-width, initial-scale=0.75">
<link rel="stylesheet" href="./index.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="engine.js"></script>
<script defer src="script.js"></script>
</head>
<body>
<div id="vertical-center">
<canvas id="game"></canvas>
</div>
</body>
</html>
I have created a circle here so that when the screen is clicked, the circle will move there. And I also have a fixed point (vertex) that I want these two points to be the origin and destination of a line.
const coordinates = document.querySelector(".coordinates");
const circle = document.querySelector(".circle");
const result = document.querySelector(".result");
const numbers = document.querySelectorAll("p");
coordinates.addEventListener("click", e => {
circle.style.setProperty('--x', `${e.clientX}px`);
circle.style.setProperty('--y', `${e.clientY}px`);
})
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.moveTo(0,0);
ctx.lineTo(50,100);
ctx.stroke();
canvas {
color: rgb(255, 255, 255);
}
.circle {
width: 20px;
height: 20px;
border-radius: 50%;
position: fixed;
--x: 47px;
left: calc(var(--x) - 10px);
--y: 47px;
top: calc(var(--y) - 10px);
background-color: white;
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #24232e;
}
.coordinates {
height: 100%;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="bg">
<div class="coordinates">
<canvas id="myCanvas" width="200" height="100" style=""></canvas>
</div>
</div>
<div class="circle">
</div>
</body>
</html>
How to do this with canvas?
! would appreciate <3
You can detect the mouse position on the canvas and draw a line to the coordinates. to get the canvas x and y of the mouse, you have to do some calculations because the site coordinates are a bit different than the canvas ones.
A more detailed description is here: https://stackoverflow.com/a/17130415/14076532
This will work only, if the canvas is big enough, logically...
Hope this helps:
const coordinates = document.querySelector(".coordinates");
const circle = document.querySelector(".circle");
const result = document.querySelector(".result");
const numbers = document.querySelectorAll("p");
coordinates.addEventListener("click", e => {
clicked(e);
circle.style.setProperty('--x', `${e.clientX}px`);
circle.style.setProperty('--y', `${e.clientY}px`);
})
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.strokeStyle = "#de7270"; // change your color here NOTE: this will remain until you change it
ctx.moveTo(0,0);
ctx.lineTo(50,100);
ctx.stroke();
function clicked(event) {
ctx.save(); // some canvas-safety-stuff
ctx.beginPath();
let mousepos = getMousePos(event); // get the mouse position in "canvas-cooridnates"
ctx.clearRect(0,0, c.width, c.height) // erease the canvas
ctx.moveTo(0, 0);
ctx.lineTo(mousepos.x, mousepos.y); // draw the line to the mouse
ctx.closePath();
ctx.stroke(); // closing of the canvas-safety-stuff
ctx.restore();
}
function getMousePos (evt) {
var rect = c.getBoundingClientRect(), // abs. size of element
scaleX = c.width / c.width, // relationship bitmap vs. element for X
scaleY = c.height / rect.height; // relationship bitmap vs. element for Y
return {
x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have
y: (evt.clientY - rect.top) * scaleY // been adjusted to be relative to element
}
}
canvas {
color: rgb(255, 255, 255);
}
.circle {
width: 20px;
height: 20px;
border-radius: 50%;
position: fixed;
--x: 47px;
left: calc(var(--x) - 10px);
--y: 47px;
top: calc(var(--y) - 10px);
background-color: white;
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #24232e;
}
.coordinates {
height: 100%;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="bg">
<div class="coordinates">
<canvas id="myCanvas" width="200" height="100" style=""></canvas>
</div>
</div>
<div class="circle">
</div>
</body>
</html>
I'm building a to-do list with a progress circle, using one of the alternatives given here (CSS Progress Circle). In my script.js I defined the function drawRingProgress() which renders the canvas when I execute it at the end of the script.
As the other functions of my script are executed to add tasks, edit, remove, or mark them as complete, the parameters pendingTasks and completedTasks get updated. However, if I call the function drawRingProgress() within the other mentioned functions, in order to update the progress, the canvas is wrongly drawn somewhere else multiple times (depending on the HTML elements these functions are acting on). What would be a correct approach to render the updated progress percentage?
Link to the working example: https://jsfiddle.net/tailslider13/f4qtmhzj/7/
let pendingTasks = 31;
let completedTasks = 69;
function drawRingProgress(pendingTasks, completedTasks) {
var el = document.getElementById('graph'); // get canvas
let progress_percentage = Math.floor((completedTasks / (completedTasks + pendingTasks)) * 100) || 0;
var options = {
// percent: el.getAttribute('data-percent') || 25,
percent: progress_percentage,
// size: 110,
size: el.getAttribute('data-size') || 220,
lineWidth: el.getAttribute('data-line') || 15,
rotate: el.getAttribute('data-rotate') || 0
}
var canvas = document.createElement('canvas');
var span = document.createElement('span');
span.textContent = options.percent + '%';
if (typeof (G_vmlCanvasManager) !== 'undefined') {
G_vmlCanvasManager.initElement(canvas);
}
var ctx = canvas.getContext('2d');
canvas.width = canvas.height = options.size;
el.appendChild(span);
el.appendChild(canvas);
ctx.translate(options.size / 2, options.size / 2); // change center
ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); // rotate -90 deg
//imd = ctx.getImageData(0, 0, 240, 240);
var radius = (options.size - options.lineWidth) / 3.2;
var drawCircle = function (color, lineWidth, percent) {
percent = Math.min(Math.max(0, percent || 1), 1);
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, false);
ctx.strokeStyle = color;
ctx.lineCap = 'round'; // butt, round or square
ctx.lineWidth = lineWidth
ctx.stroke();
};
drawCircle('#efefef', options.lineWidth, 100 / 100);
drawCircle('#046582', options.lineWidth, options.percent / 100)
}
drawRingProgress(pendingTasks, completedTasks);
Here is how I would draw the graph. I have removed all of the other functions from this so it is only showing the graph progress based on what you set the variables to. Once you get your other functions figured out you can updated them via that method.
First I would get the canvas at the beginning of the script and also designate the variables a global.
Second I would draw the white doughnut flat out. Unless you plan on changing it in some way the function drawGraph() will get called once and that's it.
Third the function drawRingProgress() will get called from your other functions when you add, delete, or complete a task. Be sure those function also update pendingTasks and completedTasks prior to calling drawRingProgress().
Inside drawRingProgress() I added the text since canvas has that built in method so you don't need to use a <span>. As far as all your options I removed them for this but you can add them back as where you see fit.
const inputField = document.getElementById("addTask");
const taskList = document.getElementById("taskList");
var canvas = document.getElementById('graph');
var ctx = canvas.getContext('2d');
canvas.width = 200;
canvas.height = 200;
let pendingTasks = 20;
let completedTasks = 5;
//Progress ring
function drawGraph() {
ctx.beginPath();
ctx.strokeStyle = "white";
ctx.arc(canvas.width/2, canvas.height/2, 50, 0, Math.PI*2, false);
ctx.lineCap = 'round'; // butt, round or square
ctx.lineWidth = 15;
ctx.stroke();
ctx.closePath();
}
drawGraph();
function drawRingProgress(pendingTasks, completedTasks) {
let progress_percentage = (completedTasks / pendingTasks) * 100;
ctx.font = "30px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#046582";
ctx.fillText(progress_percentage+'%', canvas.width/2,canvas.height/2);
percent = Math.min(Math.max(0, (progress_percentage/100) || 1), 1);
ctx.beginPath();
ctx.save();
ctx.translate(0, canvas.height); // change center
ctx.rotate((-1 / 2 + 0 / 180) * Math.PI); // rotate -90 deg
ctx.arc(canvas.width/2, canvas.height/2, 50, 0, Math.PI * 2 * percent, false);
ctx.strokeStyle = "#046582";
ctx.lineCap = 'round'; // butt, round or square
ctx.lineWidth = 15;
ctx.stroke();
ctx.restore();
ctx.closePath();
}
drawRingProgress(pendingTasks, completedTasks);
#body {
background-color: #046582;
}
header {
background-color: #f39189;
padding: 50px;
margin: 50px;
position: sticky;
top: 0px;
}
h1 {
text-align: center;
}
.listItem {
margin: 20px 0px;
background-color: white;
}
.container {
background-color: #c4c4c4;
}
.taskList {
list-style-type: none;
background-color: transparent;
overflow: hidden;
margin-top: 150px;
}
.inputContainer {
margin: 50px;
padding: 20px;
max-width: 50%;
width: 100%;
background-color: #f39189;
}
#footer {
text-align: center;
position: sticky;
bottom: 0px;
background-color: #f39189;
padding: 20px;
}
.deleteButton {
background-image: url("/content/delete.png");
background-repeat: no-repeat;
background-size: cover;
cursor: pointer;
width: 30px;
height: 30px;
margin: 15px;
}
#addTask {
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 1.3rem;
}
.taskName {
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 1.2rem;
}
.listContainer {
height: 1080px;
}
.inputContainer {
position: fixed;
}
.checkedbox {
text-decoration: line-through;
color: #f39189;
}
/* START Styling Progress ring */
.chart {
position: relative;
/* margin:0px; */
width: 220px;
height: 220px;
}
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
}
span {
color: #046582;
display: block;
line-height: 220px;
text-align: center;
width: 220px;
font-family: sans-serif;
font-size: 30px;
font-weight: 100;
margin-left: 5px;
}
/* Links progress ring */
/* https://stackoverflow.com/questions/14222138/css-progress-circle
http://jsfiddle.net/Aapn8/3410/ */
/* END Styling Progress ring */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap#5.0.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous" />
</head>
<div class="container">
<body id="body">
<header class="row justify-content-end">
<h1 class="col-4">Take note</h1>
<!-- Progress ring -->
<div class="col-4">
<canvas class="chart" id="graph"></canvas>
</div>
</header>
<!-- Input field and button -->
<div class="row inputContainer rounded">
<input class="col-auto" type="newTask" placeholder="Enter new Task" id="addTask" />
<button class="col-auto" id="btnAdd">Add</button>
</div>
<!-- List of tasks created -->
<div class="listContainer">
<ul class="taskList rounded" id="taskList"></ul>
</div>
<footer class="row" id="footer">
<h6 class="col w-100">2021</h6>
</footer>
<!-- BOOTSTRAP -->
<script src="https://cdn.jsdelivr.net/npm/#popperjs/core#2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap#5.0.0/dist/js/bootstrap.min.js" integrity="sha384-lpyLfhYuitXl2zRZ5Bn2fqnhNAKOAaM/0Kr9laMspuaMiZfGmfwRNFh8HlMy49eQ" crossorigin="anonymous">
</script>
</body>
</div>
</html>
I also wasn't sure what you intent was with using bootstraps chart. I haven't used it before but from checking the docs it didn't appear you were actually coding appropriatly for it. Also you had a <div> with the class of chart and not a <canvas> which appeared wrong to me (but like I said I haven't used it before). In the example here I changed it to <canvas> and also got rid of the canvas you were creating along with the span.
Hopefully this is what you wanted if not maybe you can still piece together what I have here with what exactly you want.
Hey Carlos and everybody interested in a solution.
After investigating the code I noticed the problem lies in creating the elements span and canvas everytime the function gets invoked but never removed.
The solution to that is to have these elements in place to begin with, namely in the html code || or create them once before the function is called.
As for the variables pendingTasks and completedTasks, I would suggest changing them to pendingTasks and totalAmountOfTasks. (Unless there is a third state in which they can be.)
Then the ratio you would feed into the circle is pendingTasks/totalAmountOfTasks.
Remember to check for dividing by zero, when there are no tasks!
Cheers,
Thomas
I am trying to render two rectangles one on the top of other inside canvas element with globalCompositeOperation set to 'source-out' and globalAlpha set to 0.2. However globalAlpha is not working on Safari and the outer rectangle in getting rendered with opacity 1.
Below is the code.
const el = document.getElementById('canvas');
const ctx = el.getContext('2d');
ctx.globalCompositeOperation = 'source-out';
ctx.beginPath();
ctx.rect(200,200,400,400);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 0.2;
ctx.beginPath();
ctx.rect(0,0,800,800);
ctx.closePath();
ctx.fill();
body {
background-color: white;
margin: 0;
}
.canvas-container {
position: relative;
width: 75%;
height: 0;
margin: auto;
margin-top : 50px;
padding-bottom: 75%;
}
.canvas-container .canvas{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="canvas-container">
This is canvas container
<canvas id="canvas" class="canvas" width="800" height="800"></canvas>
</div>
</body>
</html>
I can definitely reproduce the bug, and being a bug, the only true solution is to report it in the hope it gets fixed.
Now, there are a lot of workarounds available.
The first one is to use a semi-transparent fillStyle rather than globalAlpha:
const el = document.getElementById('canvas');
const ctx = el.getContext('2d');
ctx.beginPath();
ctx.rect(100,100,200,200);
ctx.fill();
ctx.globalCompositeOperation = 'source-out';
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.beginPath();
ctx.rect(0,0,400,400);
ctx.fill();
canvas { border: 1px solid; }
<canvas id="canvas" class="canvas" width="400" height="400"></canvas>
But this may not be really practical, for instance if you have complex gradients or even patterns as fillStyle.
A second workaround is to use the fillRect() method, which is not affected by the bug.
const el = document.getElementById('canvas');
const ctx = el.getContext('2d');
ctx.beginPath();
ctx.rect(100,100,200,200);
ctx.fill();
ctx.globalCompositeOperation = 'source-out';
ctx.globalAlpha = 0.2;
ctx.fillRect(0,0,400,400);
canvas { border: 1px solid; }
<canvas id="canvas" class="canvas" width="400" height="400"></canvas>
But this will obviously only work for rectangles, and not for any other shapes.
So a third workaround, which is probably the best one, consists in drawing the other way: first draw the semi-transparent contour, then the hole:
const el = document.getElementById('canvas');
const ctx = el.getContext('2d');
// first the contour
ctx.globalAlpha = 0.2;
ctx.beginPath();
ctx.rect(0,0,400,400);
ctx.fill();
// now the hole
ctx.globalCompositeOperation = 'destination-out';
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.rect(100,100,200,200);
ctx.fill();
canvas { border: 1px solid; }
<canvas id="canvas" class="canvas" width="400" height="400"></canvas>
But if for some reasons you can't even do that, then a last resort workaround is to use a second canvas on which you'll draw your shape and do the compositing through drawImage(), which isn't affected either by this bug:
const el = document.getElementById('canvas');
const ctx = el.getContext('2d');
const copy_ctx= el.cloneNode().getContext("2d");
ctx.beginPath();
ctx.beginPath();
ctx.rect(100,100,200,200);
ctx.fill();
copy_ctx.beginPath();
copy_ctx.rect(0,0,400,400);
copy_ctx.fill();
// we can set the alpha either here or while drawing
// on copy_ctx, it doesn't change much, both work
ctx.globalAlpha = 0.2;
ctx.globalCompositeOperation = 'source-out';
ctx.drawImage(copy_ctx.canvas,0,0);
canvas { border: 1px solid; }
<canvas id="canvas" class="canvas" width="400" height="400"></canvas>