htmlcsscanvasclip-pathcss-mask

Use canvas as background for overlaying elements using mask


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?


Solution

  • 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>