I Just made a little pixel art app and I ran into a problem while I was trying to fix the eraser tool. When I try to erase with the color ‘rgba(0, 0, 0, 0)’ it does nothing, so I am forced to settle with the color white. Do you guys know what I might be doing wrong? (I’m also having another issue so I will ask about that when this one is answered) Here is the project And below is the code for the project:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var pixelSize = 10;
var color = "#000000";
var eraser = false;
var fillStack = [];
var fillButton = document.getElementById("fill");
fillButton.addEventListener("click", function() {
processFillStack();
});
var downloadButton = document.getElementById("download");
downloadButton.addEventListener("click", function() {
var dataURL = canvas.toDataURL("image/png");
var link = document.createElement("a");
link.setAttribute("href", dataURL);
link.setAttribute("download", "pixel-art.png");
link.click();
});
var widthInput = document.getElementById("width");
var heightInput = document.getElementById("height");
var resizeButton = document.getElementById("resize");
resizeButton.addEventListener("click", function() {
canvas.width = widthInput.value;
canvas.height = heightInput.value;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
document.getElementById("eraser").addEventListener("change", function() {
eraser = this.checked;
});
canvas.addEventListener("mousedown", function(e) {
if (!fillButton.pressed) {
var x = Math.floor(e.offsetX / pixelSize);
var y = Math.floor(e.offsetY / pixelSize);
if (eraser) {
ctx.fillStyle = "rgba(255,255,255,.1)";
} else {
ctx.fillStyle = color;
}
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
});
canvas.addEventListener("mousemove", function(e) {
if (e.buttons == 1 && !fillButton.pressed) {
var x = Math.floor(e.offsetX / pixelSize);
var y = Math.floor(e.offsetY / pixelSize);
if (eraser) {
ctx.fillStyle = "rgba(255,255,255,.1)";
} else {
ctx.fillStyle = color;
}
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
});
var imageInput = document.getElementById("image-input");
imageInput.addEventListener("change", function() {
var file = imageInput.files[0];
var reader = new FileReader();
reader.onload = function(e) {
var img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
function floodFill(x, y, fillColor) {
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var pixelStack = [[x, y]];
var pixelPos, rowStart, rowEnd, up, down, i;
while (pixelStack.length) {
pixelPos = pixelStack.pop();
rowStart = pixelPos[1] * canvas.width * 4;
rowEnd = rowStart + canvas.width * 4;
up = false;
down = false;
for (i = rowStart; i < rowEnd; i += 4) {
if (matchColor(imageData.data, i, fillColor)) {
continue;
}
if (matchColor(imageData.data, i, getPixelColor(pixelPos[0], pixelPos[1]))) {
imageData.data[i] = fillColor[0];
imageData.data[i + 1] = fillColor[1];
imageData.data[i + 2] = fillColor[2];
imageData.data[i + 3] = fillColor[3];
if (pixelPos[1] > 0) {
if (matchColor(imageData.data, i - canvas.width * 4, getPixelColor(pixelPos[0], pixelPos[1] - 1))) {
if (!up) {
pixelStack.push([pixelPos[0], pixelPos[1] - 1]);
up = true;
}
} else if (up) {
up = false;
}
}
if (pixelPos[1] < canvas.height - 1) {
if (matchColor(imageData.data, i + canvas.width * 4, getPixelColor(pixelPos[0], pixelPos[1] + 1))) {
if (!down) {
pixelStack.push([pixelPos[0], pixelPos[1] + 1]);
down = true;
}
} else if (down) {
down = false;
}
}
if (pixelPos[0] > 0) {
if (matchColor(imageData.data, i - 4, getPixelColor(pixelPos[0] - 1, pixelPos[1]))) {
pixelStack.push([pixelPos[0] - 1, pixelPos[1]]);
}
}
if (pixelPos[0] < canvas.width - 1) {
if (matchColor(imageData.data, i + 4, getPixelColor(pixelPos[0] + 1, pixelPos[1]))) {
pixelStack.push([pixelPos[0] + 1, pixelPos[1]]);
}
}
}
}
}
ctx.putImageData(imageData, 0, 0);
}
function matchColor(data, i, color) {
return data[i] == color[0] && data[i + 1] == color[1] && data[i + 2] == color[2] && data[i + 3] == color[3];
}
function getPixelColor(x, y) {
var imageData = ctx.getImageData(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
var r = 0, g = 0, b = 0, a = 0;
for (var i = 0; i < imageData.data.length; i += 4) {
r += imageData.data[i];
g += imageData.data[i + 1];
b += imageData.data[i + 2];
a += imageData.data[i + 3];
}
var n = imageData.data.length / 4;
return [Math.round(r / n), Math.round(g / n), Math.round(b / n), Math.round(a / n)];
}
function processFillStack() {
var threads = 4; // number of threads to use
var stackSize = fillStack.length;
var chunkSize = Math.ceil(stackSize / threads);
var chunks = [];
for (var i = 0; i < threads; i++) {
chunks.push(fillStack.splice(0, chunkSize));
}
for (var i = 0; i < threads; i++) {
(function(chunk) {
setTimeout(function() {
for (var j = 0; j < chunk.length; j++) {
var x = chunk[j][0];
var y = chunk[j][1];
var color = chunk[j][2];
floodFill(x, y, color);
}
}, 0);
})(chunks[i]);
}
}
canvas {
background-color: white;
border: 1px solid black;
}
#color-picker {
width: 50px;
height: 50px;
/* position: absolute;
top: 10px;
left: 10px; */
}
#download {
display: block;
margin: 10px auto;
padding: 10px;
background-color: black;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
label, input, button {
display: block;
margin: 10px 0;
}
label {
font-weight: bold;
}
input[type="number"] {
width: 50px;
}
button {
padding: 10px;
background-color: black;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#eraser {
display: none;
}
#eraser + label:before {
content: "";
display: inline-block;
width: 20px;
height: 20px;
background-color: white;
border: 1px solid black;
margin-right: 5px;
}
#eraser:checked + label:before {
background-color: black;
}
<canvas id="canvas"></canvas>
<input type="color" id="color-picker">
<input type="checkbox" id="eraser">
<label for="eraser">Eraser</label>
<button id="download">Download</button>
<label for="width">Width:</label>
<input type="number" id="width" value="20">
<label for="height">Height:</label>
<input type="number" id="height" value="20">
<button id="resize">Resize</button>
<button id="fill">Fill</button>
Import Image:
<input type="file" id="image-input">
I was trying to convert the colored pixels into completely transparent ones but I ended up with the color white or nothing.
2
Answers
As I see here, when you simulating the eraser effect. You’re using a semi-transparent color which is
rgba(255,255,255,.1)
. I think that this semi-transparent color is mixing with the white background of the canvas so that it causes you a white area.Much better, you use
globalCompositeOperation
here with the valuedestination-out
, so new drawings are drawn only in the areas where the existing drawings are transparent.In your
mousedown
andmousemove
, set theglobalCompositeOperation
property todestination-out
when the eraser tool is active.In the below code,
globalCompositeOperation
is set tosource-over
after each drawing operation so that subsequent drawings are drawn normally;Then, when displaying the final image, you need to draw the original canvas onto itself using the
globalCompositeOperation
property and set todestination-out
. This will remove the erased areas from the original drawing;This should allow you to erase with any color, including
rgba(0, 0, 0, 0)
, and still see the erased areas as transparent.Like others have said,
fillRect
will paint on top of what’s already there, not replace. That lets you mix transparent colours together like on a real canvas.You can just use
clearRect
to erase.Change the event listeners in
mousedown
andmousemove
to:From the mozilla docs:
Demo: