Seph Reed
Seph Reed

Reputation: 10938

How to only draw pixels over a certain opacity?

I'm searching for a way to only draw pixels onto a canvas if their opacity is greater than some number. My hope was that there would be a globalCompositeOperation that could do so, but I'm not finding it yet.

Is there a quick way to draw (or not draw) pixels on canvas based off opacity? Or perhaps r, g, or b values?

Upvotes: 0

Views: 54

Answers (1)

Kaiido
Kaiido

Reputation: 136678

You can use an SVG filter for this (or a CanvasFilter in Chrome), notably the <feComponentTransfer> filter is your friend for this task:

(async () => {
  const img = new Image();
  // an image with interesting alpha
  img.src = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
  await img.decode();
  // we'll need access to the DOM element in this version
  const funcA = document.querySelector("feFuncA");
  
  const canvas = document.querySelector("canvas");
  canvas.width = img.width / 2;
  canvas.height = img.height / 2;
  const ctx = canvas.getContext("2d");
  
  // to show an example of "linear" transparency
  const grad = ctx.createRadialGradient(25, 75, 0, 25, 75, 25);
  grad.addColorStop(0, "red");
  grad.addColorStop(1, "transparent");

  const input = document.querySelector("input");
  input.oninput = draw;

  draw();

  async function draw() {
    // build the high-pass filter values
    // from 0 to n the values are set to 0 (completely transparent)
    const touchedValues = new Array(input.valueAsNumber);              
    touchedValues.fill(0);
    // from n to 254 set the values to their index
    const untouchedValues = Array.from({
      length: 253 - input.valueAsNumber
     }, (_, i) => (input.valueAsNumber + i) / 255);

    // merge in a single array,
    // with fully opaque always opaque
    // (and fully transparent alway transparent)
    const tableValues = [0, ...touchedValues, ...untouchedValues, 1];
    funcA.setAttribute("tableValues", tableValues);
    // we have to wait for the SVGOM to update the filter...
    await new Promise(res => setTimeout(res));
    // reset the filter so we can clear correctly
    ctx.filter = "none";
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.filter = "url(#remove-alpha)";
    // now draw our examples
    ctx.fillStyle = "green";
    ctx.fillRect(0, 0, 50, 50);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 50, 50, 50);
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  }
})().catch(console.error);
canvas {
  background-image:
    /* tint image */
    linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
    /* checkered effect */
    linear-gradient(to right, black 50%, white 50%),
    linear-gradient(to bottom, black 50%, white 50%);
  background-blend-mode: normal, difference, normal;
  background-size: 20px 20px;
}
<svg width="0" height="0" style="position:absolute;z-index:-1;">
  <defs>
    <filter id="remove-alpha" x="0" y="0" width="100%" height="100%">
      <feComponentTransfer>
        <feFuncA type="discrete" tableValues="0 1"></feFuncA>
      </feComponentTransfer>
      </filter>
  </defs>
</svg>
<input type="range" min="0" max="253"><br>
<canvas></canvas>

Or using the new CanvasFilter interface (in Chrome Canary)

(async () => {
  const img = new Image();
  img.src = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
  await img.decode();
  const canvas = document.querySelector("canvas");
  canvas.width = img.width / 2;
  canvas.height = img.height / 2;
  const ctx = canvas.getContext("2d");
  const grad = ctx.createRadialGradient(25, 75, 0, 25, 75, 25);
  grad.addColorStop(0, "red");
  grad.addColorStop(1, "transparent");
  const input = document.querySelector("input");
  input.oninput = draw;

  draw();

  function draw() {
    const untouchedValues = Array.from({ length: 253 - input.valueAsNumber}, (_, i) => (input.valueAsNumber + i) / 255);
    const touchedValues = new Array(input.valueAsNumber);              
    touchedValues.fill(0);
    const tableValues = [0, ...touchedValues, ...untouchedValues, 1];
    ctx.filter = "none";
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.filter = new CanvasFilter({
      filter: "componentTransfer",
      funcA: {
        type: "table",
        tableValues
      }
    });
    ctx.fillStyle = "green";
    ctx.fillRect(0, 0, 50, 50);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 50, 50, 50);
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  }
})().catch(console.error);
canvas {
  background-image:
    /* tint image */
    linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
    /* checkered effect */
    linear-gradient(to right, black 50%, white 50%),
    linear-gradient(to bottom, black 50%, white 50%);
  background-blend-mode: normal, difference, normal;
  background-size: 20px 20px;
}
<input type="range" min="0" max="253"><br>
<canvas></canvas>

Upvotes: 1

Related Questions