Use canvas as background for overlaying elements using mask

84 Views Asked by At

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: enter image description here

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?

1

There are 1 best solutions below

0
On BEST ANSWER

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>