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.
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
and mousemove
to:
if (eraser) {
ctx.clearRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
} else {
ctx.fillStyle = color;
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
From the mozilla docs:
The
CanvasRenderingContext2D.clearRect()
method of the Canvas 2D API erases the pixels in a rectangular area by setting them to transparent black.
Demo:
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.clearRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
} 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.clearRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
} 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">