不会写动画的前端不是好前端
/ / 点击 / 阅读耗时 14 分钟一、背景
2019年底公司计划推出一个地图需求,整个需求大致分为三幕动画:
1、第一幕
根据用户所在省份,画出用户的头像,然后画出一条贝塞尔曲线,箭头最终指向用户第一个受助人所在的省份。
2、第二幕
受助人头像开始下落变成红点,同时放大所在区域,到指定比例。 此时出现录音弹框,开始自动播放受助人录音。录音播放完毕后,背景地图恢复原始尺寸。
3、第三幕
其他受助人头像开始一个个出现的同时,并且下落到地图上,逐渐变小成为地图上的一个点,这个过程重复出现,速度越来越快,最终形成一个红点闪闪的地图。
由于页面采用大量的动画进行交互,所以第一步主要考虑如何选择合适的动画实现方案。
二、技术选型
1、实现动画的方法
前端可以使用哪些方式实现动画呢?
1、最省事的办法就是vedio或gif图片了,优点是,沟通成本低;缺点就是在色彩,透明度表现力不如png, 且交互性差。
2、Flash,手机端就别考虑了。不久的未来,也会被淘汰。
3、js + CSS3 animation, 应该是目前前端广泛使用的方式了。具有开发成本低、兼容性好、具备互动性,加上 PNG 图片很好的色彩与透明度的表现力基本能满足很多动画需求
4、Canvas, 无须插件,浏览器支持良好,可以绘制各种图形以及具备高性能图片渲染能力,缺点是,纯手工编写canvas成本较高,动画实现较为繁琐。
5、WebGL 在Canvas的基础上,增加了 OpenGL ES 的 3D 绘制能力,并且能开启硬件 3D 加速渲染,让前端动画的世界充满了更多想象的空间。
2、为什么选择Canvas
vedio或gif的适合与用户数据无关的交互。
通过JS改变Dom节点的css属性虽然具有开发成本低、兼容性好、具备互动性等优点,但是在复杂场景下,性能会是问题,不是特别流畅。
Canvas, 无须插件,浏览器支持良好,可以绘制各种图形以及具备高性能图片渲染能力,缺点是,纯手工编写canvas成本较高,动画实现较为繁琐,
不过可以通过一定的方法封装解决。
3、面临的挑战
这个需求要同时满足性能和业务的要求,面临不少的挑战:
场景多:每个用户看到的动画都是不一样的,还要考虑受助人与用户头像省份重叠的问题,箭头的指向角度问题等等。
动画多:很多的精灵动画,包括头像的出现,贝塞尔曲线,头像的下落,地图局部放大缩小等。
图片多:设计提供100多张图片。前端要想办法减少加载的图片的数量,为了保证动画的流畅,需要提前把第一幕图片提前加载好。
三、实现方案
3.1 图片资源预加载
预加载
1、为确保用户看到的动画是平滑流畅的,需要进行图片预加载,包括:用户头像(受助人20个头像),地图背景(含:大,小两套),头像外框角标等:
图片预加载函数通过Promise实现:
1 | /** |
函数做了一个小优化,如果图片第一次由于流量大等原因没有加载成功,会进行二次加载,为避免循环加载,限制了每张图片最多加载的次数为3.
利用 Promise.all
等待所有图片全部加载成功, 将所有图片对象全部返回:
1 | export const loadImages = async (avatorUrl ...) => { |
执行时机
首次进入活动页面时会先进入 loading
页面,所以说 loading
页面是最好的执行资源预加载的时机:
这块的具体实现逻辑:
1 | <!-- loading组件 --> |
countTo组件定义了初始值0,最终值99,整个动画完成时间3s
loadFinish()
将在动画完成时调用:
1 | export default Vue.extend({ |
这里定义了一个 this.loadFinished
变量,初始值为 false。 这里为什么需要设计这个变量呢?
还记得我们之前定义的图片预加载函数吗,我们在页面是这样使用的:
1 | export default Vue.extend({ |
在loadImages()
的回调函数里面也调用了 loadFinish()
函数,this.loadFinished
变量在第一次进入时,走的else分支,为 this.loadFinished
赋值为 true, 下一次再次进入 loadFinish()
时,就满足了if 分支的条件,所以就会进入主页面。
3.2 动画模块设计
1 | activity |
Sprite.js 提供了动画类基础的方法:
Avator,Connect,Landing, Map全部继承了Sprite,也就是继承了Sprite的方法。同时还分别有各自的动画实现逻辑。
3.3 requestAnimationFrame 实现动画
实现动画的原理利用 drawImage()
将图片画到 Canvas
指定的坐标画布上,实现动画就是利用window.requestAnimationFrame
实现动画循环:
1 | connectAnimation() { |
requestAnimationFrame
解决了浏览器不知道javascript不知道最佳循环间隔时间的问题。
编写动画循环的关键,是要知道延迟时间多长合适。
一方面,循环时间必须足够短,这样才能保证动画效果更平滑流畅;另一方面,循环还要足够长,这样才能保证浏览器有能力渲染产生的变化。
大多数显示器的刷新频率是60Hz,相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过了这个频率,用户体验也不会有提升。
因此最平滑动画的最佳循环间隔是1000ms/60,约等于17ms。以这个循环间隔重绘的动画是平滑的,因为这个速度最接近浏览器的最高限速。为了适应17ms的循环间隔,多重动画可能需要加以节制,以便不会完成得太快。
虽然与使用多组 setTimeout()
相比,使用 setInterval()
的动画循环效率更高。但是无论setTimeout()
还是 setInterval()
都不十分精确。为它们传入的第二个参数,实际上只是指定了把动画代码添加到浏览器UI线程队列以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务执行完成后再执行。如果UI线程繁忙,比如忙于处理用户操作,那么即使把代码加入队列也不会立即执行。
requestAnimationFrame的速度是由浏览器决定的,不同浏览器会自行决定最佳的帧效率。