Search code examples
javascriptsvgsafarihtml5-canvasdropshadow

Safari incorrectly drawing shadows on SVGs via HTML5 Canvas


I'm using HTML5 canvas in a project and occasionally need to draw drop shadows on SVGs within a canvas. I've noticed that, compared to Chrome, Safari does two things incorrectly when doing this:

  1. Safari draws a shadow on each individual shape within an SVG
  2. Safari crops off parts of the shadow that go beyond the SVG's bounds

These issues can be illustrated by the following code:

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = 'red'

var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
    context.drawImage(image, 10, 10, 100, 100);
}
<canvas id='canvas'></canvas>

I can't embed images yet, but here are some links to images that illustrate the problem:

(they are screenshots of the code above)

The results from Safari are... quite ugly, as you can see. Is there a way make Safari to render SVGs with shadows on HTML5 canvas like Chrome does?

Any help would be greatly appreciated. Thanks so much for your time!


Solution

  • That's a bug, you should report it to webkit's bug-tracker.

    Though you can workaround it by first drawing the image on a second canvas just to rasterize that svg image and use that canvas as source for the shadowing:

    var canvas = document.getElementById('canvas');
    var context = canvas.getContext('2d');
    
    var image = new Image();
    image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
    image.onload = function() {
      const off = canvas.cloneNode();
      off.getContext('2d').drawImage(image, 10, 10, 100, 100);
      context.shadowOffsetX = 10;
      context.shadowOffsetY = 10;
      context.shadowColor = 'red';
      context.drawImage(off, 0, 0);
    }
    <canvas id='canvas'></canvas>

    In order to use a single canvas, we need to use an offset trick, but it's not always easy to do since it requires knowing clearly the position of our drawing:

    var canvas = document.getElementById('canvas');
    var context = canvas.getContext('2d');
    
    var image = new Image();
    image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
    image.onload = function() {
      // first pass without shadow
      context.drawImage(image, 10, 10, 100, 100);
      // set shadow offsets to the position in page of bottom-right corner
      context.shadowOffsetX = 10 + 110;
      context.shadowOffsetY = 10 + 110;
      context.shadowColor = 'red';
      // draw behind
      context.globalCompositeOperation = "destination-over";
      // draw with inverse offset, so that the image is not visible
      // but the shadow is in-screen
      context.drawImage(canvas, -110, -110);
    }
    <canvas id='canvas'></canvas>