Search code examples
htmlcanvashtml5-canvasdropshadowputimagedata

Canvas effects such as filter or drop shadow not applied with context.putImageData


In a html canvas, I am trying to generate a drop shadow on an image with transparent pieces in it. This image is generated by code and then drawn to the canvas using: ctx.putImageData(dst, 0, 0)

The problem is that the following code is not generating any shadow:

ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 15;
ctx.shadowColor = 'rgba(0,0,0,1)';

ctx.putImageData(dst, 0, 0);

Any help would be appreciated


Solution

  • ctx.putImageData() will replace the pixels in your context with the ones contained in the ImageData that you puts.
    There is no context's property like shadowBlur, nor filter, nor globalCompositeOperation, nor even matrix tranforms that will affect it. Even transparent pixels in your ImageData will be transparent in the context.

    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'salmon';
    ctx.fillRect(0,0,300,150);
    
    ctx.translate(120, 50);
    ctx.rotate(Math.PI/3);
    ctx.translate(-25, -25);
    ctx.filter = 'blur(5px)';
    ctx.globalCompositeOperation = 'lighter';
    
    ctx.fillStyle = '#0000FF';
    ctx.fillRect(0,0,50,50);
    
    setTimeout(() => {
      // at this time, all previous filters, transform, gCO are still active 
      const bluerect = ctx.createImageData(50,50);
      const data = new Uint32Array(bluerect.data.buffer);
      data.fill(0xFFFF0000); // blue
      ctx.putImageData(bluerect, 0, 0); // same as our previous fillRect();
      // a transparent ImageData (smaller)
      const transrect = ctx.createImageData(25, 25);
      ctx.putImageData(transrect, 170, 50); // push a bit farther;
    }, 1500);
    body {
      background: lightblue;
    }
    <canvas id="canvas"></canvas>

    So, how to deal with an ImageData and still be able to apply the context's properties on it? Go through a second off-screen canvas, on which you will put your ImageData, and that you will then draw on your main canvas. drawImage accepts an HTMLCanvasElement as source, and it is affected by context properties like shadowBlur:

    const ctx = canvas.getContext('2d');
    ctx.shadowBlur = 12;
    ctx.shadowColor = "red";
    // our ImageData
    const bluerect = ctx.createImageData(50,50);
    const data = new Uint32Array(bluerect.data.buffer);
    data.fill(0xFFFF0000); // blue
    // create a new canvas, the size of our ImageData
    const offscreen = document.createElement('canvas');
    offscreen.width = bluerect.width;
    offscreen.height = bluerect.height;
    // put our ImageData on it
    offscreen.getContext('2d')
      .putImageData(bluerect, 0, 0);
    // draw it on main canvas
    ctx.drawImage(offscreen, 50, 50);
    <canvas id="canvas"></canvas>

    Now, new browsers have also the ability to do it without the use of a second browser, by generating an ImageBitmap from the ImageData, but this operation is asynchronous, so you may still prefer the old way.

    const ctx = canvas.getContext('2d');
    ctx.shadowBlur = 12;
    ctx.shadowColor = "red";
    // our ImageData
    const bluerect = ctx.createImageData(50,50);
    const data = new Uint32Array(bluerect.data.buffer);
    data.fill(0xFFFF0000); // blue
    
    // create an ImageBitmap from our ImageData
    createImageBitmap(bluerect)
    .then(bitmap => { // later
      ctx.drawImage(bitmap, 50, 50);
    });
    <canvas id="canvas"></canvas>