1

I am manually painting individual pixels in html5 canvas. I'm only using pure black and pure white. Simplifying only somewhat:

var canvas = document.querySelector(".main-canvas");
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;

var isNonNegativeInteger = function(val) {
  return Number.isFinite(val) && val % 1 === 0 && val >= 0;
};

var paintPixel = function(x, y) {
    if (!isNonNegativeInteger(x) ||
        !isNonNegativeInteger(y) ) {
        debugger;       
    }
    ctx.fillRect( x, y, 1, 1 );
};

var clearPixel = function( x, y ) {
    if (!isNonNegativeInteger(x) ||
        !isNonNegativeInteger(y) ) {
        debugger;       
    }
    ctx.clearRect( x, y, 1, 1 );
};

I am only passing integers in as x and y coordinates. Yes, I'm absolutely sure, I've put in a test.

I'm not manipulating RGB. All the pixels stay black. I'm just manipulating alpha. What I am finding is that after a great deal of painting and clearing, with lots of back-and-forth, there is a faint visual "ghosting" where the black pixels that were set to fully opaque by the fillRect() are not actually having their alpha reset to zero when clearRect() is called.

I know this because when I call ctx.getImageData() in the affected pixel neighborhood, I see low but non-zero alpha values (3, 5, or 12 etc) sprinkled in the pixel data alpha values. This is... crazy-making. Interestingly, I also see some non-255 alpha values for the black pixels, which are also problematic but less visually offensive that the "dirty" whites.

These are big pixel grids, with thousands of draw calls per frame. So I am reluctant to use getImageData() and putImageData() to manually set the alpha because they are slow. The problem is just that clearRect() is not doing what it says on the tin.

Any suggestions very appreciated.

