javascripthtmlcssgraphicstransparent

How do I add transparent Pixels without just doing white pixels?


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.


Solution

  • 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">