Add color filter to dark part of image and another filter to light part of the image?

545 Views Asked by At

My challenge is to add color filter to dark part of image and another color filter to light part of image. To achieve effect like this https://i.stack.imgur.com/niAKW.jpg

I am using canvas with globalCompositeOperation effects, but I am able to apply only one filter without affect the other one.

ctx.drawImage(image, 0, 0, 380, 540);
ctx.globalCompositeOperation = 'darken';
ctx.fillStyle = overlayFillColor;
ctx.fillRect(0, 0, 380, 540);

this works great to apply color filter to dark or light areas, based on the globalCompositeOperation, but if I add another filter, it change colors of the previous filter as well.

any idea?

thanks Ales

1

There are 1 best solutions below

4
Kaiido On BEST ANSWER

There is a nice SVG filter component which does map luminance to alpha: <feColorMatrix type="luminanceToAlpha"/>
Since we can use SVG filters in canvas, this allows us to separate the dark area from the light one and use compositing instead of blending.

This way, your input colors are preserved.

(async () => {
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const img = new Image();
  img.src = "https://picsum.photos/500/500";
  await img.decode();
  canvas.width = img.width;
  canvas.height = img.height;
  // first we create our alpha layer
  ctx.filter = "url(#lumToAlpha)";
  ctx.drawImage(img, 0, 0);
  ctx.filter = "none";
  const alpha = await createImageBitmap(canvas);
  
  // draw on "light" zone
  ctx.globalCompositeOperation = "source-in";
  ctx.fillStyle = "red";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  // save into an ImageBitmap
  // (note that we could also use a second canvas to do this all synchronously)
  const light = await createImageBitmap(canvas);
  
  // clean canvas
  ctx.globalCompositeOperation = "source-over";
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // draw on "dark" zone
  ctx.drawImage(alpha, 0, 0);
  ctx.globalCompositeOperation = "source-out";
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  // reintroduce "light" zone
  ctx.globalCompositeOperation = "source-over";
  ctx.drawImage(light, 0, 0);
})().catch(console.error);
<svg width="0" height="0" style="visibility:hidden;position:absolute">
  <filter id="lumToAlpha">
    <feColorMatrix type="luminanceToAlpha" />
  </filter>
</svg>
<canvas></canvas>
<!--
  If you don't like having an element in the DOM just for that
  you could also directly set the context's filter to a data:// URI
  url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cfilter%20id%3D%22f%22%3E%3CfeColorMatrix%20type%3D%22luminanceToAlpha%22%2F%3E%3C%2Ffilter%3E%3C%2Fsvg%3E#f");
  but you'd have to wait a least a task (setTimeout(fn, 0))
  because setting filters this way is async...
  Hopefully CanvasFilters will solve this soon enough
-->

Note that hopefully we'll have CanvasFilters objects in a near future, which will make SVG filters to canvas easier to use, and accessible in Workers (they currently aren't...). So for the ones from the future (or from the present on Canary with web-features flag on), this would look like:

// typeof CanvasFilter === "function"
// should be enough for detecting colorMatrix
// but see below for how to "correctly" feature-detect
// a particular CanvasFilter
if (supportsColorMatrixCanvasFilter()) {
(async () => {
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const img = new Image();
  img.src = "https://picsum.photos/500/500";
  await img.decode();
  canvas.width = img.width;
  canvas.height = img.height;
  // first we create our alpha layer
  ctx.filter = new CanvasFilter({
    filter: "colorMatrix",
    type: "luminanceToAlpha"
  });
  ctx.drawImage(img, 0, 0);
  ctx.filter = "none";
  const alpha = await createImageBitmap(canvas);
  
  // draw on "light" zone
  ctx.globalCompositeOperation = "source-in";
  ctx.fillStyle = "red";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  // save into an ImageBitmap
  // (note that we could also use a second canvas to do this all synchronously)
  const light = await createImageBitmap(canvas);
  
  // clean canvas
  ctx.globalCompositeOperation = "source-over";
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // draw on "dark" zone
  ctx.drawImage(alpha, 0, 0);
  ctx.globalCompositeOperation = "source-out";
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  // reintroduce "light" zone
  ctx.globalCompositeOperation = "source-over";
  ctx.drawImage(light, 0, 0);
})().catch(console.error);
}
else {
  console.error("your browser doesn't support CanvasFilters yet");
}
// Feature detection is hard...
// see https://gist.github.com/Kaiido/45d189c110d29ac2eda25a7762c470f2
// to get the list of all supported CanvasFilters
// below only checks for colorMatrix
function supportsColorMatrixCanvasFilter() {
  if(typeof CanvasFilter !== "function") {
    return false;
  }
  let supported = false;
  try {
    new CanvasFilter({
      filter: "colorMatrix",
      // "type" will be visited for colorMatrix
      // we throw in to avoid actually creating the filter
      get type() { supported = true; throw ""; }
    });
  } catch(err) {}
  return supported;
}
<canvas></canvas>