I have a canvas containing art on a transparent background. I desaturate it like this:
boardCtx.fillStyle = "rgba(0, 0, 0, 1.0)";
boardCtx.globalCompositeOperation = 'saturation';
boardCtx.fillRect(0, 0, boardCanvas.width, boardCanvas.height);
and find that the transparent background has turned opaque black. I wouldn't expect the saturation blend mode to change the alpha channel... am I doing something wrong? My current solution is to copy the canvas before desaturation and use it to mask the black background away from the desaturated copy, but that involves another canvas and a big draw... not ideal.
You can use ctx.filter
The 2D context filter can be used to apply various filters to the canvas.
ctx.filter = "saturate(0%)";
ctx.drawImage(ctx.canvas,0,0);
But this will add to the alpha if there is anti-aliasing / transparency, reducing quality.
Fix Alpha
To fix you need to use the ctx.globalCompositeOperation = "copy"
operation.
ctx.filter = "saturate(0%)";
ctx.globalCompositeOperation = "copy";
ctx.drawImage(ctx.canvas,0,0);
// restore defaults;
ctx.filter = "";
ctx.globalCompositeOperation = "source-over";
This will stop the alpha channel from being modified.
Check support.
Warning. Check browser support at bottom of filter page. If no support you will have to use a copy of the canvas to restore the alpha if you use ctx.globalCompositeOperation = "saturation"
Blending modes will work only on the foreground (source) layer without respect to the alpha channel, while the regular composite operations only use alpha channels - this is why you see the opaque result.
To solve simply add a "clipping call" to the existing content after de-saturation process using composition mode "destination-out", then redraw the image:
// draw image 1. time
boardCtx.fillStyle = "#000";
boardCtx.globalCompositeOperation = 'saturation';
boardCtx.fillRect(0, 0, boardCanvas.width, boardCanvas.height);
boardCtx.globalCompositeOperation = 'destination-out';
// draw image again 2. time
This will also restore the original alpha channel.
If the art is not an image source then you can take a snapshot by drawing the canvas to a temporary canvas, then use that temporary canvas as image source when drawing back using the same steps as above.
You can also use filters as in the other answer (there is also a filter "grayscale" which is slightly more efficient than "saturate") but currently only Chrome (from v52) and Firefox (from v49) supports filter
, as well as Webview on Android (from v52).
/*
CanvasRenderingContext2D.filter (EXPERIMENTAL, On Standard Track)
https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/filter
DESKTOP:
Chrome | Edge | Firefox | IE | Opera | Safari
----------+-----------+-----------+-----------+-----------+-----------
52 | ? | 49° | - | - | -
°) 35-48: Behind flag canvas.filters.enabled set to true.
MOBILE:
Chrome/A | Edge/mob | Firefox/A | Opera/A |Safari/iOS | Webview/A
----------+-----------+-----------+-----------+-----------+-----------
52 | ? | 49° | - | - | 52
°) 35-48: Behind flag canvas.filters.enabled set to true.
*/
A third approach is to iterate over the pixels and do the desaturation. This would only be necessary if you intend to support older browsers which do not support the blending modes.
var ctx = c.getContext("2d"), i = new Image;
i.onload = function() {
ctx.drawImage(this, 0, 0); // draw image normally
ctx.globalCompositeOperation = "saturation"; // desaturate (blending removes alpha)
ctx.fillRect(0, 0, c.width, c.height);
ctx.globalCompositeOperation = "destination-in"; // knock out the alpha channel
ctx.drawImage(this, 0, 0); // by redrawing image using this mode
};
i.src = "//i.stack.imgur.com/F4ukA.png";
<canvas id=c></canvas>