Why Lottie
Recently, I was doing an activity of auto show. H5 involves a lot of cool animations, so I looked for a ready-made animation library. After investigation, I found Lottie is more suitable, and it is also the most widely used library in the market.
“Lottie is a library for Android, iOS, Web, and Windows that parses Adobe After Effects animations exported as json with Bodymovin and renders them natively on mobile and on the web!”
Lottie parses jSON-formatted animation data that designers export via Adobe After Effects and renders them.
At present, the general implementation of multi-frame animation is:
- The front-end implementation
SVG and canvas
Animation (high implementation cost, high maintenance cost) - The designer is cut
gif
(Large file, easily jagged) png
Sequence frames (large files, easily jagged)
The current animation implementation scheme has its own problems, so we need to find a more simple, efficient and high performance animation scheme. Airbnb’s Lottie is a good animation solution.
1. How to implement a Lottie animation
- Designers use AE to create animations.
- Export the animation to JSON data files using Lottie’s Bodymovin plugin.
- Load the Lottie library and combine the JSON animation file with the following lines of code to implement a Lottie animation.
import lottie from 'lottie-web';
import animationJson from './lottieJson.json';
const anim = lottie.loadAnimation({
container: document.querySelector('.lottie-container'), // the dom element that will contain the animation
renderer: 'svg'.loop: false.autoplay: false.animationData: animationJson,
rendererSettings: {
progressiveLoad: true.preserveAspectRatio: 'xMidYMid slice'.imagePreserveAspectRatio: 'xMidYMid slice',}}); anim.play();Copy the code
2. Analyze the JSON data format of Lottie animation
2.1 Overall data structure
{
"v": "5.1.13"./ / bodymovin version
"fr": 25./ / frame rate
"ip": 0.// Start keyframe
"op": 46.// End the keyframe
"w": 750./ / wide view
"h": 1625./ / view
"nm": "818 Bus Night - Red envelope"./ / name
"ddd": 0.// Whether it is 3D
"assets": [].// Resource collection
"layers": [].// Layer collection (contains each layer and action information in the animation)
"masker": [].// Select * from *
"comps": [] // Compose layers
Copy the code
Here, FR, IP, OP, Layers and assets may need attention in practical application, especially the first three are particularly important.
2.2 Layer data structure
"ddd": 0.// Whether it is 3D
"ind": 15.// Layer ID, unique
"ty": 2.// Layer type
"nm": "light /新的.psd".// Layer name
"cl": "psd"."parent": 32."refId": "image_0"."sr": 1."ks": { / / change. Corresponding to the transform Settings in AE
"o": { "a": 0."k": 100."ix": 11 }, / / transparency
"r": {... },/ / rotation
"p": {... }, / / position
"a": {... },/ / the anchor
"s": { / / scale zooming
"a": 1."k": [{"i": { "x": [0.48.0.48.0.48]."y": [1.1.1]},"o": { "x": [0.26.0.26.0.26]."y": [1.01.1.01.0]},"t": 7.// The number of key frames (0-7 frames)
"s": [0.0.100].// represents before the change (layer is two-dimensional).
"e": [99.99.100] // represents the change (layer is two-dimensional).
},
{
"i": { "x": [0.833.0.833.0.833]."y": [1.1.1]},"o": { "x": [0.167.0.167.0.167]."y": [0.0.0]},"t": 18."s": [99.99.100]."e": [99.99.100] }, ] }, }, shapes:[...] .// Layer width and height information
"ao": 0."ip": 0."op": 150."st": 0."bm": 0
Copy the code
3. Combine source code and JSON interpretation
Above, we have understood the meaning of data attributes in JSON. Next, we will analyze and sort out ideas based on Lottie source code and Demo to understand the execution process and principle of Lottie.
3.1 Initializing the renderer
Next, let’s look at the source code to see how Lottie initializes the animation through the loadAnimation method. The renderer initialization process is as follows:
// AnimationManager.js source code
var len =0
function registerAnimation(element, animationData) {
if(! element) {return null;
}
var i = 0;
while (i < len) {
if(registeredAnimations[i].elem === element && registeredAnimations[i].elem ! = =null) {
return registeredAnimations[i].animation;
}
i += 1;
}
var animItem = new AnimationItem();
setupAnimation(animItem, element);
animItem.setData(element, animationData);
return animItem;
}
function setupAnimation(animItem, element) {
// Listen on events
animItem.addEventListener('destroy', removeElement);
animItem.addEventListener('_active', addPlayingCount);
animItem.addEventListener('_idle', subtractPlayingCount);
// Register the animation
registeredAnimations.push({ elem: element, animation: animItem });
len += 1;
}
function loadAnimation(params) {
var animItem = new AnimationItem(); // Generate the current animation instance
setupAnimation(animItem, null); // Register the animation
animItem.setParams(params); // Initialize the animation instance parameters
return animItem;
}
Copy the code
// Real application
var anim = lottie.loadAnimation({
container: lottieRef.current, // the dom element that will contain the animation
renderer: 'svg'.loop: false.autoplay: false.animationData: animJson,
rendererSettings: {
progressiveLoad: true.preserveAspectRatio: 'xMidYMid slice'.imagePreserveAspectRatio: 'xMidYMid slice',}});Copy the code
-
The loadAnimation method succeeds the AnimationItem base class, which generates instances and returns them. For details, see configuration parameters and methods.
-
After the animItem instance is generated, the setupAnimation method is called. This method first listens for destroy, _active, and _idle events to be triggered. Since multiple animations can be performed in parallel, global variables len, registeredAnimations, and so on are defined to determine and cache registered instances of animations.
-
The setParams method of the animItem instance is then called to initialize the animation parameters. In addition to initializing parameters such as loop, autoplay, and most importantly, selecting the renderer.
AnimationItem.prototype.setParams = function (params) {
if (params.wrapper || params.container) {
this.wrapper = params.wrapper || params.container;
}
var animType = 'svg';
if (params.animType) {
animType = params.animType;
} else if (params.renderer) {
animType = params.renderer;
}
// Renderer type
switch (animType) {
case 'canvas':
this.renderer = new CanvasRenderer(this, params.rendererSettings);
break;
case 'svg':
this.renderer = new SVGRenderer(this, params.rendererSettings);
break;
default:
this.renderer = new HybridRenderer(this, params.rendererSettings);
break;
}
// ...
if (params.animationData) {
this.configAnimation(params.animationData);// Render initialization parameters
}
// ...
};
Copy the code
Lottie provides SVG, Canvas, and HTML rendering modes, and generally uses either the first or the second.
- The SVG renderer supports the most features and uses the most rendering methods. And SVG is scalable, with no distortion at any resolution.
- Canvas renderer is to continuously redraw the object of each frame according to the data of animation.
- HTML renderers are limited by their capabilities and support minimal features. They can only do very simple graphics or text and do not support filters.
Each of the above renderers has its own implementation, which varies in complexity, and the more complex the animation, the greater the performance drain. If you want to know more about renderers, you can check the source code in the player/js/renderers directory. Here I only do specific analysis for SVG.
3.2 Initialize loading of animation and static resources
The setParams method calls configAnimation to initialize the parameters.
AnimationItem.prototype.configAnimation = function (animData) {
if (!this.renderer) {
return;
}
try {
this.animationData = animData;
if (this.initialSegment) {
this.totalFrames = Math.floor(this.initialSegment[1] - this.initialSegment[0]); / / the total number of frames
this.firstFrame = Math.round(this.initialSegment[0]);// The first frame
} else {
this.totalFrames = Math.floor(this.animationData.op - this.animationData.ip);
this.firstFrame = Math.round(this.animationData.ip);
}
this.renderer.configAnimation(animData); // // Renderer initialization parameters
if(! animData.assets) { animData.assets = []; }this.assets = this.animationData.assets;// Resource collection
this.frameRate = this.animationData.fr; / / frame rate
this.frameMult = this.animationData.fr / 1000;
this.renderer.searchExtraCompositions(animData.assets);
this.markers = markerParser(animData.markers || []); // Select * from *
this.trigger('config_ready');
// Load a static resource
this.preloadImages(); // Preload images
this.loadSegments();
this.updaFrameModifier(); / / update the frame
this.waitForFontsLoaded(); // Wait until the resource is loaded
if (this.isPaused) {
this.audioController.pause(); }}catch (error) {
this.triggerConfigError(error); }};Copy the code
The above method initializes most of the properties and then loads static resources such as images, fonts, and so on.
The SVGRenderer rendering process is as follows:
The following iswaitForFontsLoaded
andcheckLoaded
Source:
AnimationItem.prototype.waitForFontsLoaded = function () {
if (!this.renderer) {
return;
}
if (this.renderer.globalData.fontManager.isLoaded) {
this.checkLoaded(); // Check whether Loaded is complete
} else {
setTimeout(this.waitForFontsLoaded.bind(this), 20); }}; AnimationItem.prototype.checkLoaded =function () {
if (!this.isLoaded
&& this.renderer.globalData.fontManager.isLoaded
&& (this.imagePreloader.loadedImages() || this.renderer.rendererType ! = ='canvas')
&& (this.imagePreloader.loadedFootages())
) {
this.isLoaded = true;
dataManager.completeData(this.animationData, this.renderer.globalData.fontManager);
if (expressionsPlugin) {
expressionsPlugin.initExpressions(this);
}
// Initialize all elements
this.renderer.initItems();
setTimeout(function () {
this.trigger('DOMLoaded');
}.bind(this), 0);
Render the first frame
this.gotoFrame();
// Auto play
if (this.autoplay) {
this.play(); }}};Copy the code
The checkLoaded method initializes all layers through initItems, then renders the first frame, and then calls the animation play operation with the autoplay property configured to be true. How did initItems convert layers of different types?
3.3 How to draw different types of initial layers in animation
From the svGrender.js source code, SVGRenderer inherits the BaseRenderer base class, and initItems is also its base class method. The code is as follows:
BaseRenderer.prototype.initItems = function () {
if (!this.globalData.progressiveLoad) {
this.buildAllItems();// Convert the sublayer to the corresponding element}};Copy the code
The buildAllItems method is called, which in turn uses SVGRenderer’s own buildItem method. The buildItem method then executes the base class createItem method as follows:
BaseRenderer.prototype.buildAllItems = function () {
var i;
var len = this.layers.length;
for (i = 0; i < len; i += 1) {
this.buildItem(i); // Implements SVGRenderer's own buildItem method
}
this.checkPendingElements();
};
SVGRenderer.prototype.buildItem = function (pos) {
var elements = this.elements;
if (elements[pos] || this.layers[pos].ty === 99) {
return;
}
elements[pos] = true;
var element = this.createItem(this.layers[pos]); // Create the element
elements[pos] = element; // Add elements to SVG
// ...
};
BaseRenderer.prototype.createItem = function (layer) {
// Create an instance of the corresponding SVG element class according to the layer type
switch (layer.ty) {
case 2:
return this.createImage(layer);/ / picture
case 0:
return this.createComp(layer); // Compose layers
case 1:
return this.createSolid(layer);
case 3:
return this.createNull(layer); / / empty elements
case 4:
return this.createShape(layer); // Shape layer
case 5:
return this.createText(layer); / / text
case 6:
return this.createAudio(layer); / / audio
case 13:
return this.createCamera(layer); / / the camera
case 15:
return this.createFootage(layer); / / material
default:
return this.createNull(layer); }};Copy the code
The rendering logic of layer types, such as Image, Text, etc., is implemented in the source code player/js/elements/ folder, interested students to check themselves. The flow chart is as follows:
The following uses the SVGCompElement class as an example to show how to create an instance.
function SVGCompElement(data,globalData,comp){ // Compose layers
this.layers = data.layers; // Contains layers
this.elements = this.layers ? createSizedArray(this.layers.length) : []; // Create a child element based on layers
this.initElement(data,globalData,comp);
this.tm = data.tm ? PropertyFactory.getProp(this,data.tm,0,globalData.frameRate,this) : {_placeholder:true};
}
ICompElement.prototype.initElement = function(data,globalData,comp) {
this.initFrame();
this.initBaseData(data, globalData, comp); // Set the layer parameters
this.initTransform(data, globalData, comp); // Get data related to transform
this.initRenderable();
this.initHierarchy();
this.initRendererElement();
this.createContainerElements(); // Create a g element that will contain child elements, and set the attributes of the G element (transform, filter, mask, id, etc.) based on the previous initialization participation.
this.addMasks();
if(this.data.xt || ! globalData.progressiveLoad){this.buildAllItems(); // Convert the sublayer to the corresponding element
}
this.hide();
};
Copy the code
Ks transform
Ks corresponds to the transformation properties of the layer in AE. You can control the layer by setting anchor point, position, rotation, zoom, transparency, etc., and set the transformation curve of these properties to achieve animation. Here is a ks attribute value:
"ks": { / / change. Corresponding to the transform Settings in AE
"o": { / / transparency
"a": 0."k": 100."ix": 11
},
"r": { / / rotation
"a": 0."k": 0."ix": 10
},
"p": { / / position
"a": 0."k": [- 167..358.125.0]."ix": 2
},
"a": { / / the anchor
"a": 0."k": [667.375.0]."ix": 1
},
"s": { / / zoom
"a": 0."k": [100.100.100]."ix": 6}}Copy the code
Lottie-web handles ks as a transform property, which is used to transform elements. Transform includes translate, scale, rotate, and skew. The relevant code for handling KS (transformation) in Lottie – Web is:
function TransformProperty(elem, data, container) {
this.elem = elem;
this.frameId = -1;
this.propType = 'transform';
this.data = data;
this.v = new Matrix();
// Precalculated matrix with non animated properties
this.pre = new Matrix();
this.appliedTransformations = 0;
this.initDynamicPropertyContainer(container || elem);
// Get parameters related to translate
if (data.p && data.p.s) {
this.px = PropertyFactory.getProp(elem, data.p.x, 0.0.this);
this.py = PropertyFactory.getProp(elem, data.p.y, 0.0.this);
if (data.p.z) {
this.pz = PropertyFactory.getProp(elem, data.p.z, 0.0.this); }}else {
this.p = PropertyFactory.getProp(elem, data.p || { k: [0.0.0]},1.0.this);
}
// Obtain the parameters related to rotate
if (data.rx) {
this.rx = PropertyFactory.getProp(elem, data.rx, 0, degToRads, this);
this.ry = PropertyFactory.getProp(elem, data.ry, 0, degToRads, this);
this.rz = PropertyFactory.getProp(elem, data.rz, 0, degToRads, this);
if (data.or.k[0].ti) {
var i;
var len = data.or.k.length;
for (i = 0; i < len; i += 1) {
data.or.k[i].to = null;
data.or.k[i].ti = null; }}this.or = PropertyFactory.getProp(elem, data.or, 1, degToRads, this);
// sh Indicates it needs to be capped between -180 and 180
this.or.sh = true;
} else {
this.r = PropertyFactory.getProp(elem, data.r || { k: 0 }, 0, degToRads, this);
}
// Get skew-related parameters
if (data.sk) {
this.sk = PropertyFactory.getProp(elem, data.sk, 0, degToRads, this);
this.sa = PropertyFactory.getProp(elem, data.sa, 0, degToRads, this);
}
// Get parameters related to translate
this.a = PropertyFactory.getProp(elem, data.a || { k: [0.0.0]},1.0.this);
// Get parameters related to scale
this.s = PropertyFactory.getProp(elem, data.s || { k: [100.100.100]},1.0.01.this);
// Opacity is not part of the transform properties, that's why it won't use this.dynamicProperties. That way transforms won't get updated if opacity changes.
/ / transparency
if (data.o) {
this.o = PropertyFactory.getProp(elem, data.o, 0.0.01, elem);
} else {
this.o = { _mdf: false.v: 1 };
}
this._isDirty = true;
if (!this.dynamicProperties.length) {
this.getValue(true); }}Copy the code
function getTransformProperty(elem,data,container){ // data indicates ks data
return new TransformProperty(elem,data,container);
}
Copy the code
function applyToMatrix(mat) {
var _mdf = this._mdf;
this.iterateDynamicProperties();
this._mdf = this._mdf || _mdf;
if (this.a) {
mat.translate(-this.a.v[0] -this.a.v[1].this.a.v[2]);
}
if (this.s) {
mat.scale(this.s.v[0].this.s.v[1].this.s.v[2]);
}
if (this.sk) {
mat.skewFromAxis(-this.sk.v, this.sa.v);
}
if (this.r) {
mat.rotate(-this.r.v);
} else {
mat.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2])
.rotateY(this.or.v[1])
.rotateX(this.or.v[0]);
}
if (this.data.p.s) {
if (this.data.p.z) {
mat.translate(this.px.v, this.py.v, -this.pz.v);
} else {
mat.translate(this.px.v, this.py.v, 0); }}else {
mat.translate(this.p.v[0].this.p.v[1] -this.p.v[2]); }}Copy the code
shape
Shape, which corresponds to the shape Settings in the contents of the layer in After Effects and is used to draw graphics. The following shape json is an example:
"shapes": [{
"ty": "gr"./ / type. Mixing layer
"it": [{ // Layer json
"ind": 0."ty": "sh".// type, where sh represents the graph path
"ix": 1."ks": {
"a": 0."k": {
"i": [ // Set of inner tangent points
[0.0],
[0.0]],"o": [ // Set of tangent points
[0.0],
[0.0]],"v": [ // Vertex coordinates set
[182, -321.75],
[206.25, -321.75]],"c": false // The Bessel path is closed
},
"ix": 2
},
"nm": Path of "1"."mn": "ADBE Vector Shape - Group"."hd": false}, {"ty": "st"./ / type. Graphics stroke
"c": { // Line color
"a": 0."k": [0.0.0.1]."ix": 3
},
"o": { // Line opacity
"a": 0."k": 100."ix": 4
},
"w": { // Line width
"a": 0."k": 3."ix": 5
},
"lc": 2.// Line segment header and tail styles
"lj": 1.// Line segment join style
"ml": 4.// Angular limit
"nm": "Stroke 1"."mn": "ADBE Vector Graphic - Stroke"."hd": false}}]]Copy the code
From the JSON example of shape shape above, you can see that different shape types have different parameters. Shape corresponds to the contents of the layer in After Effects. The TY field in shape indicates the type of shape. Ty has the following types:
- Gr: graph merge
- St: Graphic stroke
- Fl: Graph fill
- Tr: Graph transformation
- Sh: graph path
- El: elliptical path
- Rc: rectangle path
- Tm: Clipping paths
4, summarize
Although we have a general understanding of the implementation principle of Lottie, there are some advantages and disadvantages in practical application, and we need to make choices according to the actual situation of the project.
4.1 Advantages of Lottie
The Lottie method scheme is animated by the designer, exported to JSON, and analyzed by the front end. So, the benefits of using the Lottie scheme are:
- Animation by design using professional animation production tools
Adobe After Effects
To achieve, make animation more convenient, animation effect is better; - The front end can easily call the animation, and control the animation, reduce the front-end animation workload;
- Animation design and production, front-end display animation, professional people do professional work, reasonable division of labor;
- 100 percent restore, SVG is scalable and does not distort at any resolution;
- With Lottie, JSON files are much smaller than GIF files and have better performance. Json files can be reused from multiple sources (Web, Android, iOS, React Native).
4.2 Deficiencies of Lottie
- The Lottie-Web file is relatively large, lottie.js is 532K in size, the lightweight version is also 150K compressed, and gzip is 43K in size.
- If the designer builds a lot of layers, there may still be a problem with json files, which requires the designer to follow certain design rules. For example, the designer is lazy to use plug-ins to implement animation, which may result in each frame being a single image, as shown in the following picture:
This can result in JSON files that are very large and require prior consultation with the designer.
- There are also some compatibility problems in the actual application, different manufacturers of mobile phones and models of different animation rendering effects have certain differences.
5. Reference materials
-
airbnb.io/lottie/#/
-
Github.com/airbnb/lott…
-
window.requestAnimationFrame