# lottie-web 动画解析

# 前言

Lottie 是一个复杂帧动画的解决方案,它提供了一套从设计师使用 AE(Adobe After Effects)到各端开发者实现动画的工具流。在设计师通过 AE 完成动画后,可以使用 AE 导出一份 JSON 格式的动画数据,然后开发同学可以通过 Lottie 将生成的 JSON 数据渲染成动画。

# 使用介绍

# 1. 基本用法

加载 Lottie 库结合 JSON 文件和下面几行代码就可以实现一个 Lottie 动画

import lottie from 'lottie-web'

const lot = lottie.loadAnimation({
  container: this.$refs.lottieBox,
  renderer: 'svg',
  loop: true,
  autoplay: true, // 是否自动播放
  path: 'data.json' // json地址
  // animationData: ''
})

:json 动画数据,与 path 互斥,建议使用 path,因为 animationData 会将数据打包,使得 js bundle 过大

# 2. 常用方法

lot.play():播放,从当前帧开始播放

lot.stop():停止,并回到第 0 帧

lot.pause():暂停,并保持当前帧

lot.goToAndStop(value, isFrame):跳到某个时刻/帧并停止

isFrame(可省略,默认false:毫秒;true:帧)指明value的单位是毫秒还是帧

lot.goToAndPlay(value, isFrame):跳到某个时刻/帧并播放

lot.goToAndStop(30, true)     // 跳转到第30帧并停止
lot.goToAndPlay(300)          // 跳转到第300毫秒并播放

lot.playSegments(arr, forceFlag):以帧为单位,播放指定片段

arr可以包含两个数字或者两个数字组成的数组,forceFlag表示是否立即强制播放该片段

lot.playSegments([10,20], false)          // 播放完之前的片段,播放10-20帧
lot.playSegments([[0,5],[10,18]], true)   // 直接播放0-5帧和10-18帧

lot.setSpeed(speed):设置播放速度,speed 为 1 表示正常速度

lot.setDirection(direction): 设置播放方向,1 表示正向播放,-1 表示反向播放

lot.destroy(): 删除该动画,移除相应的元素标签等。

# 实现过程

JSON 文件数据格式在此不做解读,接下来结合 Demo (opens new window) 的 Lottie 部分源码阅读展示,了解如何把 JSON 数据动起来的。

# 1. 初始渲染容器

images

AnimationManager.js (opens new window)

function setupAnimation(animItem, element) {
  // 监听事件
  animItem.addEventListener('destroy', removeElement)
  animItem.addEventListener('_active', addPlayingCount)
  animItem.addEventListener('_idle', subtractPlayingCount)
  // 注册动画
  registeredAnimations.push({ elem: element, animation: animItem })
  len += 1
}

function loadAnimation(params) {
  // 生成当前动画实例,AnimationItem方法来源AnimationItem.js
  var animItem = new AnimationItem()
  // 注册动画
  setupAnimation(animItem, null)
  // 初始化动画实例参数
  animItem.setParams(params)
  return animItem
}
  • AnimationItem 这个类是 Lottie 动画的基类,loadAnimation 方法会先生成一个 AnimationItem 实例并返回,开发者使用的 配置参数和方法 都是来自于这个类。

  • 生成 animItem 实例后,调用 setupAnimation 方法。这个方法首先监听了 destroy、_active、_idle 三个事件等待被触发。由于可以多个动画并行,因此定义了全局的变量 len、registeredAnimations 等,用于判断和缓存已注册的动画实例。

  • 接下来调用 animItem 实例的 setParams 方法初始化动画参数,除了初始化 loop 、 autoplay 等参数外,最重要的是选择渲染器,如下:

