I have a full-screen background canvas, I want to draw elements on top of that canvas that act as a mask. So the canvas should only be visible through the overlaying elements.
Here's a picture on what I'm trying to achieve:
In the left picture is what I currently have, where the gradient background acts as the canvas element. The picture on the right is what I attempt to achieve, where the 2 divs (mask, mask 2). In addition, the 2 elements (mask, mask 2) should be draggable, so if the element moves, the background will still match up with what the canvas is showing.
I tried to do this using mask, a clip-path, but I can't seem to get it working. What's the best approach to get the desired result?
The easiest is probably to use CSS to mask your output <canvas>
.
You can use the mask
property with one linear-gradient
as mask-image
per element to do so.
const canvas = document.querySelector("canvas");
const elements = [...document.querySelectorAll(".resizeable")];
// Set one mask image per element (separated by a `,`)
const image = elements.map(() => `linear-gradient(black, black)`).join(",")
canvas.style.setProperty("-webkit-mask-image", image);
canvas.style.setProperty("mask-image", image);
const updateMask = () => {
// Get the updated bounding rect of every elements
const rects = elements.map((el) => el.getBoundingClientRect());
// Build the position & size list values
const position = rects.map(({left, top}) => `${left}px ${top}px`).join(",");
const size = rects.map(({width, height}) => `${width}px ${height}px`).join(",");
// Update the mask properties
canvas.style.setProperty("-webkit-mask-position", position);
canvas.style.setProperty("mask-position", position);
canvas.style.setProperty("-webkit-mask-size", size);
canvas.style.setProperty("mask-size", size);
};
// Handle resizing of our elements
const observer = new ResizeObserver(() => updateMask());
elements.forEach((el) => observer.observe(el));
// Handle dragging of our elements
let dragged = null;
const offset = { x: 0, y: 0 };
document.addEventListener("mousedown", (evt) => {
if (evt.target.matches(".resizeable")) {
return;
}
dragged = evt.target.closest(".resizeable");
offset.x = evt.offsetX;
offset.y = evt.offsetY;
if (dragged) {
evt.preventDefault();
}
});
document.addEventListener("mousemove", (evt) => {
if (dragged) {
dragged.style.setProperty("--left", evt.clientX - offset.x);
dragged.style.setProperty("--top", evt.clientY - offset.y);
updateMask();
}
});
document.addEventListener("mouseup", (evt) => {
dragged = null;
});
document.addEventListener("scroll", updateMask);
// Render some noise on the canvas
const ctx = canvas.getContext("2d");
const img = new ImageData(1000, 1000);
const data = new Uint32Array(img.data.buffer);
ctx.fillStyle = ctx.createLinearGradient(0, 0, 1000, 0);
ctx.fillStyle.addColorStop(0, "orange");
ctx.fillStyle.addColorStop(0.2, "blue");
ctx.fillStyle.addColorStop(0.4, "green");
ctx.fillStyle.addColorStop(0.6, "orange");
ctx.fillStyle.addColorStop(0.8, "#F0F");
ctx.globalCompositeOperation = "color";
const anim = () => {
for (let i = 0; i<data.length; i++) {
data[i] = (Math.random() * 0xFFFFFF) + 0xFF000000;
}
ctx.putImageData(img, 0, 0);
ctx.fillRect(0, 0, 1000, 1000);
requestAnimationFrame(anim);
};
requestAnimationFrame(anim);
.resizeable {
position: absolute;
border: 1px solid;
overflow: hidden;
resize: both;
width: 300px;
height: 250px;
left: calc(var(--left) * 1px);
top: calc(var(--top) * 1px);
}
.resizeable:nth-of-type(2) {
--left: 350;
--top: 120;
}
.draggable {
width: 100%;
height: 100%;
}
canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
/* Do not forget to disable the mask-repeat */
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
}
<canvas width=1000 height=1000></canvas>
<div class=resizeable><div class=draggable>foo bar</div></div>
<div class=resizeable><div class=draggable>baz bla</div></div>