帧动画
- 基本介绍
- 动画
- 帧率(FPS)
- 帧动画开发
- 帧动画的实现方案
- gif图实现动画
- css实现动画
- js实现逐帧动画
- (1)raf介绍
- (2)为什么建议raf(定时器和Raf区别)?
- 实现帧动画常用的解决方案
- demo实践
- 使用raf实现轮播动画
- 使用raf实现一个进度条
- 帧率监听工具
基本介绍
本片文章以简单的case说明下,从前端开发角度什么是帧动画呢?
动画
英语:(Animation)是一种通过定时拍摄一系列多个静止的固态
图像(帧)以一定频率连续变化、运动(播放)的速度(如每秒16张)而导致肉眼的视觉残象产生的错觉,而误以为图画或物体(画面)活动的作品及其视频技术。
帧率(FPS)
帧率(英文:frame rate)是用于测量显示帧数的度量。测量单位为“每秒显示帧数”(frame per second,FPS)或“赫兹”,一般来说FPS用于描述视频、电子绘图或游戏每秒播放多少帧。
通俗来讲,以最早的电影胶片为例,电影通常是很长的胶卷,每部电影都是通过投影设备将胶片的每个画面放大呈现在大屏幕上,帧率即就是每秒能够播放的画面个数。
我们每次购买电脑或者相关设备时都会去关注两个指标:帧率和屏幕分辨率
帧率越大,视频画面越流畅,尤其是对游戏高端玩家,帧率直接影响玩家的游戏体验,我们通常看的电影帧率一般在24fps,手机拍摄的视频一般帧率在30fps,一般设备的最大帧率在60fps,我们可以在系统中可以查看设备当前的实时帧速率。同样在chroms的移动端模拟器中也可使查看实时帧率。虽然帧率越大视频的流畅度越好,但同时会带来视频的体积较大,因此通常调整帧率也是压缩mp4视频体积的一种方式。
帧动画开发
在H5开发过程中,我们经常为了给用户一个更好的用户体验,会需要实现一些动画,在实现动画时我们有以下几种实现方式:帧动画分为关键帧动画和逐帧动画都是动画的一种实现方式,我们平常接触到的都是关键帧动画。
关键帧动画:又称为补间动画,设计师只需要去关注两端的关键帧页,在关键帧上绘制一个基础形状,然后在时间帧上对另一个关键帧进行形状转变或绘制另一个形状等,然后中间的动画过程是由计算机自动生成。补间动画可以实现两个关键帧图形之间颜色、大小、形状或位置的相互变化。关键帧动画比逐帧动画制作简单,且资源体积更小。
逐帧动画:逐帧动画认为每一帧都为关键帧,在每一帧页上都需要绘制帧内容,连续播放每一帧实现动画效果。
逐帧动画:逐帧动画有称为定格动画,是一种动画技术,器原理即将每帧不同的图像连续播放,从而产生动画效果,简言之,实现逐帧动画需要两个条件:(1)相关联的不同图像,即动画帧;(2)连续播放、
帧动画的实现方案
gif图实现动画
gif 可以有多个动画帧,连续播放是其自身属性,是否循环也是由其本身决定的。它往往用来实现小细节动画,成本较低、使用方便。
但根据以上录屏展示(绿色部分表示帧重绘区域),帧动画会周期性引起页面的重绘(repaint), 导致页面性能较差。并且gif图在清晰度上表现较差,图片放大后锯齿毛边比较严重。因此gif实现动画适应于一些对画面细节要求较低的动效上,开发成本相对较低,使用起来方便。
css实现动画
在css3中的Animation动画可以实现逐帧动画,使用animation-timing-function 的阶梯函数 steps(number_of_steps, direction) 来切换图片实现逐帧动画的连续播放。我们可以在@keyframes中定义每一帧的动画效果。例如,我们在不同的step替换背景图片,来完成图片连续播放,实现动画。这种方式只适用于帧数较少的动画实现。
.animate {width: 300px;height: 300px;background-repeat: no-repeat;background-image: url(frame.png);animation: frame 333ms steps(1,end) both infinite;
}
@keyframes frame {0% {background-image: url(frame0.png);}10% {background-image: url(frame1.png);}20% {background-image: url(frame2.png);}35% {background-image: url(frame3.png);}40% {background-image: url(frame4.png);}50% {background-image: url(frame5.png);}60% {background-image: url(frame6.png);}70% {background-image: url(frame7.png);}80% {background-image: url(frame8.png);}90% {background-image: url(frame9.png);}100% {background-image: url(frame10.png);}
}
但此种方式需要有多张图片,请求每张图片会导致多次请求http,并且每张图片切换首次加载时会出现闪烁的情况,切多张图片维护起来成本较高,因此,我们可以采用合成雪碧图等方式,修改图片位置,来实现图片的逐帧播放。
在日常开发中,我们通常会采用css逐帧动画方案实现一些类似于大小缩放、位移、渐变等样式属性的简单动画,如按钮呼吸动画,闪烁动画,元素漂移、loading动画等效果,我们通过想设计师给的动画参数即可以完成动效效果,性能相对于动图等较高。
js实现逐帧动画
js实现逐帧动画的方案较多,最直接的方式是通过js动态去修改img标签的src或者修改元素样式完成动画的连续播放,其次就是canvase绘制。
js实现动画需要关注的点就是循环去触发函数,实现驱帧效果?
在动画驱动的时候我们可以采用requestAnimationFrame(raf)和setTimeOut(定时器)两种方案去驱动step帧动画。
在日常的动画实现上我们通常建议采用raf方式
(1)raf介绍
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
(2)为什么建议raf(定时器和Raf区别)?
(1)执行时机:定时器是指定时间后完成帧画面的切换,不依赖于设备帧率,通常我们为了使切换帧和屏幕刷新同频,我们的时间间隔采用1000/60(通常的屏幕帧率为60fps),但刷新率在不同设备下可能存在不同,因此按60fps设置固定的时间间隔,可能会存在仍和帧率不同步的问题,raf是的执行是根据屏幕的刷新帧率步伐走,在每一次页面重绘前通知系统完成raf的回调。
(2)丢帧情况:定时器的执行不依赖于设备帧率,如果回调方法的执行和系统的刷新时机步调不一致,就可能会导致中间某一帧的操作被跨越过去,而直接更新下一帧的图像,产生丢帧的情况。而raf的执行依赖于系统步伐,能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
(3)性能:在页面退入后台的时候,即未激活状态的时候,定时器会仍然在后台执行任务,但此时的页面是非激活状态,因此在后台刷新帧画面是没有任何意义的,因此是一定程度上CPU资源的占用,因此在开发过程中出于性能考虑,需要在页面未激活态的时候将定时器暂停掉,进入页面的时候再重启定时器刷新帧画面。而raf是和设备的帧率是相同步的,当页面处于未激活态的时候,屏幕刷新进程会被中断,因此raf动画任务也会中断,因此减少资源的占用。
实现帧动画常用的解决方案
方案 | 技术点 | 工具库 | 特点 |
---|---|---|---|
Apng | apng-js | 开源库 | 体积较大,需要注意体积的压缩 |
Lottie | lottie-web | 开源库 | 体积适中,适用于处理交互较为复杂的交互动效lottie-web的包体积较大 |
视频 | video/透明视频 | 原生video | (1)体积较小,性能较好(2)safari 和 chrome 中可能无法自动播放 |
demo实践
使用raf实现轮播动画
目标实现一个无缝轮播组件,我采用raf驱动的形式完成视频的轮播动画。避免生硬,我们设定每次动画过度时间为300ms。由于以上描述大部分的屏幕帧率是60HZ,因次我们设定动画间隔为1000ms/60=17为动画间隔,以这个循环间隔重绘的动画时比较平缓的,因为这个速度最接近屏幕的最高限速。
采用raf做位移驱动,完成位移动画
定义一个raf方法:
// 当不支持raf时采用定时器完成动画
function rafPolyfill(fn: TimerHandler) {const id = setTimeout(fn, 1000/60);return id;
}function raf(fn: FrameRequestCallback) {const requestAnimationFrame = window.requestAnimationFrame || rafPolyfill;return requestAnimationFrame.call(window, fn);
}
定义每次帧刷新的回调函数,为了实现在每一次刷新前都更新下一次刷新,在回调函数中再次调用raf方法,完成刷新,直到动画播放完成。
export function animateTo(options: { to: any; from: any; duration: any; callback: any; }) {const { to, from, duration, callback } = options;if (to === from) {callback(to, true);return;}let start = 0;const during = Math.ceil(duration / 17);let rafId: number;const step = () => {const value = cubicEaseOut(start, from, to - from, during);start++;if (start <= during) {callback(value);rafId = raf(step);} else {callback(to, true);}};step();return () => {cancelRaf(rafId);};
}
使用raf实现一个进度条
<template><div class="progress-container"><span class="progress-desc">正在loading中......</span><div class="progress-bar"><divclass="progress-bar progress-status-bar":style="{ transform: `translateX(${progressWidth}%)` }"></div></div><span class="progress-desc progress-value">{{ progressWidth }}<img class="percent-img" src="../assets/icon/percent.png"/></span></div>
</template>
<script>
export default {data() {return {progressWidth: 0,};},mounted() {this.getProcess();},methods: {getProcess() {const timer = window.requestAnimationFrame(() => {if (this.progressWidth <= 99) {// 进度条长度设置this.progressWidth++;this.getProcess();} else {this.progressWidth = 0;this.getProcess();window.cancelAnimationFrame(timer);}});},},
};
帧率监听工具
- 移动端
以安卓为例:打开手机开发者工具 =》选择“功能监测”=》开启 “Frame Rate Monitor Tools”
- pc端
打开控制台=》更多工具=》打开“渲染”界面=》开启“帧渲染统计信息”(vscode编译器)