javascriptcanvasclip

Clip a canvas using a polygon and a blured circle


I try to clip an canvas using to different shape : a polygon (saying a star) and a blured circle (its edges fade-out from transparent to partially opaque).

Here is a sample of the code to make this clip but I'm totally unable to create the "blur effect" around the circle.

      const canvas = document.getElementById("canvas");
      const ctx = canvas.getContext("2d");

      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const img = new Image();
      img.src = "https://picsum.photos/800/600";
      img.onload = function () {
        let mouseX = canvas.width / 2;
        let mouseY = canvas.height / 2;
        const radius = 70;

        function draw() {
          ctx.save();

          ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
          ctx.fillRect(0, 0, canvas.width, canvas.height);

          const starPoints = createStar(5, mouseX, mouseY, 100, 50);
          ctx.beginPath();
          ctx.moveTo(starPoints[0].x, starPoints[0].y);
          for (let i = 1; i < starPoints.length; i++) {
          ctx.lineTo(starPoints[i].x, starPoints[i].y);
          }
          ctx.closePath();
          ctx.clip();

          const gradient = ctx.createRadialGradient(
            mouseX,
            mouseY,
            radius * 0.8,
            mouseX,
            mouseY,
            radius
          );
          gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
          gradient.addColorStop(1, "rgba(0, 0, 0, 0.7)");

          ctx.fillStyle = gradient;
          ctx.beginPath();
          ctx.arc(mouseX, mouseY, radius, 0, Math.PI * 2);
          ctx.fill();

          ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

          ctx.restore();
        }

        function createStar(points, cx, cy, outerRadius, innerRadius) {
          const result = [];
          const angleStep = Math.PI / points;
          for (let i = 0; i < 2 * points; i++) {
            const angle = i * angleStep - Math.PI / 2;
            const radius = i % 2 === 0 ? outerRadius : innerRadius;
            result.push({
              x: cx + radius * Math.cos(angle),
              y: cy + radius * Math.sin(angle),
            });
          }
          return result;
        }

        canvas.addEventListener("mousemove", function (event) {
          mouseX = event.clientX;
          mouseY = event.clientY;
          draw();
        });

        draw();
      };
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
      canvas {
        display: block;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
  </body>
</html>
I need to keep star edges sharp, but the circle edge fading-out


Solution

  • Use compositing instead of clipping, the latter uses a path and disregards transparency, it thus can't do antialiasing, let alone blur. The former on the other hand does work directly with transparency.

    But, since you are using some kind of semi-transparency covering to create a fade effect, you'll need another layer where you'll do the compositing first.
    For this, you can simply use another <canvas>, or an OffscreenCanvas. And since we're at using layers, we can even create one for the blurred star, avoiding tracing a new one + blur at every frame.

    // Prepare the mask:
    const mask = new OffscreenCanvas(200, 200);
    {
      const maskCtx = mask.getContext("2d");
      // Edited so that it returns an SVGPath string
      const createStar = (points, cx, cy, outerRadius, innerRadius) => {
        const result = [];
        const angleStep = Math.PI / points;
        for (let i = 0; i < 2 * points; i++) {
          const angle = i * angleStep - Math.PI / 2;
          const radius = i % 2 === 0 ? outerRadius : innerRadius;
          const x = cx + radius * Math.cos(angle);
          const y = cy + radius * Math.sin(angle);
          result.push(`${x},${y}`);
        }
        return `M${result.join("L")}`;
      }
      const starPath = new Path2D(createStar(5, 100, 100, 100, 50));
      maskCtx.fill(starPath);
      // We'll make the blur through a filter,
      // you can keep your gradient if you prefer
      maskCtx.filter = "blur(5px)";
      maskCtx.arc(100, 100, 80, 0, Math.PI * 2);
      maskCtx.globalCompositeOperation = "destination-in";
      maskCtx.fill();
    }
    
    // Where we'll apply the mask over the image
    const clippingCanvas = new OffscreenCanvas(innerWidth, innerHeight);
    const updateClip = (mouseX, mouseY) => {
      const ctx = clippingCanvas.getContext("2d");
      if ( // same size as target canvas
        clippingCanvas.width !== canvas.width ||
        clippingCanvas.height !== canvas.height) {
        clippingCanvas.width = canvas.width;
        clippingCanvas.height = canvas.height;
      }
      else {
        ctx.globalCompositeOperation = "source-over";
        ctx.clearRect(0, 0, canvas.width, canvas.height);
      }
      ctx.drawImage(mask, mouseX - mask.width / 2, mouseY - mask.height / 2);
      ctx.globalCompositeOperation = "source-in";
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    }
    
    // Now the visible canvas
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    
    const img = new Image();
    img.src = "https://picsum.photos/800/600";
    img.onload = function () {
    
      let mouseX = canvas.width / 2;
      let mouseY = canvas.height / 2;
      const radius = 70;
    
      function draw() {
        // fade
        ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        updateClip(mouseX, mouseY);
        ctx.drawImage(clippingCanvas, 0, 0);
      }
    
      canvas.addEventListener("mousemove", function (event) {
        mouseX = event.clientX;
        mouseY = event.clientY;
        draw();
      });
    
      draw();
    };
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <style>
          body {
            margin: 0;
            overflow: hidden;
          }
          canvas {
            display: block;
          }
        </style>
      </head>
      <body>
        <canvas id="canvas"></canvas>
      </body>
    </html>