需求需要在h5页面中做复杂动效,调研了几种常用的方案,跟UI一起尝试。
占用内存对比:
从大到小: 视频>序列帧>GIF>APNG/WEBP>LOTTIE/SVGA
质量稳定对比:
从差到好: 视频<GIF<序列帧<APNG/WEBP<LOTTIE/SVGA
支持AE动效对比:
从多到少:视频>GIF>序列帧>APNG/WEBP>LOTTIE/SVGA
业务方案:
- gif虽然体积小、兼容性好、但效果差、不推荐使用、除非非常在意多端兼容性与性能!
- 简单的动图采用webp、比如简单的聊天表情动图(骰子、石头剪刀布等)
- lottie适合一些复杂的动画、比如复杂的加载动画、引导动画等、不适合做直播间大礼物特效
- 直播间复杂的大礼物动可以用效用svga,webp还有apng
- MP4不建议做直播间礼物动画防范
这里我们的需求是需要较复杂的动效,并且希望可配置,一键替换动效文件,svga和lottie比较符合我们的需求。gif图文件太大,并且不好精细操作;而序列帧也是一样的问题,并且不适合做配置替换。
另外调研阶段发现腾讯出了一款PAG,号称全面支持AE动效,UI也一并做了动效导出。
发现PAG在h5端加载慢,原因是它除了js包和动画文件以外,还要额外引入一个几M的web assembly的包。感觉PAG更适合app这种本地文件实现。
json原先导出后有一个json文件和一个图片文件夹,部署相较Svga单个文件麻烦些。但是现在lottie支持把图片转成base64,导出成单个json文件了。二者在使用上便捷性差不多。导出的文件lottie会大一些。
对这两种类型都做了配置支持。二者的api均支持精细控制。
设计用AE做出效果,通过SVGAConverter插件导出。
引入SVGAPlayer-Web-Lite(https://github.com/svga/SVGAPlayer-Web-Lite)。
然后在页面创建一个canvas对象,设置id。
基本使用如下:
import { Parser, Player } from 'svga' const parser = new Parser() const svga = await parser.load('xx.svga') const player = new Player(document.getElementById('canvas')) await player.mount(svga) player.onStart = () => console.log('onStart') player.onResume = () => console.log('onResume') player.onPause = () => console.log('onPause') player.onStop = () => console.log('onStop') player.onProcess = () => console.log('onProcess', player.progress) player.onEnd = () => console.log('onEnd') // 开始播放动画 player.start() // 暂停播放动画 // player.pause() // 继续播放动画 // player.resume() // 停止播放动画 // player.stop() // 清空动画 // player.clear() // 销毁 // parser.destroy() // player.destroy()
通过配置拿到svga文件的url。先用parser.load载入url,获取文件对象。再用canvas对象初始化player。最后用player挂载svga。
在挂载后打印player对象,可以看到totalFrames可以拿到全部帧数。而在onProcess可以监听到每一帧。这样我们就可以在指定帧操作player。
通过上面demo可以看到,这个娃娃机的动画先是左右晃了几下,然后开始向下抓,然后播放成功动画展示奖品,后面静止不动了,控制台可以看到帧数还在跑,直到最后结束。所以这个动画可以分为几部分:
起始帧(0) -> 抽奖帧 -> 展示帧 -> 结束(totalFrames)
如果我们想要延迟抽奖前的动画,就可以每次执行到抽奖帧再回到起始,把前面的部分循环播放。反过来,如果我们需要点击立即抽奖,可以在点击时跳到抽奖帧。我这里通过三步来实现:
player.pause() player.setConfig({ startFrame: drawFrame, }) player.resume()
通过pause方法暂停,通过setConfig方法设置起始帧,最后resume。
初始化的参数可以设置循环次数,但是这个动画本身就设置了循环,导致播放结束又继续播放了,所以我在播放到快结束时,再次执行了pause,并设置起始帧为结束帧,就当做已经播放完了。
设计用AE做出效果,通过bodymovin插件导出。
引入lottie-web(https://github.com/airbnb/lottie-web)。
这里跟svga有区别的是,创建一个指定id的div元素,而不是canvas。
类似的初始化:
const player = lottie.loadAnimation({ container: element, // the dom element renderer: 'svg', loop: true, autoplay: true, animationData: animationData, // the animation data // ...or if your animation contains repeaters: // animationData: cloneDeep(animationData), // e.g. lodash.clonedeep rendererSettings: { context: canvasContext, // the canvas context, only support "2d" context preserveAspectRatio: 'xMinYMin slice', // Supports the same options as the svg element's preserveAspectRatio property clearCanvas: false, progressiveLoad: false, // Boolean, only svg renderer, loads dom elements when needed. Might speed up initialization for large number of elements. hideOnTransparent: true, //Boolean, only svg renderer, hides elements when opacity reaches 0 (defaults to true) className: 'some-css-class-name', id: 'some-id', } }); player.addEventListener('DOMLoaded', () => { console.log('DOMLoaded: ') // 挂载成功,可以开始获取帧数和渲染 }); player.addEventListener('complete', () => { console.log('complete') }) player.addEventListener('enterFrame', () => { console.log('enterFrame') })
这里可以像svga一样,先通过url请求对象,再初始化,通过animationData传入。也可以直接用path参数传入url。
对player监听事件,类似svga的on方法。
要实现同样的效果,首先监听enterFrame打印帧数,发现打印的不是整数,类似浏览器监听滚动,不太好跟设定的帧数进行比对。
发现除了play方法,还有playSegments方法,可以播放指定帧数区间,并且播放完成后监听到了complete。故而用这个方法来实现逻辑。一段一段的去播放,每一段结束去播放下一段。点击事件时也去执行第二段。
this.player.playSegments( [this.startFrame, this.drawFrame] , true) // baseFn.loadingHide() this.step = 1 this.container.addEventListener('click', this.onLottieClick) // 动画播放完成触发 this.player.addEventListener('complete', () => { console.log('complete: ', this.step) if (this.step === 1) { this.player.playSegments( [this.drawFrame, this.formFrame] , true) this.step = 2 } else if (this.step === 2) { this.$emit('end') this.player.playSegments( [this.formFrame, this.endFrame] , true) this.step = 3 } });
感觉lottie的api更好用一些。
挂载
在挂载完成前获取相关属性值或执行相关api会报错。
跨域
可以看到通过url获取了svga文件或lottie的json文件,这里实际上是发起了一个get请求。所以当我们在后台上传配置文件时要注意跨域问题。
尝试用jsonp来规避这个问题,发现现在chrome浏览器已经默认禁止jsonp。
加载体验
从获取配置,到发起get请求文件下载,到完成渲染,这个时间动画是不可见的,屏幕会展示背景色如白屏等。为了解决这个用户体验,可以先放一个跟动画起始一模一样的图片,这样从图片过渡到动画,用户体验会好很多。