1

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);
  • Without an example source, this is a theoretical question here. Any suggestion would only be guess. Please provide some source examples so we can help. – Jan Franta Jul 05 '17 at 07:35
  • OK, I figured that was pretty well stated. Here's a very simple program that demonstrates the problem. It does what I indicated above. Well, I'm trying but apparently you can't enter a new line here because it just saves the post. I'll post the code in a subsequent post. – Dean Roddey Jul 05 '17 at 16:42
  • Couldn't do that either, so I updated the original post – Dean Roddey Jul 05 '17 at 16:46
  • Well, in the process of posting the code, I discovered something. I had the usual translate(0.5, 0.5) on the canvas, to get clean lines and text. That's what is causing it. If I remove that, the image blit fills the whole clip area. If the translation is there, the image blit leaves a border around the image. So this sort of raises the question as to why that is. So I guess I either turn translate() off and on all the time, or I just increase the clip area when drawing images by one. Either of those things work, but both sort of seem inconsistent. – Dean Roddey Jul 05 '17 at 16:49
  • So I guess the line that is being left unfilled is actually half a pixel, and that seems to be true. It's quite thin. Anyway, something seems inconsistent. Why would the translate cause the image draw to leave a border around it will color fills don't? Either way, you have to do a lot of hacky work-arounds to set the right things for the right types of primitives, and if you are doing both in a given method, you'll have to switch back and forth. That's not in the spirit of minimizing canvas state changes. And why isn't the line just on the top/left or bottom right? It's all the way around. – Dean Roddey Jul 05 '17 at 16:55

1 Answers1

1

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" />
AndreaBogazzi
  • 14,323
  • 3
  • 38
  • 63
  • But I was doing translate(0.5, 0.5). If the translation applies to everything, and I'd think it should, then why wouldn't that have made the edges of the clip path fall onto actual pixel boundaries? Why would having them be at the wierd half pixel boundaries make the line go away? You'd think that having the translation would make the line go away, right? And why doesn't a color fill get affected the same way? Anyway, it's a fairly sub-optimal design, IMO, if it indeed is as designed, since it makes things operate differently for images vs everything else. – Dean Roddey Jul 06 '17 at 18:10
  • the fillrect is affected also. what you see is the fill black on half pixel. is hard yo explain good, i ll add more detail but i m nearly sure – AndreaBogazzi Jul 07 '17 at 08:52
  • OK, yeh, it does affect the fill as well. if you do: 1. Fill whole area with back 2. Fill 10, 10, 200, 200 with red 3. Set a clip on 10, 10, 200, 200 4. Do a fill of black on that same area You end up with a red line around the black, because the last fill didn't fill the whole area. IMO, that sucks and is a bad decision, as was the whole thing about not having pixels be on pixel boundaries, which I find to be totally counter-productive. I would also consider any anti-aliasing without my permission to be a bad decision as well. At least there are ways around it. – Dean Roddey Jul 08 '17 at 01:59
  • The point is that there are no specs about anti aliasing. working without would be terrible. imagine lines would look very bad. Canvas is not tha low level for you to decide. if you want to avoid half pixels you have to count then – AndreaBogazzi Jul 08 '17 at 10:28
  • Square operations (fills or images) on pixel boundaries should never require anti-aliasing. The entire world survived like that for the history of computing. A rectangular fill on pixel boundaries should therefore never be affected. An image with alpha transparency includes its own anti-aliasing and should never be affected. It's just bad design in my opinion. – Dean Roddey Jul 13 '17 at 22:09
  • canvas does not unlimited have pixels. fillRect does not take integers as arguments but takes floats also. You can rotate canvas before creating a fillRect. So what color would you give to the first pixel of a red filled rect that starts at 0.4, 0.4? Probably a red with 0.36 of opacity. That is an anti aliasing logic. Canvas would look terribly without. – AndreaBogazzi Jul 14 '17 at 18:57
  • But it can clearly sense when it's doing a rectangular operation on actual pixel boundaries and, like most other graphics systems out there, render exactly what is given. If we had to deal with every graphics operation potentially being fuzzy or modified in standard OS graphics systems it would be a mess. – Dean Roddey Jul 15 '17 at 20:12
  • i think generally you want a rect wide 10 at 0.5 to look different from one at 0.2 – AndreaBogazzi Jul 16 '17 at 07:46
  • But these are all on pixel boundaries, hence the translate(0,5, 0.5) and no other transformations. Any use of the canvas for UI type stuff is probably going to be just like it is in any other UI system, with rectangular operations on pixel boundaries. If you can't create a UI without lots of anti-aliasing artifacts if you try to use clipping, then the canvas becomes useless for a lot of things. And I'm still struggling to avoid artifacts. A game that is redrawing the whole screen every time wouldn't have such problems. – Dean Roddey Jul 22 '17 at 22:43
  • The conversation is going off topic but you can totally reset transform before clipping. You can clip on boundaries if you play with easy transforms and you know where your objects are with precisions. ctx.save, neutral transform, ctx.rect, ctx.clip. ctx.restore. You are back to the 0.5 translation but you clipped without. Is just a matter of what are you doing. – AndreaBogazzi Jul 23 '17 at 10:03
  • I just punted altogether. I draw onto a background canvas, and never set any clipping. At the end of the current drawing cycle, I blit the area that was to be updated to the main canvas. Since no clipping was ever done, it creates a clean result. ANd since I just blit the update area, that effectively clips the output. A little more overhead perhaps, but well worth it to just avoid this whole issue. – Dean Roddey Jul 26 '17 at 20:19
  • If you could at least upvote the answer i will win my bronze badge of canvas :D – AndreaBogazzi Jul 27 '17 at 11:20