skip to Main Content

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


  1. 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 value destination-out, so new drawings are drawn only in the areas where the existing drawings are transparent.

    In your mousedown and mousemove, set the globalCompositeOperation property to destination-out when the eraser tool is active.

    In the below code, globalCompositeOperation is set to source-over after each drawing operation so that subsequent drawings are drawn normally;

    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.globalCompositeOperation = "destination-out";
              ctx.fillStyle = "rgba(0,0,0,1)";
              ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
          } else {
              ctx.fillStyle = color;
              ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
          }
          ctx.globalCompositeOperation = "source-over";
      }
    });
    
    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.globalCompositeOperation = "destination-out";
              ctx.fillStyle = "rgba(0,0,0,1)";
              ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
          } else {
              ctx.fillStyle = color;
              ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
          }
          ctx.globalCompositeOperation = "source-over";
      }
    });
    

    Then, when displaying the final image, you need to draw the original canvas onto itself using the globalCompositeOperation property and set to destination-out. This will remove the erased areas from the original drawing;

    function displayFinalImage() {
      ctx.globalCompositeOperation = "destination-out";
      ctx.drawImage(canvas, 0, 0);
      ctx.globalCompositeOperation = "source-over";
    }
    
    

    This should allow you to erase with any color, including rgba(0, 0, 0, 0), and still see the erased areas as transparent.

    Login or Signup to reply.
  2. 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">
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search