“saturation” globalCompositeOperation without changing transparency?


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 <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter" rel="nofollow">filter</a> 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.

<h2>Fix Alpha</h2>

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.

<h2>Check support.</h2>

Warning. Check browser support at bottom of <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter" rel="nofollow">filter</a> 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).

<pre class="snippet-code-js lang-js prettyprint-override">/* 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.

<pre class="snippet-code-js lang-js prettyprint-override">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"; <pre class="snippet-code-html lang-html prettyprint-override"><canvas id=c></canvas>


  • Can I manipulate a KML using Google Maps API v3?
  • SKSpriteKit, detect non-transparency parts
  • How to overlap java graphics and an Image and make it look nice?
  • Overlay while loading page into QWebView
  • How to give transparent hexagon angle at the last only?
  • IE Print font size smaller
  • Can I make a variable temporarily volatile?
  • Restoring deleted mysql database
  • Find 4 minimal values in 4 __m256d registers
  • How to select sequential duplicates in SQL Server
  • Drawing a polygon over the entire map
  • How can i decode an mp3 and encode it as aac with ezstream
  • Detecting both left and right mouse movement and no movement
  • Fail SonarQube quality gate when coverage decreases
  • Ansible: setting user on dynamic ec2
  • dc.js: Reducing rows in data table
  • How can I allow tags through rails 4 sanitize?
  • Does argparse support multiple exclusive arguments?
  • Plotting Route with Multiple Points in iOS
  • What is the default HTTP verb in WebApi ? GET or POST?
  • Open Existing DB in MySQL WorkBench
  • C#: Import/Export Settings into/from a File
  • hide missing dates from x-axis ggplot2
  • blade.php method outputting it's result to the form
  • Thread 1: EXC_BAD_ACCESS (code =1 address = 0x0)
  • TFS 2015 - Waiting for an agent to be requested
  • How to synchronize jQuery dialog box to act like alert() of Javascript
  • Object and struct member access and address offset calculation
  • The plugin 'org.apache.maven.plugins:maven-jboss-as-plugin' does not exist or no valid ver
  • Regex thinks I'm nesting, but I'm not
  • Counter field in MS Access, how to generate?
  • Bug in WPF DataGrid
  • TFS: Get latest causes slow project reloading
  • Javascript Callbacks with Object constructor
  • Javascript + PHP Encryption with pidCrypt
  • How to make Safari send if-modified-since header?
  • Websockets service method fails during R startup
  • Why can't I rebase on to an ancestor of source changesets if on a different branch?