Reputation: 906
I am making a Venn diagram based game and have implemented a flood fill algorithm to fill each region that looks like the below.
As I don't want to be able to flood fill the outline of the circles I have a check that ensures that if if the edge is clicked on then it isn't filled. This is done by checking that the colour is not red.
function circleEdgeClicked(x, y) {
var colorLayer = context.getImageData(0, 0, canvasWidth, canvasHeight);
var pixelPos = (x * canvasWidth + y) * 4;
startR = colorLayer.data[pixelPos];
startG = colorLayer.data[pixelPos + 1];
startB = colorLayer.data[pixelPos + 2];
if (startR === 255 && startG === 0 && startB === 0) {
console.log("Colour is red");
}
return (startR === 255 && startG === 0 && startB === 0);
}
The desired behavior is a click will flood fill to yellow and another click will revert by flood filling with white. toHowever, sometimes when clicking inside the circle, either when it is yellow or white the area does not fill to the opposite color. Debugging the cause has shown the issue is because getImageData is incorrectly saying that the pixel clicked on is red.
Here is the code in a JSFiddle where the bug can be replicated by just clicking in the circles and after a few tries it will not fill and the logs will say that the colour is red even though it isn't.
In the getImageData documentation there is this note
Due to the lossy nature of converting to and from premultiplied alpha color values, pixels that have just been set using putImageData() might be returned to an equivalent getImageData() as different values.
Does this have anything to do with it and is there a workaround?
Upvotes: 3
Views: 1287
Reputation:
Change this line from:
var pixelPos = (x * canvasWidth + y) * 4;
to
var pixelPos = (y * canvasWidth + x) * 4; // or use <<2 instead of * 4
so y
(representing line/stride) and not x
is multiplied with width.
It would also be faster to either cache the bitmap beforehand, or read a single pixel:
var colorLayer = context.getImageData(x, y, 1, 1);
You may want to do boundary checks for x/y vs. width/height in this case. You can also read it as 32-bit value and compare 0xff0000ff (ABGR) directly assuming there is no alpha, or shift it over removing the alpha:
function circleEdgeClicked(x, y) {
var color = new Uint32Array(context.getImageData(x, y, 1, 1).data.buffer)[0];
return (color<<8) === 0xff00; // is red? ignoring alpha
}
An additional boost will be to grab the bitmap when the red outline is initialized. Since we will prevent filling on top of those you can cache the bitmap in this state and check without transferring a new bitmap each time a pixel needs to be checked (and there is not much memory overhead as the Uint32 view will share the buffer of the original bitmap):
// by here the red outlines are drawn, so we grab it at this stage
var pixels =
new Uint32Array(context.getImageData(0, 0, canvasWidth, canvasHeight).data.buffer);
// then reuse the buffer when a pixel needs to be checked
function circleEdgeClicked(x, y) {
var color = pixels[y * canvasWidth + x]; // all values are 32-bit
return (color<<8) === 0xff00; // is red? ignoring alpha
}
About lossy nature: there is that issue, which in some cases which is caused by rounding errors. On top of that, if the source drawing is drawn from an image the bitmap values can be altered by gamma or ICC profiles when loaded.
The workaround is to implement a tolerance, for example for red, accept the value to be in the range of 250-255 instead of exactly 255, blue and green to have a range 0-5 etc. (+/- 5 is arbitrary here, just experiment to find an optimal value). You will also discover that the alpha will need to be considered if the base drawing has a non-opaque background.
Upvotes: 2
Reputation: 1389
I'm not quite sure, but since pixel data is one dimensional array, in order to transform x, y into an index of an array in pixel data, you have to use
(x + canvasWidth * y) * 4
Because y position will represent how many lines of horizontal pixels you have to skip to get to the pixel, and that is going to be canvasWidth * y, and then + x for horizontal axis.
Above is just to fix your error, but my suggestion and question is, why don't you just use getImageData straight ahead? getImageData() takes x, y, width and height arguments, so you are able to extract a single pixel just by it's position.
function circleEdgeClicked(x, y) {
// will return an array of 4 integers (rgba)
var colorLayer = context.getImageData(x, y, 1, 1);
startR = colorLayer.data[0];
startG = colorLayer.data[1];
startB = colorLayer.data[2];
if (startR === 255 && startG === 0 && startB === 0) {
console.log("Colour is red");
}
return (startR === 255 && startG === 0 && startB === 0);
}
Upvotes: 4