BBnyc BBnyc - 6 months ago 5x
Javascript Question

Canvas clipping with "feather" edges effect

I'm currently drawing an image to an HTML5 Canvas and masking it with an arc, calling clip() before I draw the image so that only the portion that's in the arc is shown. How can I feather the edges of this arc? I know from googling around that there is no simple way to simply apply a "feather" to a shape drawn with canvas. What abut going in on the pixel data for the image where its edges touch the arc? Thanks for any help.

Here is the relevant portion of my code:

ctx.arc(canvas.width/2, canvas.height/2, 250, 0, 6.28, false);//draw the circle
ctx.drawImage(background, 0, 0,
background.width * scale, background.height * scale);
ctx.clip();//call the clip method so the next render is clipped in last path
ctx.drawImage(img, 0, 0,
img.width * scale, img.height * scale);


Thanks for the thorough answer and very helpful code/comments Ken!! I spent a few hours last night trying to work this solution in my particular use case and I'm having trouble. It seems that if I clip an image with the second-canvas technique you describe I can't redraw it on transforms the same way that I can with an arc() and clip() routine. Here's a JS Fiddle of what I'm trying to accomplis, minus the feathering on the arc, notice the click and drag events on the two layered images.

I tried replacing the arc() with your method, but I'm having a hard time getting that to be responsive to the transforms that happen on mouse events.



You can achieve this by combining the following steps:

  • Use off-screen canvas
  • Use the shadow feature (the secret ingredient)
  • Use composite modes

The concept is based on having the browser make the feather internally by utilizing the blurred shadow. This is much faster than blurring in JavaScript. As we can make shadow for any object you can make complex feathered masks.

The off-screen canvas is used to draw the shadow only. We achieve this by moving the actual shape outside the canvas and then offset the shadow accordingly. The result is that shadow is drawn on the off-screen canvas while the actual shape is "invisible".

Now that we have a feathered version of our shape we can use that as a mask for composite mode. We choose destination-out to cleat where the shadow is drawn, or destination-in to invert the mask.


Lets create a wrapper function that do all the steps for us


function clipArc(ctx, x, y, r, f) { /// context, x, y, radius, feather size

    /// create off-screen temporary canvas where we draw in the shadow
    var temp = document.createElement('canvas'),
        tx = temp.getContext('2d');

    temp.width = ctx.canvas.width;
    temp.height = ctx.canvas.height;

    /// offset the context so shape itself is drawn outside canvas
    tx.translate(-temp.width, 0);

    /// offset the shadow to compensate, draws shadow only on canvas
    tx.shadowOffsetX = temp.width;    
    tx.shadowOffsetY = 0;

    /// black so alpha gets solid
    tx.shadowColor = '#000';

    /// "feather"
    tx.shadowBlur = f;

    /// draw the arc, only the shadow will be inside the context
    tx.arc(x, y, r, 0, 2 * Math.PI);

    /// now punch a hole in main canvas with the blurred shadow;
    ctx.globalCompositeOperation = 'destination-out';
    ctx.drawImage(temp, 0, 0);

That's all there is to it.


clipArc(context, centerX, centerY, radius, featherSize);

With demo background (see fiddle):

ctx.fillStyle = '#ffa';
ctx.fillRect(0, 0, demo.width, demo.height);

clipArc(ctx, 200, 200, 150, 40);


Demo snapshot

If you want to keep center intact just replace composite mode with destination-in.

Demo for inverted feathered mask

demo snapshot inverted mask