帧动画的实现原理与实践

帧动画的实现原理与实践

通常所说的帧动画,即逐帧动画。

逐帧动画:是一种常见的动画形式(Frame By Frame),其原理是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。

简单来说就是不断切换视觉内图片内容,利用视觉滞留生理来实现连续播放的动画效果。就像我们小时候玩翻页动画一样,把许多帧相似的图片连起来,一张张翻过去,看起来就像是在动。

手翻书动画

手翻书动画


合适的动画不仅更能吸引人们的眼球,也能让你的应用体验更为流畅,而将动画的效果做到极致,才能让用户感到使用你的应用是一种享受,而不是觉得生硬和枯燥。本文旨在介绍各种前端帧动画效果实现方式方法以及优劣,具体应用中如何实现,还得根据自身的情况进行考量。

前端逐帧动画的实现方案

前端中实现逐帧动画,最主要且最常用的有三种实现方式:
  1. GIF动画;
  2. JavaScript直接实现;
  3. CSS3 animation实现;

GIF动画

第一张会动的图片产自1987年,CompuServe公司。


GIF动图有着显著的优点,比如连续播放、文件小、资源占用少且兼容性很好,
绝大多数网站或多或少都会用到,很是受欢迎。
但GIF动画只支持 256 色调色板,会丢失颜色信息,Alpha透明度支持差,导致图像锯齿毛边严重。并且不支持交互,灵活性差。
综合了这些优缺点,目前GIF动图常用于制作logo、loading导航条等细节动画。

JavaScript

JavaScript实现动画,可谓是’花样百出‘,先看个示例:

这么一个简单的动画,大概有以下五种实现方式:

  1. 将所有图片合成雪碧图,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>
        
  1. 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>
        
  1. 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>
        
  1. 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>
        
  1. 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 的工作机制

看图不理解?针对以上代码有疑问?不着急,往下看↓

第一个问题,既然都详细定义关键帧了,是不是可以不用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,怎么选择呢?

几种实现方式的对比

众所周知,好的动画效果,有这么几个特点:

  1. 流畅,没有过多的性能消耗;
  2. 可交互,可开始、暂停、中止等操作;
  3. 实现简单;
  4. 兼容性好;

根据这几个特点,我们来依次讨论对比。

流畅

首先,网页渲染流程:

  1. HTML代码转化成DOM
  2. CSS代码转化成CSSOM(CSS Object Model)
  3. 结合DOM和CSSOM,生成一棵渲染树(包含每个节点的视觉信息)
  4. 生成布局(layout),即将所有渲染树的所有节点进行平面合成
  5. 将布局绘制(paint)在屏幕上

耗费时间越长,性能越差。
我们来录制一下几个页面渲染时时间的花费。

js-修改background-position

js-修改background-position
js-canvas绘图

js-canvas绘图
js-动态创建img

js-动态创建img
animation-steps实现动画

animation-steps实现动画


概括为下表(对比表格,单位: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+关键帧即可实现动画,不需要额外开发接口 不兼容低端浏览器 有规律,不太复杂的动画

万事都没有十全十美的,我们能做的,就是更深入地了解各种方式的优劣,作出一定的牺牲,选择更重要的。

踩坑

所谓“常在河边走,哪有不湿鞋”,在实现帧动画时,也是踩过了一些坑:

  1. 切换背景图片实现动画效果时,图片首次加载会出现闪烁的情况。

  建议解决办法:将使用的所有图片预加载。图片预加载的方式有多种:纯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>
        
  1. 通过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>
        
  1. JS使用setTimeout和setInterval实现动画,当页面存在多个动画且还有一系列复杂的计算时,复杂的计算、动画1、动画2、动画3...等等依次排队时,
    有时不知道要等多久才能执行动画,造成动画卡顿。

  建议解决办法:使用requestAnimationFrame代替setTimeout和setInterval。requestAnimationFrame是浏览器专门为优化动画提供的接口。具体用法请看window.requestAnimationFrame

此外还有一些小的建议,在实现动画时也会带来好的效果:

  1. 减少复杂的计算;
  2. 减少DOM操作;
  3. 仔细规划动画、简化绘制的复杂度。

总结

  动画给予了页面丰富的视觉体验,而丝滑般顺畅的动画更是我们追求的目标。开始动手实现之前,充分了解各种方式实现的优缺点以及改进方案,选择最优,实现最优。

参考链接:

  1. 网页性能详解
  2. 高性能动画
  3. 浏览器的工作原理:新式网络浏览器幕后揭秘

添加新评论

我们会加密处理您的邮箱保证您的隐私. 标有星号的为必填信息 *