Search code examples
htmlcanvassavehtml5-canvasgrayscale

Save canvas in grayscale


I was wondering if it is possible to have a coloured canvas in HTML5 and then save it as a black and white (grayscale) PNG, while you click on the save button. I managed to save the canvas as a PNG already, which is great! But I'd like to add the grayscale feature without having to change it myself with a program as Illustrator etc. The canvas has moving particles btw. (I'm not sure if that has any effect though)

Thanks~!

function download(){
    var dt = c.toDataURL('image/png');
    this.href = dt; 
}; 
downloadlink.addEventListener('click', download, false); 

Solution

  • It's possible by extracting the RGB data from the canvas and calculate their luma values.

    There are a couple of requisites that needs to be fulfilled though, like CORS (which does not seem to be a problem in this case as you can already save out an image) and if you wish to keep the original data after save you can either copy the current image on canvas to a temporary one, or keep a backup of the pixel data that needs to be extracted.

    RGB to Luma

    The formula for converting the RGB data into a luma value (grey) is as follows based on the REC 709 (or BT.709) formula:

    luma = Red x 0.2126 + Green x 0.7152 + Blue x 0.0722
    

    Alternatively you could use the REC 601 (BT.601) formula instead (which is more common for SD video footage):

    luma = Red x 0.299 + Green x 0.587 + Blue x 0.114
    

    To use this, simply iterate over all pixels, obtain the luma value using one of these formulas and replace all channels in target with the resulting luma value.

    Temporary data

    For temporary data we can do either (when save button is clicked):

    Temporary canvas

    • Create a temporary canvas
    • Set the size equal to source canvas
    • Draw in source canvas using drawImage()
    • Extract target canvas' pixel data using getImageData()
    • Iterate over the pixels, convert using the formula, put data back
    • Save out image then discard temporary canvas

    or

    ImageData backup

    • Extract image data using getImageData() - this will act as our backup
    • Create new ImageData the same size using createImageData()
    • Iterate over the pixel from the backup, but put the result in luma into the created ImageData
    • Put back the created ImageData, save out image
    • Put back the backup data

    If the latter step is necessary depends really on how you update the canvas. If you are animating a loop there may not be need to put back the backup (or keep one) if everything gets updated anyways, but if so and you see flash of grey or some areas left grey, then this step would be needed.

    Example using the ImageData approach

    var ctx = c.getContext("2d"), img = new Image;
    img.onload = setup;  img.crossOrigin = "";
    img.src = "//i.imgur.com/OrYVGI8.jpg";
    
    function setup() {
      c.width = this.naturalWidth;  c.height = this.naturalHeight;
      ctx.drawImage(this, 0, 0);    btn.disabled = false
    }
    
    // Main code for demo
    btn.onclick = function() {
      
      var idataSrc = ctx.getImageData(0, 0, c.width, c.height), // original
          idataTrg = ctx.createImageData(c.width, c.height),    // empty data
          dataSrc = idataSrc.data,                              // reference the data itself
          dataTrg = idataTrg.data,
          len = dataSrc.length, i = 0, luma;
      
      // convert by iterating over each pixel each representing RGBA
      for(; i < len; i += 4) {
        // calculate luma, here using Rec 709
        luma = dataSrc[i] * 0.2126 + dataSrc[i+1] * 0.7152 + dataSrc[i+2] * 0.0722;
    
        // update target's RGB using the same luma value for all channels
        dataTrg[i] = dataTrg[i+1] = dataTrg[i+2] = luma;
        dataTrg[i+3] = dataSrc[i+3];                            // copy alpha
      }
      
      // put back luma data so we can save it as image
      ctx.putImageData(idataTrg, 0, 0);
      demo.src = c.toDataURL();                                 // set demo result's src url
      
      // restore backup data
      ctx.putImageData(idataSrc, 0, 0);
    };
    <button disabled id=btn>SAVE TO GREY</button> (click then scroll down to see result)<br>
    <canvas id=c></canvas><br><img id=demo>