htmlcanvasclippingimage-clipping

Are HTML canvas clip paths inclusive or exclusive?


I've been working on a Typescript based touch screen client for our CQC home automation platform, and ran across something odd. There are lots of places where various graphical elements are layered over images. When it's time to update some area of the screen, I set a clip area and update.

But I always ended up with a line around everything, which was the color of the underlying color fill behind the image. I of course blamed myself. But, in the end, instead of committing suicide, I did a little test program.

It seems to indicate that drawImage() does NOT include the clip path boundary, while a color fill does. So blitting over the part of the images that underlies the area I'm updating doesn't completely fill the target area, leaving a line around the area.

After that simple program demonstrated the problem, I went back and for image updates I inflated the clip area by one, but left it alone for everything else, and now it's all working. I tested this in Chrome and Edge, just to make sure it wasn't some bug, and they both act exactly the same.

Strangely, I've never see any statement in the docs about whether clip paths are intended to be exclusive or inclusive of the boundary, but surely it shouldn't be one way for one type of primitive and another way for others, right?

function drawRect(ctx, x, y, w, h) {
    ctx.moveTo(x, y);
    ctx.lineTo(x + w, y);
    ctx.lineTo(x + w, y + h);
    ctx.lineTo(x, y + h);
    ctx.lineTo(x, y);
}

function init()
{
    var canvas = document.getElementById("output");
    canvas.style.width = 480;
    canvas.style.height = 480;
    canvas.width = 480;
    canvas.height = 480;

    var drawCtx = canvas.getContext("2d");
    drawCtx.translate(0.5, 0.5);

    var img = new Image();
    img.src = "test.jpg";
    img.onload = function() {

        // DRaw the image
        drawCtx.drawImage(img, 0, 0);

        // SEt a clip path
        drawCtx.beginPath();
        drawRect(drawCtx, 10, 10, 200, 200);
        drawCtx.clip();

        // Fill the whole area, which fills the clip area
        drawCtx.fillStyle = "black";
        drawCtx.fillRect(0, 0, 480, 480);

        // Draw the image again, which should fill the area
        drawCtx.drawImage(img, 0, 0);

        // But it ends up with a black line around it
    }
}

window.addEventListener("load", init, false);

Solution

  • I think they behave same.

    Clip region are not inclusive of the border, but they can use anti aliasing.

    Chrome was not using this techinque and was giving jagged lines on clipping. ( probably they changed recently ).

    The thin black border is the side effect of a compositing operation. The clip region is across a pixel. so the fillRect will draw black everywhere, but the border will be 50% black and 50% transparent, compositing with the first image draw.

    The second draw image get clpped, at the border with 50% opacity to simulate the half pixel. at this point at the clip border you have:

    image 100% black fill 50% image 50%

    This will make a small dark border appear.

    function drawRect(ctx, x, y, w, h) {
        ctx.moveTo(x, y);
        ctx.lineTo(x, y + h);
        ctx.lineTo(x + w, y + h);
        ctx.lineTo(x + w, y);
        ctx.closePath();
    }
    
    function init()
    {
        var canvas = document.getElementById("output");
        canvas.style.width = 480;
        canvas.style.height = 480;
        canvas.width = 480;
        canvas.height = 480;
    
        var drawCtx = canvas.getContext("2d");
        drawCtx.translate(0.5, 0.5);
    
        var img = new Image();
        img.src = "http://fabricjs.com/assets/printio.png";
        img.onload = function() {
    
            // DRaw the image
            drawCtx.drawImage(img, 0, 0);
    
            // SEt a clip path
            drawCtx.beginPath();
            drawRect(drawCtx, 10, 10, 200, 200);
            drawCtx.clip();
    
            // Fill the whole area, which fills the clip area
            drawCtx.fillStyle = "black";
            drawCtx.fillRect(0, 0, 480, 480);
    
            // Draw the image again, which should fill the area
            drawCtx.drawImage(img, 0, 0);
    
            // But it ends up with a black line around it
        }
    }
    
    init();
    <canvas id="output" />