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 implementationSVG and canvasAnimation (high implementation cost, high maintenance cost)
  • The designer is cutgif(Large file, easily jagged)
  • pngSequence 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 iswaitForFontsLoadedandcheckLoadedSource:

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 toolsAdobe After EffectsTo 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