AnimationItem.prototype.setParams = function (params) {
  // 渲染容器
  if (params.wrapper || params.container) {
    this.wrapper = params.wrapper || params.container
  }
  // 默认是svg格式
  var animType = 'svg'
  if (params.animType) {
    animType = params.animType
  } else if (params.renderer) {
    animType = params.renderer
  }
  // 根据配置选择渲染器,如果不存在走html类型
  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
  }
  this.imagePreloader.setCacheType(animType, this.renderer.globalData.defs)
  this.renderer.setProjectInterface(this.projectInterface)
  this.animType = animType
  if (
    params.loop === '' ||
    params.loop === null ||
    params.loop === undefined ||
    params.loop === true
  ) {
    this.loop = true
  } else if (params.loop === false) {
    this.loop = false
  } else {
    this.loop = parseInt(params.loop, 10)
  }
  // 默认自动播放
  this.autoplay = 'autoplay' in params ? params.autoplay : true
  // 动画名称
  this.name = params.name ? params.name : ''
  this.autoloadSegments = Object.prototype.hasOwnProperty.call(params, 'autoloadSegments')
    ? params.autoloadSegments
    : true
  this.assetsPath = params.assetsPath
  this.initialSegment = params.initialSegment
  if (params.audioFactory) {
    this.audioController.setAudioFactory(params.audioFactory)
  }
  // 渲染数据初始化
  if (params.animationData) {
    this.setupAnimation(params.animationData)
  } else if (params.path) {
    if (params.path.lastIndexOf('\\') !== -1) {
      this.path = params.path.substr(0, params.path.lastIndexOf('\\') + 1)
    } else {
      this.path = params.path.substr(0, params.path.lastIndexOf('/') + 1)
    }
    this.fileName = params.path.substr(params.path.lastIndexOf('/') + 1)
    this.fileName = this.fileName.substr(0, this.fileName.lastIndexOf('.json'))
    // 源码在 player/js/utils/dataManager.js
    // 创建一个Worker子线程去加载json数据
    dataManager.loadAnimation(params.path, this.configAnimation, this.onSetupError)
  }
}

Lottie 提供了 SVG、Canvas 和 HTML 三种渲染模式,默认 SVG 模式。

  • SVG 渲染器支持的特性最多,也是使用最多的渲染方式。并且 SVG 是可伸缩的,任何分辨率下不会失真。

  • Canvas 渲染器就是根据动画的数据将每一帧的对象不断重绘出来。

  • HTML 渲染器受限于其功能,支持的特性最少,只能做一些很简单的图形或者文字,也不支持滤镜效果。

每个渲染器均有各自的实现,复杂度也各有不同,但是动画越复杂,其对性能的消耗也就越高,这些要看实际的状况再去判断。渲染器源码在 player/js/renderers/ 文件夹下。

# 2. 初始化动画属性,加载静态资源

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])
      this.firstFrame = Math.round(this.initialSegment[0])
    } else {
      this.totalFrames = Math.floor(this.animationData.op - this.animationData.ip)
      this.firstFrame = Math.round(this.animationData.ip)
    }
    // 渲染器初始化参数
    this.renderer.configAnimation(animData)
    if (!animData.assets) {
      animData.assets = []
    }

    this.assets = this.animationData.assets
    // 帧率
    this.frameRate = this.animationData.fr
    this.frameMult = this.animationData.fr / 1000
    this.renderer.searchExtraCompositions(animData.assets)
    this.markers = markerParser(animData.markers || [])
    this.trigger('config_ready')
    // 加载静态资源
    this.preloadImages()
    this.loadSegments()
    this.updaFrameModifier()
    // 等待静态资源加载完毕
    this.waitForFontsLoaded()
    // 暂停声音
    if (this.isPaused) {
      this.audioController.pause()
    }
  } catch (error) {
    this.triggerConfigError(error)
  }
}

在这个方法中将会初始化更多动画对象的属性,比如总帧数 totalFrames 、帧率 frameMult 等。然后加载一些其他资源,比如图像、字体等。如下图所示:

images

同时在 waitForFontsLoaded 方法中等待静态资源加载完毕,加载完毕后便会调用 SVG 渲染器的 initItems 方法绘制动画图层,也就是将动画绘制出来。

AnimationItem.prototype.waitForFontsLoaded = function () {
  if (!this.renderer) {
    return
  }
  // 检查加载完毕
  if (this.renderer.globalData.fontManager.isLoaded) {
    this.checkLoaded()
  } 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
    if (expressionsPlugin) {
      expressionsPlugin.initExpressions(this)
    }
    // 初始化所有元素
    this.renderer.initItems()
    setTimeout(
      function () {
        this.trigger('DOMLoaded')
      }.bind(this),
      0
    )
    // 渲染第一帧
    this.gotoFrame()
    // 自动播放
    if (this.autoplay) {
      this.play()
    }
  }
}

在 checkLoaded 方法中可以看到,通过 initItems 初始化所有元素后,便通过 gotoFrame 渲染第一帧,如果开发者配置了 autoplay 为 true,则会直接调用 play 方法播放。接下来先看 initItems 实现细节。

# 3. 绘制动画初始图层

initItems 方法主要是调用 buildAllItems 创建所有图层。buildItem 方法又会调用 createItem 确定具体图层类型,下面看下 createItem 源码 player/js/renderers/BaseRenderer.js 实现:

images

在制作动画时,设计师操作的图层元素有很多种,比如图片、形状、文字等等。所以 layers 中每个图层会有一个字段 ty 来区分。结合 createItem 方法来看,一共有以下 9 中类型。

