BBnyc BBnyc - 7 months ago 13
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.restore();
ctx.save();
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);
ctx.closePath();
ctx.restore();


UPDATE

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.

http://jsfiddle.net/g3WkN/

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.

K3N K3N
Answer

Technique

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.

Example

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

ONLINE DEMO HERE

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);
    tx.closePath();
    tx.fill();

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

That's all there is to it.

USAGE

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);

Result:

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