Ben
  • 11,082
  • 8
  • 33
  • 47
  • If you are keeping all pixels black anyway, you could use fillRect instead. Do you change the transformation matrix anywhere? Have you tested multiple browsers? – Kenney Oct 22 '15 at 02:07
  • 1
    How can you "erase" with fillRect? Setting ctx.fillStyle to zero alpha just means fills have no effect at all. No transform matrix. Seeing similar results in both FF and Chrome. – Ben Oct 22 '15 at 02:21
  • [Here](http://stackoverflow.com/questions/16776665/canvas-clearrect-with-alpha) is more info on that. – Kenney Oct 22 '15 at 02:26
  • No I'm definitely not advising that. Read the answer below that, and the comments on the question. – Kenney Oct 22 '15 at 02:33
  • @Kaiido I meant this comment: *"You can use context.fillColor = "rgba(0-255, 0-255, 0-255, 0-1)" and use fillRect. The 4th parameter it's the alpha value. 0 is max transparent and 1 is totally opaque. – Gustavo Carvalho"* – Kenney Oct 22 '15 at 02:35
  • @Kenney But as pointed out by OP, `ctx.fillStyle=rgba(0,0,0,0); ctx.fillRect(x,y,1,1)` will only draw a new transparent pixel on what was already drawn at x y coordinates. In other words, it does nothing. *(nb :`ctx.fillColor` doesn't exists)* – Kaiido Oct 22 '15 at 03:03
  • 1
    Two things to chew on. 1. Low alpha values (<15) are usually a sign of the browser doing anti-aliasing. This might mean that your artifacts are being caused by filling neighboring pixels and the browser applying anti-aliasing to the ghost pixel. 2. Or, Unlikely, but if you've done `drawImage` on the canvas, the browser is then allowed to do gamma correction which might affect alpha. Without having code it's difficult to analyze further, but I would strongly suspect you are running afoul of the browsers anti-aliasing algorithm when you repeatedly `fillRect` with 1 pixel width & height. – markE Oct 22 '15 at 04:32
  • but, @markE, if he does only fills and clears at integer values, as OP's stated that he is "absolutely sure" to do, there shouldn't be any anti-aliasing right? Also, according to [its comment on my answer](http://stackoverflow.com/questions/33272027/clearrect-on-a-single-pixel-in-html5-canvas-doesnt-actually-set-alpha-to-0#comment-54346932) using globalCompositeOperation does fix the issue on FF but not on Chrome – Kaiido Oct 22 '15 at 04:41
  • The canvas always runs anti-aliasing so there's always a danger that its anti-aliasing algorithm will glitch -- especially when filling an edge case space like 1x1. Also keep in mind that javascript always internally uses floats so even parseInt will produce a "many zero" float. I'm always willing to be wrong (my passion in life is learning!), but I still strongly suspect that anti-aliasing will be the cause of ghost pixels with alpha<15. – markE Oct 22 '15 at 04:51
  • Agreed but still, I can't repro : http://jsfiddle.net/myr36qsh/ – Kaiido Oct 22 '15 at 04:59
  • Yep, repro will likely require both more code from the OP and more info about how the code is being used when the problem occurs. :-) – markE Oct 22 '15 at 05:02
  • @markE I am having trouble creating a reduced test case. The problem seems to only be visible at scale. I am only painting and clearing through those two functions, though, so the drawing code that calls them shouldn't really matter as far as aliasing. I have a feeling this might just be a bug, the byproduct of some internal performance optimization in Blink. – Ben Oct 22 '15 at 16:09
  • @Ben, what do you mean by "at scale"? Are you playing with ctx matrix? Also, have you got a non reduced test case ? If you are only using those two functions, that shouldn't be too hard to repro. – Kaiido Oct 24 '15 at 04:05
  • @Kaiido No, I am not playing with ctx matrix. I am setting CSS scale transforms on the canvas element, but the ghosting persists when the CSS transform scale changes. By "scale" I meant I can't get the problem to show on small canvases. Only in certain spots on large canvases. I got around the problem by double buffering and calling clearRect() on the entire (offscreen) canvas for each frame, but it's slower and I'm not thrilled about it. – Ben Nov 01 '15 at 20:09
  • @Ben, you will have to give us a live example that we can repro... Here I made [an other snippet](http://codepen.io/anon/pen/KdBPwO), with that css information added, but I still can't reproduce your issue. – Kaiido Nov 02 '15 at 00:45

1 Answers1

0

That's really odd, the only cause I can think of would be that your x and y coordinate are not int. Drawing at float position will indeed create an antialiasing artifact, on clearRect too :

var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
ctx.textBaseline = "middle"
var paintPixel = function(x, y) {
  ctx.fillRect(x, y, 1, 1);
};
var clearPixel = function(x, y) {
  ctx.clearRect(x, y, 1, 1);
};
var i = 0;
for (i = 0; i < 25; i += .1) {
  paintPixel(i, 10);
  clearPixel(i, 10);
}
ctx.fillText('float', 30, 10)

for (i = 0; i < 25; i++) {
  paintPixel(i, 30);
  clearPixel(i, 30);
}
ctx.fillText('int', 30, 30)
// an other solution could have been to use globalCompositeOperation
var gCOClear = function(x, y) {
  ctx.globalCompositeOperation = 'destination-out';
  ctx.fillRect(x, y, 1, 1);
  ctx.globalCompositeOperation = 'source-over';
}
for (i = 0; i < 25; i += .1) {
  paintPixel(i, 50);
  gCOClear(i, 50);
}
ctx.fillText('gCO float', 30, 50)
for (i = 0; i < 25; i++) {
  paintPixel(i, 70);
  gCOClear(i, 70);
}
ctx.fillText('gCO int', 30, 70)

So the solutions are : either you round your coordinates before drawing, either your clear slightly bigger than you drawn.

But as you specified that you are absolutely sure to be "only passing integers in as x and y coordinates", this is probably not your issue...

Also, note that sadly, ctx.imageSmoothingEnabled has no effect on drawing methods like fillRect() and clearRect()

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 1
    Thanks, but I meant it when I said I'm sure that the arguments are integers. :) I've edited the question to hopefully clarify. If I change it to "erasing" by painting with opaque white, or using `ctx.globalCompositeOperation = 'destination-out'` both fix the ghosting in FF, but I am still getting it in Blink. – Ben Oct 22 '15 at 03:48
  • @Ben Oups missed that line :-/ I'll let the answer though as it may have some interest for future readers. Can you add a live example? That seems odd that gCO works and not `clearRect` and even more that it works only on FF. – Kaiido Oct 22 '15 at 03:59