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({