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 of r + g + b and also alpha but no use. Can any one help me out? You can check the live site from here. Anyone who will help me, I shall grant 50 bounty to him/her. Thanks. This is my code.
<script type="text/javascript">
var initial = screen.width - 50;
if (initial < 1000) {
initial = 1000;
}
var firsttime = null;
var colorYellow = {
r: 255,
g: 207,
b: 51
};
var context;
var canvasWidth = initial;
var canvasHeight = initial;
var myColor = colorYellow;
var curColor = myColor;
var outlineImage = new Image();
var backgroundImage = new Image();
var drawingAreaX = 0;
var drawingAreaY = 0;
var drawingAreaWidth = initial;
var drawingAreaHeight = initial;
var colorLayerData;
var outlineLayerData;
var totalLoadResources = 2;
var curLoadResNum = 0;
var undoarr = new Array();
var redoarr = new Array();
function history(command) { // handles undo/redo button events.
var data;
if (command === "redo") {
data = historyManager.redo(); // get data for redo
} else
if (command === "undo") {
data = historyManager.undo(); // get data for undo
}
if (data !== undefined) { // if data has been found
setColorLayer(data); // set the data
}
}
// sets colour layer and creates copy into colorLayerData
function setColorLayer(data) {
context.putImageData(data, 0, 0);
colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
}
// Clears the canvas.
function clearCanvas() {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
}
// Draw the elements on the canvas
function redraw() {
uc = 0;
rc = 0;
var locX,
locY;
// Make sure required resources are loaded before redrawing
if (curLoadResNum < totalLoadResources) {
return; // To check if images are loaded successfully or not.
}
clearCanvas();
// Draw the current state of the color layer to the canvas
context.putImageData(colorLayerData, 0, 0);
historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
redoarr = new Array();
// Draw the background
context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
// Draw the outline image on top of everything. We could move this to a separate
// canvas so we did not have to redraw this everyime.
context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
};
function matchOutlineColor(r, g, b, a) {
return (r + g + b < 50 && a >= 50);
};
function matchStartColor(pixelPos, startR, startG, startB) {
var r = outlineLayerData.data[pixelPos],
g = outlineLayerData.data[pixelPos + 1],
b = outlineLayerData.data[pixelPos + 2],
a = outlineLayerData.data[pixelPos + 3];
// If current pixel of the outline image is black
if (matchOutlineColor(r, g, b, a)) {
return false;
}
r = colorLayerData.data[pixelPos];
g = colorLayerData.data[pixelPos + 1];
b = colorLayerData.data[pixelPos + 2];
// If the current pixel matches the clicked color
if (r === startR && g === startG && b === startB) {
return true;
}
// If current pixel matches the new color
if (r === curColor.r && g === curColor.g && b === curColor.b) {
return false;
}
return true;
};
function colorPixel(pixelPos, r, g, b, a) {
colorLayerData.data[pixelPos] = r;
colorLayerData.data[pixelPos + 1] = g;
colorLayerData.data[pixelPos + 2] = b;
colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
};
function floodFill(startX, startY, startR, startG, startB) {
document.getElementById('color-lib-1').style.display = "none";
document.getElementById('color-lib-2').style.display = "none";
document.getElementById('color-lib-3').style.display = "none";
document.getElementById('color-lib-4').style.display = "none";
document.getElementById('color-lib-5').style.display = "none";
change = false;
var newPos,
x,
y,
pixelPos,
reachLeft,
reachRight,
drawingBoundLeft = drawingAreaX,
drawingBoundTop = drawingAreaY,
drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
pixelStack = [
[startX, startY]
];
while (pixelStack.length) {
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
// Get current pixel position
pixelPos = (y * canvasWidth + x) * 4;
// Go up as long as the color matches and are inside the canvas
while (y >= drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) {
y -= 1;
pixelPos -= canvasWidth * 4;
}
pixelPos += canvasWidth * 4;
y += 1;
reachLeft = false;
reachRight = false;
// Go down as long as the color matches and in inside the canvas
while (y <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
y += 1;
colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);
if (x > drawingBoundLeft) {
if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
if (!reachLeft) {
// Add pixel to stack
pixelStack.push([x - 1, y]);
reachLeft = true;
}
} else if (reachLeft) {
reachLeft = false;
}
}
if (x < drawingBoundRight) {
if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
if (!reachRight) {
// Add pixel to stack
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
};
// Start painting with paint bucket tool starting from pixel specified by startX and startY
function paintAt(startX, startY) {
var pixelPos = (startY * canvasWidth + startX) * 4,
r = colorLayerData.data[pixelPos],
g = colorLayerData.data[pixelPos + 1],
b = colorLayerData.data[pixelPos + 2],
a = colorLayerData.data[pixelPos + 3];
if (r === curColor.r && g === curColor.g && b === curColor.b) {
// Return because trying to fill with the same color
return;
}
if (matchOutlineColor(r, g, b, a)) {
// Return because clicked outline
return;
}
floodFill(startX, startY, r, g, b);
redraw();
};
// Add mouse event listeners to the canvas
function createMouseEvents() {
$('#canvas').mousedown(function(e) {
// Mouse down location
var mouseX = e.pageX - this.offsetLeft,
mouseY = e.pageY - this.offsetTop;
// assuming that the mouseX and mouseY are the mouse coords.
if (this.style.width) { // make sure there is a width in the style
// (assumes if width is there then height will be too
var w = Number(this.style.width.replace("px", "")); // warning this will not work if size is not in pixels
var h = Number(this.style.height.replace("px", "")); // convert the height to a number
var pixelW = this.width; // get the canvas resolution
var pixelH = this.height;
mouseX = Math.floor((mouseX / w) * pixelW); // convert the mouse coords to pixel coords
mouseY = Math.floor((mouseY / h) * pixelH);
}
if ((mouseY > drawingAreaY && mouseY < drawingAreaY + drawingAreaHeight) && (mouseX <= drawingAreaX + drawingAreaWidth)) {
paintAt(mouseX, mouseY);
}
});
};
resourceLoaded = function() {
curLoadResNum += 1;
//if (curLoadResNum === totalLoadResources) {
createMouseEvents();
redraw();
//}
};
var historyManager = (function() { // Anon for private (closure) scope
var uBuffer = []; // this is undo buff
var rBuffer = []; // this is redo buff
var currentState = undefined; // this holds the current history state
var undoElement = undefined;
var redoElement = undefined;
var manager = {
UI: { // UI interface just for disable and enabling redo undo buttons
assignUndoButton: function(element) {
undoElement = element;
this.update();
},
assignRedoButton: function(element) {
redoElement = element;
this.update();
},
update: function() {
if (redoElement !== undefined) {
redoElement.disabled = (rBuffer.length === 0);
}
if (undoElement !== undefined) {
undoElement.disabled = (uBuffer.length === 0);
}
}
},
reset: function() {
uBuffer.length = 0;
rBuffer.length = 0;
currentState = undefined;
this.UI.update();
},
push: function(data) {
if (currentState !== undefined) {
var same = true
for (i = 0; i < data.data.length; i++) {
if (data.data[i] !== currentState.data[i]) {
same = false;
break;
}
}
if (same) {
return;
}
}
if (currentState !== undefined) {
uBuffer.push(currentState);
}
currentState = data;
rBuffer.length = 0;
this.UI.update();
},
undo: function() {
if (uBuffer.length > 0) {
if (currentState !== undefined) {
rBuffer.push(currentState);
}
currentState = uBuffer.pop();
}
this.UI.update();
return currentState; // return data or unfefined
},
redo: function() {
if (rBuffer.length > 0) {
if (currentState !== undefined) {
uBuffer.push(currentState);
}
currentState = rBuffer.pop();
}
this.UI.update();
return currentState;
},
}
return manager;
})();
function start() {
var canvas = document.createElement('canvas');
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
canvas.setAttribute('id', 'canvas');
document.getElementById('canvasDiv').appendChild(canvas);
if (typeof G_vmlCanvasManager !== "undefined") {
canvas = G_vmlCanvasManager.initElement(canvas);
}
context = canvas.getContext("2d");
backgroundImage.onload = resourceLoaded();
backgroundImage.src = "images/t.png";
outlineImage.onload = function() {
context.drawImage(outlineImage, drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);
try {
outlineLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
} catch (ex) {
window.alert("Application cannot be run locally. Please run on a server.");
return;
}
clearCanvas();
colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
resourceLoaded();
};
outlineImage.src = "images/products/<?php echo $product['product_image']; ?>";
};
if (historyManager !== undefined) {
// only for visual feedback and not required for the history manager to function.
historyManager.UI.assignUndoButton(document.querySelector("#undo-button"));
historyManager.UI.assignRedoButton(document.querySelector("#redo-button"));
}
getColor = function() {
};
</script>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With