帧动画的实现原理与实践
通常所说的帧动画,即逐帧动画。
逐帧动画:是一种常见的动画形式(Frame By Frame),其原理是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。
简单来说就是不断切换视觉内图片内容,利用视觉滞留生理来实现连续播放的动画效果。就像我们小时候玩翻页动画一样,把许多帧相似的图片连起来,一张张翻过去,看起来就像是在动。
合适的动画不仅更能吸引人们的眼球,也能让你的应用体验更为流畅,而将动画的效果做到极致,才能让用户感到使用你的应用是一种享受,而不是觉得生硬和枯燥。本文旨在介绍各种前端帧动画效果实现方式方法以及优劣,具体应用中如何实现,还得根据自身的情况进行考量。
前端逐帧动画的实现方案
前端中实现逐帧动画,最主要且最常用的有三种实现方式:
1. GIF动画;
2. JavaScript直接实现;
3. CSS3 animation实现;
GIF动画
第一张会动的图片产自1987年,CompuServe公司。
GIF动图有着显著的优点,比如连续播放、文件小、资源占用少且兼容性很好,
绝大多数网站或多或少都会用到,很是受欢迎。
但GIF动画只支持 256 色调色板,会丢失颜色信息,Alpha透明度支持差,导致图像锯齿毛边严重。并且不支持交互,灵活性差。
综合了这些优缺点,目前GIF动图常用于制作logo、loading导航条等细节动画。
JavaScript
JavaScript实现动画,可谓是’花样百出‘,先看个示例:
这么一个简单的动画,大概有以下五种实现方式:
- 将所有图片合成雪碧图,JS修改background-position实现动画效果;
<div id="box" width='300px' height='168px'></div>
<script>
(function (ele,positions){
var positions = ['0 0','-300 0','-600 0','-900 0','-1200 0','-1500 0'];
var ele = document.getElementById('box');
var index = 0;
setInterval(function(){
var pos = positions[index].split(' ');
ele.style.backgroundPosition = pos[0] + 'px ' + pos[1] + 'px';
index++;
if(index >= positions.length){
index = 0;
}
}, 500);
})();
</script>
- JS控制图片显示和隐藏实现动画效果;
<div id="box">
<img src='./huangshan/1.jpg' />
<img src='./huangshan/2.jpg' />
<img src='./huangshan/3.jpg' />
<img src='./huangshan/4.jpg' />
<img src='./huangshan/5.jpg' />
<img src='./huangshan/6.jpg' />
</div>
<script>
(function () {
var box = document.getElementById('box');
var images = document.getElementsByTagName('img');
var index = -1;
setInterval(function(){
if( index < 5) {
index ++;
} else {
for(var i=0; i<images.length; i++) {
images[i].style.display = 'none';
}
index = 0;
}
images[index].style.display = 'block';
},500);
})();
</script>
- JS控制图片的透明度实现动画效果;
<div id="box">
<img src='./huangshan/1.jpg' />
<img src='./huangshan/2.jpg' />
<img src='./huangshan/3.jpg' />
<img src='./huangshan/4.jpg' />
<img src='./huangshan/5.jpg' />
<img src='./huangshan/6.jpg' />
</div>
<script>
(function() {
var box = document.getElementById('box');
var images = document.getElementsByTagName('img');
var index = -1;
setInterval(function(){
if( index < 5) {
index ++;
} else {
for(var i=0; i<images.length; i++) {
images[i].style.opacity = '0';
}
index = 0;
}
images[index].style.opacity = '1';
}, 500);
})();
</script>
- JS动态创建img节点,预加载所有图片,切换图片实现动画效果;
<div id="box"></div>
<script>
(function() {
var imgSrcArr =['./huangshan/1.jpg','./huangshan/2.jpg','./huangshan/3.jpg','./huangshan/4.jpg','./huangshan/5.jpg','./huangshan/6.jpg'];
var imgWrap = [];
function preloadImg(arr) {
for(var i =0; i< arr.length ;i++) {
imgWrap[i] = new Image();
imgWrap[i].src = arr[i];
}
}
preloadImg(imgSrcArr);
var box = document.getElementById('box');
var index = 0;
setInterval(function(){
box.innerHTML = '';
box.appendChild(imgWrap[index]);
if (index <= 4 ) {
index ++;
} else{
index = 0;
}
}, 500);
})();
</script>
- JS结合canvas,不断绘制、擦除图片实现动画效果;
<canvas id="canvas"></canvas>
<script>
(function () {
var timer = null,
canvas = document.getElementById("canvas"),
context = canvas.getContext('2d'),
img = new Image(),
width = 300,
height = 168,
k = 6,
i = 0;
img.src = "./sprite_huangshan.png";
function drawImage() {
context.clearRect(0, 0, width, height);
i++;
if (i == k) {
i = 0;
}
context.drawImage(img,i*width, 0, width, height, 0, 0, width, height);
}
img.onload = function () {
timer = setInterval(drawImage, 500);
}
})();
</script>
以上,通过代码,很容易发现第二、三两种方案,有几个比较明显的缺点:
1. 加载多张图片,需要进行多次HTTP请求;
2. 每张图片首次加载时会造成图片切换时的闪烁;
3. 不利于文件的管理,特别是复杂的动画,文件数量过多;
这里推荐第一、五方案使用的雪碧图方式(在线制作雪碧图),很大程度的改善了动画的流畅度,第四种预加载图片的方式也解决了图片首次加载时闪烁的问题。
那么第一、四、五三种实现方式,哪种更好一些呢,我们后面会讨论到。
随着CSS3的广泛使用,CSS3 animation的出现打破了GIF动画和JS帧动画的垄断地位,特别是移动端,帧动画的实现,更多的开发者倾向于使用CSS3的animation,而它真的有那么优秀吗?与JS的几种方式对比呢?带着这两个问题,往下看!
CSS3 animation
利用CSS3中animation动画,确切的说是使用animation-timing-function 的阶梯函数steps(number_of_steps, direction) 来实现逐帧动画的连续播放的。
制作CSS3帧动画的几种方案:
1. 加载所有图片,定义关键帧,连续切换背景图;
2. 所有图片合成雪碧图,定义关键帧,连续切换背景图位置;
方案一的缺点在JavaScript动画中提过,这里我们使用第二种比较高效的方法。
先准备一张雪碧图
使用 steps 实现动画播放
代码如下↓:
<style>
div {animation: jump 3s steps(1, start) normal infinite;}
@keyframes jump {
0%{
background-position: 0px 0px;
}
17%{
background-position: 0px 0px;
}
34%{
background-position: -300px 0px;
}
51%{
background-position: -600px 0px;
}
68%{
background-position: -900px 0px;
}
84%{
background-position: -1200px 0px;
}
100% {
background-position: -1500px 0px;
}
}
</style>
<div id="box"></div>
引用W3C的一段解释:
steps 是 animation-timing-function 的一个属性值。
steps 函数指定了一个阶跃函数
第一个参数指定了时间函数中的间隔数量(必须是正整数);
第二个参数可选,接受 start 和 end; 两个值,指定在每个间隔的起点或是终点发生阶跃变化,默认为 end。
step-start等同于steps(1,start),动画分成1步,动画执行时为开始左侧端点的部分为开始;
W3C中提供了一张图片来理解 steps 的工作机制:
看图不理解?针对以上代码有疑问?不着急,往下看↓
第一个问题,既然都详细定义关键帧了,是不是可以不用steps函数了,直接定义linear变化不就好了吗?
对于此问题,如果代码如下↓:
div { animation: jump linear 3s infinite; }
效果:
动画不会成阶梯状,一步步的执行,而是会连续变化背景图的位置,它会在每个关键帧之间插入补间动画,所以动画效果是连贯性的。
第二个问题,为什么steps第一个参数是1,而不是5了呢?
steps是animation-timing-function的一个属性值,而animation-timing-function 作用于每两个关键帧之间,而不是整个动画,即两个关键帧之间要变化几次。
重新定义keyframes
div { animation: jump 3s steps(5, start) normal infinite; }
@keyframes jump {
from {
background-position: 0px 0px;
}
to {
background-position: -1500px 0px;
}
}
此时,两个关键帧之间会变化5次,也可以达到同样的效果。
第三个问题,第二个参数是start和end,有什么区别?
来看一个简单的例子:
代码如下↓:
@keyframes jump {
0% {background: red}
50%{background: green}
100% {background: blue}
}
#div1 { animation: jump steps(1,start) 2s infinite; }
#div2 { animation: jump steps(1,end) 2s infinite; }
看到区别了吧:
steps(1, start):绿色和蓝色相互切换,从不展示红色;
steps(1, end):红色和绿色相互切换,从不展示蓝色;
start和end都会选择性的跳过前后部分,start跳过0%,选择下一帧的显示效果来填充间隔动画,而相反的,end跳过100%,选择上一帧的显示效果来填充间隔动画。这也是为什么定义关键帧时,0%和17%是用的同一个位置了。
此时,再返回去看steps工作机制图,是不是好理解了很多。
至此,我们了解了动画的几种实现方式和方法。
再把上述提到的问题抛出来:
JS动画的几种实现方式(修改background-position、js+canvas、动态创建节点)、CSS3 animation,怎么选择呢?
几种实现方式的对比
众所周知,好的动画效果,有这么几个特点:
- 流畅,没有过多的性能消耗;
- 可交互,可开始、暂停、中止等操作;
- 实现简单;
- 兼容性好;
根据这几个特点,我们来依次讨论对比。
流畅
首先,网页渲染流程:
- HTML代码转化成DOM
- CSS代码转化成CSSOM(CSS Object Model)
- 结合DOM和CSSOM,生成一棵渲染树(包含每个节点的视觉信息)
- 生成布局(layout),即将所有渲染树的所有节点进行平面合成
- 将布局绘制(paint)在屏幕上
耗费时间越长,性能越差。
我们来录制一下几个页面渲染时时间的花费。
概括为下表(对比表格,单位:ms):
JS-修改background-position | JS-canvas绘图 | JS-创建新节点 | CSS3 animation | |
---|---|---|---|---|
Loading | 1.2 | 0.5 | 0.7 | 0.7 |
Scripting | 1.9 | 3.4 | 4.3 | 0 |
Rendering | 1.5 | 1.3 | 4.6 | 44.2 |
Painting | 1.3 | 14.0 | 1.6 | 8.2 |
总计 | 5.9 | 19.2 | 11.2 | 53.1 |
在时间花费方面,优劣一目了然。
也许有人说多个十几、几十毫秒没有太大影响,但一个页面中不可能只有这么一个简单的动画,为了吸引用户,各种酷炫效果层出不穷,性能的消耗将是巨大的。
这里针对前三个JS实现方案,有一些需要特别提醒:
1. JavaScript在浏览器的主线程中运行,实现动画,就是在频繁的操作DOM和CSS,这时浏览器会不停的执行重排和重绘,导致有很多其他需要运行的JavaScript、样式计算、布局、绘制等对其干扰。这也就导致了线程可能出现阻塞,从而造成丢帧的情况;
2. 利用JS创建img节点并预加载图片,虽然避免了图片第一次加载时的闪烁,但会发送多个HTTP请求,加大了响应时间;
可交互
JS动画的绝对优势就是可以控制动画,用户可进行开始、暂停、中止等操作。这是GIF动画和CSS3 animation动画无法抗衡的。
实现简单
JS实现动画,需要开发者编写这些动画事件的接口,以便更好的做下一步的工作,而CSS3 animation动画不需要开发者编写这些事件接口,浏览器本身就已经提供了,只需定义一个关键帧即可,实现起来比JS动画要简单很多
兼容性
CSS3 animation作为最近几年才开始流行起来的动画能力,兼容一些老的浏览器是没有JS动画好(除了使用canvas绘制)。
CSS3 animation在书写时,需要分内核加前缀(-o-,-webkit-,-moz-)。
更多兼容性问题,可参考W3C)。
最后,我们再以一个简单的表格来汇总三种实现方式(GIF动画、JS动画、CSS3 animation动画)的优劣:
性能消耗 | 交互效果 | 实现成本 | 兼容性 | 擅长 | |
---|---|---|---|---|---|
GIF动画 | 性能消耗大,周期性引发页面重绘 | 不支持交互,加载完成后,连续播放 | 实现简单,一般由UI设计人员提供动图 | 兼容所有浏览器 | 小细节动画,loading、logo等等 |
JS动画 | 性能消耗可高可低,具体见上述的对比表格 | 支持交互,可以暂停、中止、重新开始 | 实现成本较大,需要具有专业的JS开发能力,需要额外开发接口 | 兼容所有浏览器 | 需要复杂计算、可交互的动画 |
CSS3 animation动画 | 性能消耗高,不断地触发网页重绘和重排。 | 不支持交互,加载完成后直接播放,可以设置循环次数 | 实现简单,animation+关键帧即可实现动画,不需要额外开发接口 | 不兼容低端浏览器 | 有规律,不太复杂的动画 |
万事都没有十全十美的,我们能做的,就是更深入地了解各种方式的优劣,作出一定的牺牲,选择更重要的。
踩坑
所谓“常在河边走,哪有不湿鞋”,在实现帧动画时,也是踩过了一些坑:
- 切换背景图片实现动画效果时,图片首次加载会出现闪烁的情况。
建议解决办法:将使用的所有图片预加载。图片预加载的方式有多种:纯CSS方式预加载图片、JS方式预加载图片,下面是JS方式:
<div id="box"></div>
<script>
(function() {
var imgSrcArr = ['./huangshan/1.jpg','./huangshan/2.jpg','./huangshan/3.jpg','./huangshan/4.jpg','./huangshan/5.jpg','./huangshan/6.jpg'];
var imgWrap = [];
function preloadImg(arr) {
for(var i =0; i< arr.length ;i++) {
imgWrap[i] = new Image();
imgWrap[i].src = arr[i];
}
}
preloadImg(imgSrcArr);
})();
</script>
- 通过canvas drawImage方法绘制跨域图片会报错的问题,
建议解决办法:(1)、如果图片不大,数量不多,建议用base64,来代替跨域引用的图片;(2)、 设置img标签的crossOrigin属性,并且设置服务端的Access-Control-Allow-Origin:* (或允许的域名),代码如下↓:
<canvas id="box"></canvas>
<script>
(function() {
var canvas = document.getElementById('box');
var context = canvas.getContext('2d');
var img = new Image();
img.crossOrigin = 'anonymous'; //同时修改服务端的Access-Control-Allow-Origin
img.onload = function(){
context.drawImage(img,0,0,canvas.width,canvas.height);
var imgData = context.getImageData(100, 100, 1, 1);
};
img.src = "test.png";
})();
</script>
- JS使用setTimeout和setInterval实现动画,当页面存在多个动画且还有一系列复杂的计算时,复杂的计算、动画1、动画2、动画3...等等依次排队时,
有时不知道要等多久才能执行动画,造成动画卡顿。
建议解决办法:使用requestAnimationFrame代替setTimeout和setInterval。requestAnimationFrame是浏览器专门为优化动画提供的接口。具体用法请看window.requestAnimationFrame。
此外还有一些小的建议,在实现动画时也会带来好的效果:
- 减少复杂的计算;
- 减少DOM操作;
- 仔细规划动画、简化绘制的复杂度。
总结
动画给予了页面丰富的视觉体验,而丝滑般顺畅的动画更是我们追求的目标。开始动手实现之前,充分了解各种方式实现的优缺点以及改进方案,选择最优,实现最优。
参考链接: