Merge branch 'master' of github.com:airbnb/lottie-web
diff --git a/player/js/elements/canvasElements/CVBaseElement.js b/player/js/elements/canvasElements/CVBaseElement.js
index 581f7e2..dc1e649 100644
--- a/player/js/elements/canvasElements/CVBaseElement.js
+++ b/player/js/elements/canvasElements/CVBaseElement.js
@@ -1,3 +1,4 @@
+import assetManager from '../../utils/helpers/assetManager';
 import getBlendMode from '../../utils/helpers/blendModes';
 import Matrix from '../../3rd_party/transformation-matrix';
 import CVEffects from './CVEffects';
@@ -6,11 +7,35 @@
 function CVBaseElement() {
 }
 
+var operationsMap = {
+  1: 'source-in',
+  2: 'source-out',
+  3: 'source-in',
+  4: 'source-out',
+};
+
 CVBaseElement.prototype = {
   createElements: function () {},
   initRendererElement: function () {},
   createContainerElements: function () {
+    // If the layer is masked we will use two buffers to store each different states of the drawing
+    // This solution is not ideal for several reason. But unfortunately, because of the recursive
+    // nature of the render tree, it's the only simple way to make sure one inner mask doesn't override an outer mask.
+    // TODO: try to reduce the size of these buffers to the size of the composition contaning the layer
+    // It might be challenging because the layer most likely is transformed in some way
+    if (this.data.tt >= 1) {
+      this.buffers = [];
+      var canvasContext = this.globalData.canvasContext;
+      var bufferCanvas = assetManager.createCanvas(canvasContext.canvas.width, canvasContext.canvas.height);
+      this.buffers.push(bufferCanvas);
+      var bufferCanvas2 = assetManager.createCanvas(canvasContext.canvas.width, canvasContext.canvas.height);
+      this.buffers.push(bufferCanvas2);
+      if (this.data.tt >= 3 && !document._isProxy) {
+        assetManager.loadLumaCanvas();
+      }
+    }
     this.canvasContext = this.globalData.canvasContext;
+    this.transformCanvas = this.globalData.transformCanvas;
     this.renderableEffectsManager = new CVEffects(this);
   },
   createContent: function () {},
@@ -37,19 +62,89 @@
       this.maskManager._isFirstFrame = true;
     }
   },
-  renderFrame: function () {
+  clearCanvas: function (canvasContext) {
+    canvasContext.clearRect(
+      this.transformCanvas.tx,
+      this.transformCanvas.ty,
+      this.transformCanvas.w * this.transformCanvas.sx,
+      this.transformCanvas.h * this.transformCanvas.sy
+    );
+  },
+  prepareLayer: function () {
+    if (this.data.tt >= 1) {
+      var buffer = this.buffers[0];
+      var bufferCtx = buffer.getContext('2d');
+      this.clearCanvas(bufferCtx);
+      // on the first buffer we store the current state of the global drawing
+      bufferCtx.drawImage(this.canvasContext.canvas, 0, 0);
+      // The next four lines are to clear the canvas
+      // TODO: Check if there is a way to clear the canvas without resetting the transform
+      this.currentTransform = this.canvasContext.getTransform();
+      this.canvasContext.setTransform(1, 0, 0, 1, 0, 0);
+      this.clearCanvas(this.canvasContext);
+      this.canvasContext.setTransform(this.currentTransform);
+    }
+  },
+  exitLayer: function () {
+    if (this.data.tt >= 1) {
+      var buffer = this.buffers[1];
+      // On the second buffer we store the current state of the global drawing
+      // that only contains the content of this layer
+      // (if it is a composition, it also includes the nested layers)
+      var bufferCtx = buffer.getContext('2d');
+      this.clearCanvas(bufferCtx);
+      bufferCtx.drawImage(this.canvasContext.canvas, 0, 0);
+      // We clear the canvas again
+      this.canvasContext.setTransform(1, 0, 0, 1, 0, 0);
+      this.clearCanvas(this.canvasContext);
+      this.canvasContext.setTransform(this.currentTransform);
+      // We draw the mask
+      const mask = this.comp.getElementById('tp' in this.data ? this.data.tp : this.data.ind - 1);
+      mask.renderFrame(true);
+      // We draw the second buffer (that contains the content of this layer)
+      this.canvasContext.setTransform(1, 0, 0, 1, 0, 0);
+
+      // If the mask is a Luma matte, we need to do two extra painting operations
+      // the _isProxy check is to avoid drawing a fake canvas in workers that will throw an error
+      if (this.data.tt >= 3 && !document._isProxy) {
+        // We copy the painted mask to a buffer that has a color matrix filter applied to it
+        // that applies the rgb values to the alpha channel
+        var lumaBuffer = assetManager.getLumaCanvas(this.canvasContext.canvas);
+        var lumaBufferCtx = lumaBuffer.getContext('2d');
+        lumaBufferCtx.drawImage(this.canvasContext.canvas, 0, 0);
+        this.clearCanvas(this.canvasContext);
+        // we repaint the context with the mask applied to it
+        this.canvasContext.drawImage(lumaBuffer, 0, 0);
+      }
+      this.canvasContext.globalCompositeOperation = operationsMap[this.data.tt];
+      this.canvasContext.drawImage(buffer, 0, 0);
+      // We finally draw the first buffer (that contains the content of the global drawing)
+      // We use destination-over to draw the global drawing below the current layer
+      this.canvasContext.globalCompositeOperation = 'destination-over';
+      this.canvasContext.drawImage(this.buffers[0], 0, 0);
+      this.canvasContext.setTransform(this.currentTransform);
+      // We reset the globalCompositeOperation to source-over, the standard type of operation
+      this.canvasContext.globalCompositeOperation = 'source-over';
+    }
+  },
+  renderFrame: function (forceRender) {
     if (this.hidden || this.data.hd) {
       return;
     }
+    if (this.data.td === 1 && !forceRender) {
+      return;
+    }
     this.renderTransform();
     this.renderRenderable();
     this.setBlendMode();
     var forceRealStack = this.data.ty === 0;
+    this.prepareLayer();
     this.globalData.renderer.save(forceRealStack);
     this.globalData.renderer.ctxTransform(this.finalTransform.mat.props);
     this.globalData.renderer.ctxOpacity(this.finalTransform.mProp.o.v);
     this.renderInnerContent();
     this.globalData.renderer.restore(forceRealStack);
+    this.exitLayer();
     if (this.maskManager.hasMasks) {
       this.globalData.renderer.restore(true);
     }
diff --git a/player/js/elements/canvasElements/CVContextData.js b/player/js/elements/canvasElements/CVContextData.js
index a4f2e7e..f5d8b93 100644
--- a/player/js/elements/canvasElements/CVContextData.js
+++ b/player/js/elements/canvasElements/CVContextData.js
@@ -35,4 +35,56 @@
   this.cO = 1;
 };
 
+CVContextData.prototype.popTransform = function () {
+  var popped = this.saved[this.cArrPos];
+  var i;
+  var arr = this.cTr.props;
+  for (i = 0; i < 16; i += 1) {
+    arr[i] = popped[i];
+  }
+  return popped;
+};
+
+CVContextData.prototype.popOpacity = function () {
+  var popped = this.savedOp[this.cArrPos];
+  this.cO = popped;
+  return popped;
+};
+
+CVContextData.prototype.pop = function () {
+  this.cArrPos -= 1;
+  var transform = this.popTransform();
+  var opacity = this.popOpacity();
+  return {
+    transform: transform,
+    opacity: opacity,
+  };
+};
+
+CVContextData.prototype.push = function () {
+  var props = this.cTr.props;
+  if (this._length <= this.cArrPos) {
+    this.duplicate();
+  }
+  var i;
+  var arr = this.saved[this.cArrPos];
+  for (i = 0; i < 16; i += 1) {
+    arr[i] = props[i];
+  }
+  this.savedOp[this.cArrPos] = this.cO;
+  this.cArrPos += 1;
+};
+
+CVContextData.prototype.getTransform = function () {
+  return this.cTr;
+};
+
+CVContextData.prototype.getOpacity = function () {
+  return this.cO;
+};
+
+CVContextData.prototype.setOpacity = function (value) {
+  this.cO = value;
+};
+
 export default CVContextData;
diff --git a/player/js/renderers/BaseRenderer.js b/player/js/renderers/BaseRenderer.js
index 4eab1c4..53b3252 100644
--- a/player/js/renderers/BaseRenderer.js
+++ b/player/js/renderers/BaseRenderer.js
@@ -133,6 +133,17 @@
   }
 };
 
+BaseRenderer.prototype.getElementById = function (ind) {
+  var i;
+  var len = this.elements.length;
+  for (i = 0; i < len; i += 1) {
+    if (this.elements[i].data.ind === ind) {
+      return this.elements[i];
+    }
+  }
+  return null;
+};
+
 BaseRenderer.prototype.getElementByPath = function (path) {
   var pathValue = path.shift();
   var element;
diff --git a/player/js/renderers/CanvasRendererBase.js b/player/js/renderers/CanvasRendererBase.js
index fd444a4..8a38160 100644
--- a/player/js/renderers/CanvasRendererBase.js
+++ b/player/js/renderers/CanvasRendererBase.js
@@ -72,12 +72,17 @@
     this.canvasContext.transform(props[0], props[1], props[4], props[5], props[12], props[13]);
     return;
   }
+  // Resetting the canvas transform matrix to the new transform
   this.transformMat.cloneFromProps(props);
-  var cProps = this.contextData.cTr.props;
+  // Taking the last transform value from the stored stack of transforms
+  var currentTransform = this.contextData.getTransform();
+  var cProps = currentTransform.props;
+  // Applying the last transform value after the new transform to respect the order of transformations
   this.transformMat.transform(cProps[0], cProps[1], cProps[2], cProps[3], cProps[4], cProps[5], cProps[6], cProps[7], cProps[8], cProps[9], cProps[10], cProps[11], cProps[12], cProps[13], cProps[14], cProps[15]);
-  // this.contextData.cTr.transform(props[0],props[1],props[2],props[3],props[4],props[5],props[6],props[7],props[8],props[9],props[10],props[11],props[12],props[13],props[14],props[15]);
-  this.contextData.cTr.cloneFromProps(this.transformMat.props);
-  var trProps = this.contextData.cTr.props;
+  // Storing the new transformed value in the stored transform
+  currentTransform.cloneFromProps(this.transformMat.props);
+  var trProps = currentTransform.props;
+  // Applying the new transform to the canvas
   this.canvasContext.setTransform(trProps[0], trProps[1], trProps[4], trProps[5], trProps[12], trProps[13]);
 };
 
@@ -85,15 +90,17 @@
   /* if(op === 1){
         return;
     } */
+  var currentOpacity = this.contextData.getOpacity();
   if (!this.renderConfig.clearCanvas) {
     this.canvasContext.globalAlpha *= op < 0 ? 0 : op;
-    this.globalData.currentGlobalAlpha = this.contextData.cO;
+    this.globalData.currentGlobalAlpha = currentOpacity;
     return;
   }
-  this.contextData.cO *= op < 0 ? 0 : op;
-  if (this.globalData.currentGlobalAlpha !== this.contextData.cO) {
-    this.canvasContext.globalAlpha = this.contextData.cO;
-    this.globalData.currentGlobalAlpha = this.contextData.cO;
+  currentOpacity *= op < 0 ? 0 : op;
+  this.contextData.setOpacity(currentOpacity);
+  if (this.globalData.currentGlobalAlpha !== currentOpacity) {
+    this.canvasContext.globalAlpha = currentOpacity;
+    this.globalData.currentGlobalAlpha = currentOpacity;
   }
 };
 
@@ -113,17 +120,7 @@
   if (actionFlag) {
     this.canvasContext.save();
   }
-  var props = this.contextData.cTr.props;
-  if (this.contextData._length <= this.contextData.cArrPos) {
-    this.contextData.duplicate();
-  }
-  var i;
-  var arr = this.contextData.saved[this.contextData.cArrPos];
-  for (i = 0; i < 16; i += 1) {
-    arr[i] = props[i];
-  }
-  this.contextData.savedOp[this.contextData.cArrPos] = this.contextData.cO;
-  this.contextData.cArrPos += 1;
+  this.contextData.push();
 };
 
 CanvasRendererBase.prototype.restore = function (actionFlag) {
@@ -135,19 +132,13 @@
     this.canvasContext.restore();
     this.globalData.blendMode = 'source-over';
   }
-  this.contextData.cArrPos -= 1;
-  var popped = this.contextData.saved[this.contextData.cArrPos];
-  var i;
-  var arr = this.contextData.cTr.props;
-  for (i = 0; i < 16; i += 1) {
-    arr[i] = popped[i];
-  }
-  this.canvasContext.setTransform(popped[0], popped[1], popped[4], popped[5], popped[12], popped[13]);
-  popped = this.contextData.savedOp[this.contextData.cArrPos];
-  this.contextData.cO = popped;
-  if (this.globalData.currentGlobalAlpha !== popped) {
-    this.canvasContext.globalAlpha = popped;
-    this.globalData.currentGlobalAlpha = popped;
+  var popped = this.contextData.pop();
+  var transform = popped.transform;
+  var opacity = popped.opacity;
+  this.canvasContext.setTransform(transform[0], transform[1], transform[4], transform[5], transform[12], transform[13]);
+  if (this.globalData.currentGlobalAlpha !== opacity) {
+    this.canvasContext.globalAlpha = opacity;
+    this.globalData.currentGlobalAlpha = opacity;
   }
 };
 
diff --git a/player/js/utils/featureSupport.js b/player/js/utils/featureSupport.js
index 6be62ce..128ebbf 100644
--- a/player/js/utils/featureSupport.js
+++ b/player/js/utils/featureSupport.js
@@ -1,10 +1,15 @@
 const featureSupport = (function () {
   var ob = {
     maskType: true,
+    svgLumaHidden: true,
+    offscreenCanvas: typeof OffscreenCanvas !== 'undefined',
   };
   if (/MSIE 10/i.test(navigator.userAgent) || /MSIE 9/i.test(navigator.userAgent) || /rv:11.0/i.test(navigator.userAgent) || /Edge\/\d./i.test(navigator.userAgent)) {
     ob.maskType = false;
   }
+  if (/firefox/i.test(navigator.userAgent)) {
+    ob.svgLumaHidden = false;
+  }
   return ob;
 }());
 
diff --git a/player/js/utils/helpers/assetManager.js b/player/js/utils/helpers/assetManager.js
new file mode 100644
index 0000000..74e17c3
--- /dev/null
+++ b/player/js/utils/helpers/assetManager.js
@@ -0,0 +1,96 @@
+import createTag from './html_elements';
+import createNS from './svg_elements';
+import featureSupport from '../featureSupport';
+
+var lumaLoader = (function () {
+  var id = '__lottie_element_luma_buffer';
+  var lumaBuffer = null;
+  var lumaBufferCtx = null;
+  var svg = null;
+
+  // This alternate solution has a slight delay before the filter is applied, resulting in a flicker on the first frame.
+  // Keeping this here for reference, and in the future, if offscreen canvas supports url filters, this can be used.
+  // For now, neither of them work for offscreen canvas, so canvas workers can't support the luma track matte mask.
+  // Naming it solution 2 to mark the extra comment lines.
+  /*
+  var svgString = [
+    '<svg xmlns="http://www.w3.org/2000/svg">',
+    '<filter id="' + id + '">',
+    '<feColorMatrix type="matrix" color-interpolation-filters="sRGB" values="',
+    '0.3, 0.3, 0.3, 0, 0, ',
+    '0.3, 0.3, 0.3, 0, 0, ',
+    '0.3, 0.3, 0.3, 0, 0, ',
+    '0.3, 0.3, 0.3, 0, 0',
+    '"/>',
+    '</filter>',
+    '</svg>',
+  ].join('');
+  var blob = new Blob([svgString], { type: 'image/svg+xml' });
+  var url = URL.createObjectURL(blob);
+  */
+
+  function createLumaSvgFilter() {
+    var _svg = createNS('svg');
+    var fil = createNS('filter');
+    var matrix = createNS('feColorMatrix');
+    fil.setAttribute('id', id);
+    matrix.setAttribute('type', 'matrix');
+    matrix.setAttribute('color-interpolation-filters', 'sRGB');
+    matrix.setAttribute('values', '0.3, 0.3, 0.3, 0, 0, 0.3, 0.3, 0.3, 0, 0, 0.3, 0.3, 0.3, 0, 0, 0.3, 0.3, 0.3, 0, 0');
+    fil.appendChild(matrix);
+    _svg.appendChild(fil);
+    _svg.setAttribute('id', id + '_svg');
+    if (featureSupport.svgLumaHidden) {
+      _svg.style.display = 'none';
+    }
+    return _svg;
+  }
+
+  function loadLuma() {
+    if (!lumaBuffer) {
+      svg = createLumaSvgFilter();
+      document.body.appendChild(svg);
+      lumaBuffer = createTag('canvas');
+      lumaBufferCtx = lumaBuffer.getContext('2d');
+      // lumaBufferCtx.filter = `url('${url}#__lottie_element_luma_buffer')`; // part of solution 2
+      lumaBufferCtx.filter = 'url(#' + id + ')';
+      lumaBufferCtx.fillStyle = 'rgba(0,0,0,0)';
+      lumaBufferCtx.fillRect(0, 0, 1, 1);
+    }
+  }
+
+  function getLuma(canvas) {
+    if (!lumaBuffer) {
+      loadLuma();
+    }
+    lumaBuffer.width = canvas.width;
+    lumaBuffer.height = canvas.height;
+    // lumaBufferCtx.filter = `url('${url}#__lottie_element_luma_buffer')`; // part of solution 2
+    lumaBufferCtx.filter = 'url(#' + id + ')';
+    return lumaBuffer;
+  }
+  return {
+    load: loadLuma,
+    get: getLuma,
+  };
+});
+
+function createCanvas(width, height) {
+  if (featureSupport.offscreenCanvas) {
+    return new OffscreenCanvas(width, height);
+  }
+  var canvas = createTag('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  return canvas;
+}
+
+const assetLoader = (function () {
+  return {
+    loadLumaCanvas: lumaLoader.load,
+    getLumaCanvas: lumaLoader.get,
+    createCanvas: createCanvas,
+  };
+}());
+
+export default assetLoader;
diff --git a/player/js/worker_wrapper.js b/player/js/worker_wrapper.js
index f14c89e..449968d 100644
--- a/player/js/worker_wrapper.js
+++ b/player/js/worker_wrapper.js
@@ -1,108 +1,266 @@
 function workerContent() {
+  function extendPrototype(sources, destination) {
+    var i;
+    var len = sources.length;
+    var sourcePrototype;
+    for (i = 0; i < len; i += 1) {
+      sourcePrototype = sources[i].prototype;
+      for (var attr in sourcePrototype) {
+        if (Object.prototype.hasOwnProperty.call(sourcePrototype, attr)) destination.prototype[attr] = sourcePrototype[attr];
+      }
+    }
+  }
+  function ProxyElement(type, namespace) {
+    this._state = 'init';
+    this._isDirty = false;
+    this._isProxy = true;
+    this._changedStyles = [];
+    this._changedAttributes = [];
+    this._changedElements = [];
+    this._textContent = null;
+    this.type = type;
+    this.namespace = namespace;
+    this.children = [];
+    localIdCounter += 1;
+    this.attributes = {
+      id: 'l_d_' + localIdCounter,
+    };
+    this.style = new Style(this);
+  }
+  ProxyElement.prototype = {
+    appendChild: function (_child) {
+      _child.parentNode = this;
+      this.children.push(_child);
+      this._isDirty = true;
+      this._changedElements.push([_child, this.attributes.id]);
+    },
+
+    insertBefore: function (_newElement, _nextElement) {
+      var children = this.children;
+      for (var i = 0; i < children.length; i += 1) {
+        if (children[i] === _nextElement) {
+          children.splice(i, 0, _newElement);
+          this._isDirty = true;
+          this._changedElements.push([_newElement, this.attributes.id, _nextElement.attributes.id]);
+          return;
+        }
+      }
+      children.push(_nextElement);
+    },
+
+    setAttribute: function (_attribute, _value) {
+      this.attributes[_attribute] = _value;
+      if (!this._isDirty) {
+        this._isDirty = true;
+      }
+      this._changedAttributes.push(_attribute);
+    },
+
+    serialize: function () {
+      return {
+        type: this.type,
+        namespace: this.namespace,
+        style: this.style.serialize(),
+        attributes: this.attributes,
+        children: this.children.map(function (child) { return child.serialize(); }),
+        textContent: this._textContent,
+      };
+    },
+
+    // eslint-disable-next-line class-methods-use-this
+    addEventListener: function (_, _callback) {
+      setTimeout(_callback, 1);
+    },
+
+    setAttributeNS: function (_, _attribute, _value) {
+      this.attributes[_attribute] = _value;
+      if (!this._isDirty) {
+        this._isDirty = true;
+      }
+      this._changedAttributes.push(_attribute);
+    },
+
+  };
+
+  Object.defineProperty(ProxyElement.prototype, 'textContent', {
+    set: function (_value) {
+      this._isDirty = true;
+      this._textContent = _value;
+    },
+  });
+
   var localIdCounter = 0;
   var animations = {};
 
   var styleProperties = ['width', 'height', 'display', 'transform', 'opacity', 'contentVisibility', 'mix-blend-mode'];
-  function createElement(namespace, type) {
-    var style = {
-      serialize: function () {
-        var obj = {};
-        for (var i = 0; i < styleProperties.length; i += 1) {
-          var propertyKey = styleProperties[i];
-          var keyName = '_' + propertyKey;
-          if (keyName in this) {
-            obj[propertyKey] = this[keyName];
-          }
+
+  function convertArguments(args) {
+    var arr = [];
+    var i;
+    var len = args.length;
+    for (i = 0; i < len; i += 1) {
+      arr.push(args[i]);
+    }
+    return arr;
+  }
+
+  function Style(element) {
+    this.element = element;
+  }
+  Style.prototype = {
+    serialize: function () {
+      var obj = {};
+      for (var i = 0; i < styleProperties.length; i += 1) {
+        var propertyKey = styleProperties[i];
+        var keyName = '_' + propertyKey;
+        if (keyName in this) {
+          obj[propertyKey] = this[keyName];
         }
-        return obj;
+      }
+      return obj;
+    },
+  };
+  styleProperties.forEach(function (propertyKey) {
+    Object.defineProperty(Style.prototype, propertyKey, {
+      set: function (value) {
+        if (!this.element._isDirty) {
+          this.element._isDirty = true;
+        }
+        this.element._changedStyles.push(propertyKey);
+        var keyName = '_' + propertyKey;
+        this[keyName] = value;
       },
+      get: function () {
+        var keyName = '_' + propertyKey;
+        return this[keyName];
+      },
+    });
+  });
+
+  function CanvasContext(element) {
+    this.element = element;
+  }
+
+  CanvasContext.prototype = {
+    createRadialGradient: function () {
+      function addColorStop() {
+        instruction.stops.push(convertArguments(arguments));
+      }
+      var instruction = {
+        t: 'rGradient',
+        a: convertArguments(arguments),
+        stops: [],
+      };
+      this.element.instructions.push(instruction);
+      return {
+        addColorStop: addColorStop,
+      };
+    },
+
+    createLinearGradient: function () {
+      function addColorStop() {
+        instruction.stops.push(convertArguments(arguments));
+      }
+      var instruction = {
+        t: 'lGradient',
+        a: convertArguments(arguments),
+        stops: [],
+      };
+      this.element.instructions.push(instruction);
+      return {
+        addColorStop: addColorStop,
+      };
+    },
+
+  };
+
+  Object.defineProperties(CanvasContext.prototype, {
+    canvas: {
+      enumerable: true,
+      get: function () {
+        return this.element;
+      },
+    },
+  });
+
+  var canvasContextMethods = [
+    'fillRect',
+    'setTransform',
+    'drawImage',
+    'beginPath',
+    'moveTo',
+    'save',
+    'restore',
+    'fillText',
+    'setLineDash',
+    'clearRect',
+    'clip',
+    'rect',
+    'stroke',
+    'fill',
+    'closePath',
+    'bezierCurveTo',
+    'lineTo',
+  ];
+
+  canvasContextMethods.forEach(function (method) {
+    CanvasContext.prototype[method] = function () {
+      this.element.instructions.push({
+        t: method,
+        a: convertArguments(arguments),
+      });
     };
-    styleProperties.forEach(function (propertyKey) {
-      Object.defineProperty(style, propertyKey, {
-        set: function (value) {
-          if (!element._isDirty) {
-            element._isDirty = true;
-          }
-          element._changedStyles.push(propertyKey);
-          var keyName = '_' + propertyKey;
-          this[keyName] = value;
-        },
-        get: function () {
-          var keyName = '_' + propertyKey;
-          return this[keyName];
+  });
+
+  var canvasContextProperties = [
+    'globalAlpha',
+    'strokeStyle',
+    'fillStyle',
+    'lineCap',
+    'lineJoin',
+    'lineWidth',
+    'miterLimit',
+    'lineDashOffset',
+    'globalCompositeOperation',
+  ];
+
+  canvasContextProperties.forEach(function (property) {
+    Object.defineProperty(CanvasContext.prototype, property,
+      {
+        set: function (_value) {
+          this.element.instructions.push({
+            t: property,
+            a: _value,
+          });
         },
       });
-    });
-    localIdCounter += 1;
-    var element = {
-      _state: 'init',
-      _isDirty: false,
-      _changedStyles: [],
-      _changedAttributes: [],
-      _changedElements: [],
-      _textContent: null,
-      type: type,
-      namespace: namespace,
-      children: [],
-      attributes: {
-        id: 'l_d_' + localIdCounter,
-      },
-      style: style,
-      appendChild: function (child) {
-        child.parentNode = this;
-        this.children.push(child);
-        this._isDirty = true;
-        this._changedElements.push([child, this.attributes.id]);
-      },
-      insertBefore: function (newElement, nextElement) {
-        var children = this.children;
-        for (var i = 0; i < children.length; i += 1) {
-          if (children[i] === nextElement) {
-            children.splice(i, 0, newElement);
-            this._isDirty = true;
-            this._changedElements.push([newElement, this.attributes.id, nextElement.attributes.id]);
-            return;
-          }
-        }
-        children.push(nextElement);
-      },
-      setAttribute: function (attribute, value) {
-        this.attributes[attribute] = value;
-        if (!element._isDirty) {
-          element._isDirty = true;
-        }
-        element._changedAttributes.push(attribute);
-      },
-      serialize: function () {
-        return {
-          type: this.type,
-          namespace: this.namespace,
-          style: this.style.serialize(),
-          attributes: this.attributes,
-          children: this.children.map(function (child) { return child.serialize(); }),
-          textContent: this._textContent,
-        };
-      },
-      getContext: function () { return { fillRect: function () {} }; },
-      addEventListener: function (_, callback) {
-        setTimeout(callback, 1);
-      },
-      setAttributeNS: function (_, attribute, value) {
-        this.attributes[attribute] = value;
-        if (!element._isDirty) {
-          element._isDirty = true;
-        }
-        element._changedAttributes.push(attribute);
-      },
-    };
-    element.style = style;
-    Object.defineProperty(element, 'textContent', {
-      set: function (value) {
-        element._isDirty = true;
-        element._textContent = value;
-      },
-    });
-    return element;
+  });
+
+  function CanvasElement(type, namespace) {
+    ProxyElement.call(this, type, namespace);
+    this.instructions = [];
+    this.width = 0;
+    this.height = 0;
+    this.context = new CanvasContext(this);
+  }
+
+  CanvasElement.prototype = {
+
+    getContext: function () {
+      return this.context;
+    },
+
+    resetInstructions: function () {
+      this.instructions.length = 0;
+    },
+  };
+  extendPrototype([ProxyElement], CanvasElement);
+
+  function createElement(namespace, type) {
+    if (type === 'canvas') {
+      return new CanvasElement(type, namespace);
+    }
+    return new ProxyElement(type, namespace);
   }
 
   var window = self; // eslint-disable-line no-redeclare, no-unused-vars
@@ -117,6 +275,8 @@
     getElementsByTagName: function () {
       return [];
     },
+    body: createElement('', 'body'),
+    _isProxy: true,
   };
   /* eslint-enable */
   var lottieInternal = (function () {
@@ -175,11 +335,17 @@
       var wrapper;
       var animation;
       var elements = [];
+      var canvas;
       if (params.renderer === 'svg') {
         wrapper = document.createElement('div');
         params.container = wrapper;
       } else {
-        var canvas = params.rendererSettings.canvas;
+        canvas = params.rendererSettings.canvas;
+        if (!canvas) {
+          canvas = document.createElement('canvas');
+          canvas.width = params.animationData.w;
+          canvas.height = params.animationData.h;
+        }
         var ctx = canvas.getContext('2d');
         params.rendererSettings.context = ctx;
       }
@@ -248,6 +414,18 @@
             },
           });
         });
+      } else if (canvas._isProxy) {
+        animation.addEventListener('drawnFrame', function (event) {
+          self.postMessage({
+            type: 'CanvasUpdated',
+            payload: {
+              instructions: canvas.instructions,
+              id: payload.id,
+              currentTime: event.currentTime,
+            },
+          });
+          canvas.resetInstructions();
+        });
       }
       animation.addEventListener('DOMLoaded', function () {
         self.postMessage({
@@ -488,6 +666,40 @@
     }
   }
 
+  function createInstructionsHandler(canvas) {
+    var ctx = canvas.getContext('2d');
+    var map = {
+      beginPath: ctx.beginPath,
+      closePath: ctx.closePath,
+      rect: ctx.rect,
+      clip: ctx.clip,
+      clearRect: ctx.clearRect,
+      setTransform: ctx.setTransform,
+      moveTo: ctx.moveTo,
+      bezierCurveTo: ctx.bezierCurveTo,
+      lineTo: ctx.lineTo,
+      fill: ctx.fill,
+      save: ctx.save,
+      restore: ctx.restore,
+    };
+    return function (instructions) {
+      for (var i = 0; i < instructions.length; i += 1) {
+        var instruction = instructions[i];
+        var fn = map[instruction.t];
+        if (fn) {
+          fn.apply(ctx, instruction.a);
+        } else {
+          ctx[instruction.t] = instruction.a;
+        }
+      }
+    };
+  }
+
+  function handleCanvasAnimationUpdate(payload) {
+    var animation = animations[payload.id];
+    animation.instructionsHandler(payload.instructions);
+  }
+
   function handleEvent(payload) {
     var animation = animations[payload.id];
     if (animation) {
@@ -516,6 +728,7 @@
     DOMLoaded: handleAnimationLoaded,
     SVGloaded: handleSVGLoaded,
     SVGupdated: handleAnimationUpdate,
+    CanvasUpdated: handleCanvasAnimationUpdate,
     event: handleEvent,
     playing: handlePlaying,
     paused: handlePaused,
@@ -759,9 +972,17 @@
           }
 
           // Transfer control to offscreen if it's not already
-          var offscreen = canvas instanceof OffscreenCanvas ? canvas : canvas.transferControlToOffscreen();
-          animationParams.rendererSettings.canvas = offscreen;
-          transferedObjects.push(animationParams.rendererSettings.canvas);
+          var transferCanvas = canvas;
+          if (typeof OffscreenCanvas === 'undefined') {
+            animation.canvas = canvas;
+            animation.instructionsHandler = createInstructionsHandler(canvas);
+          } else {
+            if (!(canvas instanceof OffscreenCanvas)) {
+              transferCanvas = canvas.transferControlToOffscreen();
+              animationParams.rendererSettings.canvas = transferCanvas;
+            }
+            transferedObjects.push(transferCanvas);
+          }
         }
         animations[animationId] = animation;
         workerInstance.postMessage({