BaseRenderer.prototype.createItem = function(layer) {
  // 根据图层类型,创建相应的 svg 元素类的实例
  switch (layer.ty) {
    case 2:
      return this.createImage(layer); // 图片
    case 0:
      return this.createComp(layer); // 合成
    case 1:
      return this.createSolid(layer); // 固态
    case 3:
      return this.createNull(layer); // 空元素
    case 4:
      return this.createShape(layer); // 形状
    case 5:
      return this.createText(layer); // 文字
    case 6:
      return this.createAudio(layer); // 音频
    case 13:
      return this.createCamera(layer); // 摄像机
    case 15:
      return this.createFootage(layer); // 镜头
    default:
      return this.createNull(layer);
};

如上所知,图层类型的渲染逻辑,如 Image、Text、Audio 等等,每一种元素的渲染逻辑都实现在源码 player/js/elements/ 文件夹下,具体实现逻辑这里就不进行展开了,感兴趣的同学自行查看。

# 4. 动画播放

图层绘制完毕后,就来看看动画播放。Lottie 动画播放主要是使用 AnimationItem 实例的 play 方法。如果开发者配置了 autoplay 为 true,则会在所有初始化工作准备完毕后,直接调用 play 方法播放。否则由开发者主动调用 play 方法播放。

接下来从 play 方法了解一下整个播放流程的细节

AnimationItem.prototype.play = function (name) {
  if (name && this.name !== name) {
    return
  }
  if (this.isPaused === true) {
    this.isPaused = false
    this.audioController.resume()
    if (this._idle) {
      this._idle = false
      this.trigger('_active')
    }
  }
}

play 方法主要是触发了 _active 事件,这个 _active 事件便是初始化时注册的,该事件调用 addPlayingCount 方法,addPlayingCount 里再调用 activate 方法,如下:

function activate() {
  if (!_isFrozen && playingAnimationsNum) {
    if (_stopped) {
      // 触发第一帧渲染
      window.requestAnimationFrame(first)
      _stopped = false
    }
  }
}

触发后通过调用 requestAnimationFrame 方法,不断的调用 resume 方法来控制动画。

function first(nowTime) {
  initTime = nowTime
  // requestAnimationFrame 每次都进行计算修改 DOM
  window.requestAnimationFrame(resume)
}

requestAnimationFrame 在正常情况下能达到 60 fps(每隔 16.7ms 左右)。通过 requestAnimationFrame 方法,每隔 16.7ms 去计算,计算的更细致而已,而且还会使得动画更流畅。来看下resume源码是如何处理的。

function resume(nowTime) {
  // 两次 requestAnimationFrame 间隔时间
  var elapsedTime = nowTime - initTime
  var i
  for (i = 0; i < len; i += 1) {
    registeredAnimations[i].animation.advanceTime(elapsedTime)
  }
  initTime = nowTime
  if (playingAnimationsNum && !_isFrozen) {
    window.requestAnimationFrame(resume)
  } else {
    _stopped = true
  }
}
  • 首先会计算当前时间和上次时间的 diff 时间。

  • 之后计算动画开始到现在的时间的当前帧数。

  • 最后通过 renderFrame() 方法更新当前帧对应的 DOM 变化。

在 Lottie 中,每一次变化是根据 requestAnimationFrame 的间隔(每隔 16.7ms 左右)计算了更细致,保证动画的流畅运行。requestAnimationFrame 采用系统时间间隔,保持最佳绘制效率,让动画能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

# 生成 SVG

images

# 总结

  1. Lottie 的优势
  • 动画还原度高,使动画实现更加方便,动画效果也更好。
  • SVG 是可伸缩的,任何分辨率下不会失真。
  • JSON 文件,可以多端复用(Web、Android、iOS、React Native)。
  • JSON 文件大小会比 GIF 以及 APNG 等文件小很多,性能也会更好。
  1. Lottie 的不足
  • Lottie-web 文件本身仍然比较大,未压缩lottie.js大小为 532k,轻量版压缩后lottie_light.min.js也有 147k。所以,需要注意 Lottie-web 的加载。
  • 不必要的序列帧。Lottie 的主要动画思想是绘制某一个图层不断的改变 CSS 属性,如果用了一些插件实现的动画效果,可能会造成每一帧都是一张图,那就会造成这个 JSON 文件非常大。
  • 部分 AE 特效不支持。有少量的 AE 动画效果,Lottie 无法实现,有些是因为性能问题,有些是没有做。
  • Lottie 动画其实可以理解为 svg/canvas 动画,不能给已存在的 html 添加动画效果。