I am creating a drawing app. I have succeeded to do everything. When I paint the image with a dark color, some white pixels appear at the edges. I tried to change the value
Your check for the outline is simply too strict, marking light gray pixels as ones you cannot color in. I simply tweaked your threshholds:
function matchOutlineColor (r,g,b,a,e) {
return a >= 150;
}
Notice my much higher numbers for the white value and alpha. You could maybe tweak this further but this looks good to me. Here are some before and after photos:
Before (zoomed in 200%)
After (zoomed in 200%)
I suggest two changes:
Filling in horizontal and vertical direction until the sign of the intensity gradient flips from either + to - OR - to + lets us fill the whole area including 'half' of the black border. By inspecting the gradient we just make sure not to overstep the intensity minimum and thus avoid filling a neighboring area.
Have a look at the following demo:
// Get pixel intensity:
function getIntensity(data, i) {
return data[i] + data[i + 1] + data[i + 2];
}
// Set pixel color:
function setColor(data, i, r, g, b) {
data[i] &= r;
data[i + 1] &= g;
data[i + 2] &= b;
}
// Fill a horizontal line:
function fill(x, y, data, width, r, g, b) {
var i_start = y * (width << 2);
var i = i_start + (x << 2);
var i_end = i_start + (width << 2);
var i_intensity = getIntensity(data, i);
// Horizontal line to the right:
var prev_gradient = 0;
var prev_intensity = i_intensity;
for (var j = i; j < i_end; j += 4) {
var intensity = getIntensity(data, j);
gradient = intensity - prev_intensity;
prev_intensity = intensity;
if ((prev_gradient > 0 && gradient < 0) || (prev_gradient < 0 && gradient > 0)) break;
if (gradient != 0) prev_gradient = gradient;
setColor(data, j, 255, 0, 0);
}
// Horizontal line to the left:
prev_gradient = 0;
prev_intensity = i_intensity;
for (var j = i - 4; j > i_start; j -= 4) {
var intensity = getIntensity(data, j);
gradient = intensity - prev_intensity;
prev_intensity = intensity;
if ((prev_gradient > 0 && gradient < 0) || (prev_gradient < 0 && gradient > 0)) break;
if (gradient != 0) prev_gradient = gradient;
setColor(data, j, 255, 0, 0);
}
}
// Demo canvas:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
// Fill horizontal line on click:
canvas.addEventListener('mousedown', event => {
var rect = canvas.getBoundingClientRect();
var x = event.clientX - rect.left | 0;
var y = event.clientY - rect.top | 0;
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
fill(x, y, imageData.data, imageData.width);
context.putImageData(imageData, 0, 0);
});
// Load a sample image:
var image = new Image();
image.addEventListener('load', event => {
context.drawImage(event.target, 0, 0, canvas.width, canvas.height);
});
image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAACCAIAAAABwbG5AAAAA3NCSVQICAjb4U/gAAAAGXRFWHRTb2Z0d2FyZQBnbm9tZS1zY3JlZW5zaG907wO/PgAAACZJREFUCJlj+I8Kbt68mZeXd/PmTbgIw38MsG/fvoKCgqdPn0K4ACAfPGrloJp1AAAAAElFTkSuQmCC';
<canvas id="canvas"></canvas>
You can improve the performance by tracing only one color channel instead of summing all three for your getIntensity
function.
Also, you would need to deal with 'clearing' an already filled area before filling it again with another color, due to the blending mode.
You could e. g. keep a single-channel grayscale image data array of the background image in memory and use it as the source image for your fill algorithm.
It might be even more performant to (manually or automatically) transform all your grayscale images to 1-bit outlines with binary boundaries and use them as the source for your floodfill algorithm, blending the result smoothly with the grayscale image.
To be honest it's not really your drawing program's fault so much as the fault of the images being drawn on. The 'white' pixels are actually pale grey, a side effect of the brush tool used to draw the lines in the images. There are two ways around this:
To remove all pale grey pixels from the image and make them white. Using a color select tool and a pencil tool will fix that. The only side effect is that the lines at some points may seem a bit jerky.
To give some leniency when it comes to what colors get painted over. So, instead of just replacing straight white, replace pale greys as well. Any color up to around #CCC (or rgb(204, 204, 204)) should be colored over.
The code for option two is as follows:
if(r >= 204 && g >= 204 && b >= 204 && r === g && g === b){
return true;
}
This simply checks if the pixel is a light greyscale color and returns true if so. Use this instead of your outline color checking function.
Yes, the "white" spots left out aren't actually white as there's a tiny gradient going on between white and black. Try giving it some leeway around these lines:
if ((r <= curColor.r + 10 && r >= curColor.r - 10) && (r >= curColor.g - 10 && r <= curColor.g + 10) && (b >= curColor.b - 10 && b <= curColor.b + 10)) {
return false;
}
You can modify the 10 factor until it looks good. Just tweak it until it's okay. (Might be bad code , I just woke up but you should get the picture :D )
You could also preprocess the image in a separate buffer and reduce the number of colors. That way it's easier to fill the beginning of gradients, thus reducing or elimitating the undesired effect you are describing.