Invisible Object Shadow Native Canvas

by Paul Wheeler

HTML

<html>

  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.7.0/fabric.js"></script>
  </head>

  <body>
    <canvas id="c"></canvas>
  </body>

</html>

CSS

html {
  height: 100%;
}

body {
  height: 100%;
}

JavaScript

fabric.BoxShadow = fabric.util.createClass(fabric.Rect, {

  shadowColor: undefined,

  shadowBlur: 0,
  shadowOffsetX: 0,
  shadowOffsetY: 0,

  initialize(options) {
    this.callSuper('initialize', options);

    this._shadow = new fabric.Shadow({
      color: this.shadowColor,
      blur: this.shadowBlur,
      offsetX: this.shadowOffsetX,
      offsetY: this.shadowOffsetY
    });
  },

  _render: function(ctx) {
    ctx.save();

    // set clip path
    let [offsetX, offsetY, blur] = [this.shadowOffsetX,
      this.shadowOffsetY,
      this.shadowBlur
    ];

    let [top, left] = [this.width / -2, this.height / -2];

    let region = new Path2D();
    // The outer rectangle for our clipping path completely encompases the object and its shadow
    let bounds = {
      t: Math.min(top, top + offsetY - blur),
      l: Math.min(left, left + offsetX - blur),
      b: Math.max(top + this.height, top + this.height + offsetY + blur),
      r: Math.max(left + this.width, left + this.width + offsetX + blur),
    };

    region.rect(bounds.l, bounds.t, bounds.r - bounds.l, bounds.b - bounds.t);

    // now we subtract the actual object from our clipping path
    // Note: we have to add  beginPath function because the base class render code is going to treat this likc a CanvasRenderingContext2D instead of a Path2D
    region.beginPath = function() { };
    this.callSuper('_render', region);
    
    ctx.clip(region, "evenodd");

    // Fabric draws shadows, oddly enough, around the entire area rendered within this function. I haven't figured out the correct function to override to get our clip path to work with the normal fabric rendering pipeline
    this.shadow = this._shadow;
    // leverage the FabricJS shadow sizing logic
    this._setShadow(ctx);
    
    this.callSuper('_render', ctx);
    this.shadow = undefined;

    ctx.restore();
  },
  
  _renderPaintInOrder: function(ctx) {
    if (ctx instanceof CanvasRenderingContext2D) {
   ...