认识一下动画系统

动画,顾名思义,就是能“动”的画。
人的眼睛对图像有短暂的记忆效应,所以当眼睛看到多张图片连续快速的切换时,就会被认为是一段连续播放的动画了。

比如,中国古代的“走马灯”,就是用的这个原理。
有些人还会在一个本子每页上手绘一些漫画,当快速翻页的时候,也会看到动画的效果,比如:


认识一下动画系统_第1张图片
走马灯

计算机动画的实现方式

动画是由一张张图片组成的,在计算机中,我们称每一张图片为一帧画面

如果我们想实现这么一个动画:一个水杯放在桌子的左边,移动到右边,那么我们实际操作的,只是水杯。
所以动画的实现,只是对运动变化了的部分的处理。

我们一般在计算机上用 FPS ( Frames Per Second) ,即 每秒的帧数 来表示动画的刷新速度,基于屏幕的刷新率等其他原因,在计算机上一般采用 60 FPS。
如果运动变化幅度较缓,减半到 30 FPS 时,我们肉眼也是可接受的。
较低的 FPS 会让我们有“卡顿”的感觉。

逐帧动画和关键帧动画

从帧速率区分动画的话,一般来说我们常见的动画都是属于关键帧动画(Keyframe Animation),而逐帧动画(Frame By Frame)是一帧一幅画,从词语来说意味着全片每一秒都是标准24帧逐帧纯手的(在Flash的区分是逐帧动画与动作补间动画,还有分为全动画半动画。)。

简单介绍一下这几个概念,想详细了解,可以自取百度或者Google。
动作补间动画:做flash动画时,在两个关键帧中间需要做“补间动画”,才能实现图画的运动;插入补间动画后两个关键帧之间的插补帧是由计算机自动运算而得到的。
全动画:为追求画面完美和动作流畅,按照24帧/s制作动画。
半动画:又名“有限动画”,为追求经济效益,以6帧/s制作动画。

逐帧动画

逐帧动画是一种在连续的关键帧中分解动画动作,即在时间轴的每一帧上绘制不同内容并使之连续播放成动画的一种常见的动画形式。

类似于上面提到的手绘翻页方式,我们可以将这个水杯在每帧画面中的位置一一找出来,这样实现动画的方式就叫作逐帧动画,我们需要处理动画中的每一帧。
逐帧动画是最直接的,但要处理的帧数太多,所以实现过程是会麻烦。
计算机的工作就是来完成重复单调的工作的,所以,有些工作是可以考虑让计算机来完成的。

了解更多关于逐帧动画在电影和动漫上的应用可以点这里

关键帧动画

上面的例子,可以变成一个涉及数学和物理的问题:一个杯子初始位置在左边,n秒后匀速运动到右边,那么在每 1/60 秒的时候,这个杯子的位置显然是可以计算出来的了。
所以,我们其实只需要指定一些 关键 信息就能让计算机自己计算出每一帧杯子的位置了:

  • 起始位置,比如一个坐标 (0,0)
  • 结束位置,再比如一个坐标 (100,0)
  • 动画总时间,比如 0.25 秒
  • 匀速运动

这种方式就称之为关键帧动画。即我们只需要给定几个关键帧的画面信息,关键帧与关键帧之间的过渡帧都将由计算机自动生成。

这里说的 关键帧动画,是指的广义上的一种动画制作方式,并不仅指 CAKeyframeAnimation,CABasicAnimation的实现方式也属于 关键帧动画

两者的对比

关键帧动画的实现方式,只需要修改某个属性值就可以了,简单方便,但涉及的深层次内容较多,需要更多的理解和练习。

采用逐帧动画的实现方式,实现原理简单,但绘制动画的过程要复杂。如果动画过程处理的事情较多,也会带来较大的开销,就有可能造成动画帧数的下降,出现卡顿的现象,因此需要较多的测试和调试。
动画绘制的过程中,会要求较多的数学、物理等知识来计算中间态的数据。

但这两种方式也不是绝对分离开的。
关键帧动画实现方式,一般只能对系统实现了可动画的属性做动画处理,但其实也是允许实现自定义属性的动画处理的。
这就需要自己来实现系统中自动计算过渡帧的操作了,也就是逐帧实现动画的方式了。

前端高性能动画

何为高性能动画?让人感觉流程顺滑即可。24fps的电影就能让人感觉到流畅,但是游戏却要60fps以上才能让人感觉到流畅。分析原因,我们得出如下结论:

  1. 视频的每一帧记录的是一段时间段(1/24s)的信息,而游戏的每一帧都由显卡绘制,它只能生成一个时间点的信息;
  2. 视频的帧率是稳定的,而在系统负载不平稳时,显卡很难保证游戏帧率的稳定性;
    前端动画与游戏的原理类似,我们设计高性能动画的基本思路就是提高帧率稳定帧率。让我们首先一起了解一下浏览器渲染页面的基本过程。

理解浏览器渲染流水线

渲染的基本流程是:扫描HTML文档结构、计算对应的CSS样式并生成RenderTree,然后根据RenderTree进行布局和绘制,基本过程示意图如下:


认识一下动画系统_第2张图片
渲染主流程

可以简单的描述为以下四部分:

  1. 解析HTML以构建DOM树
  2. 构建render树
  3. 布局render树
  4. 绘制render树

但实际上渲染的过程是这样的:

  1. 在浏览器进行渲染的时候,渲染引擎首先会解析HTML代码,然后将标签转化为DOM树上的一个个对应节点(我们可以在chorme的Elements面板中查看到)。
  2. 接着,渲染引擎解析外部CSS文件及style标签中的样式信息。这些样式信息以及HTML中的可见性指令将被用来构建另一棵树---render树。Render树由一些包含有颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。
  3. Render树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。
  4. 然后就是绘制,即遍历render树,并使用UI后端层绘制每个节点。

值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

另外我们应该知道,在执行某些CSS、JS操作时可能会造成浏览器的回流或者重绘操作,导致浏览器重新渲染页面或者重新绘制页面的某一部分,这些都是很消耗性能的。所以我们在提高动画性能时要合理的考虑这些因素,尽量减少渲染或者重绘的次数。

提高动画性能指标

上文提到过,动画的性能指标有两个,帧率数和帧率稳定性。我们分别从动画实现,节点的处理,属性的选择等方面讨论如何提高这两个动画性能指标。

选择稳定的实现方式

css3动画使用起来非常简单,目前的浏览器支持率也不错,足以应对一般的交互需求,我们应该优先使用它。当浏览器不支持css3时,或动画场景过于复杂而仅凭css3无能为力时,就需要引入js来帮忙了。我们最常想到的js动画的实现方式,就是固定时间间隔修改元素的样式:

setInterval(function(){
    var anmationNode = document.getElementById('animation-node'); 
    //定期修改节点的样式
}, 100);

但这是一种非常粗暴的方式,其弱点是很明显的。浏览器的timer的触发时间点是不固定的,如果遇到比较长的同步任务,其触发时间点就会推迟,显然也就保证不了动画帧率的平稳性。HTML5为创建逐帧动画提供了一个新的API:RequestAnimationFrame,该方法在每次浏览器渲染时触发,其触发频率为60fps,我们可以通过这个函数来实现动画,而当动画中某些帧计算量太大无法在1/60s完成时,浏览器会将刷新评论降低到30fps,以保证帧率的稳定性。

function step(){
    //修改节点样式
    RequestAnimationFrame(step);
}
RequestAnimationFrame(step);

但是由于RequestAnimationFrame支持程度还不高(手机浏览器普遍不支持),我们可以结合RequestAnimationFramesetInterval实现一套逐渐增强和优雅降级的方案,下面是兼容各个浏览器的终极版本:

function getAnimationFrame() {
    if (window.requestAnimationFrame) { 
    //较新浏览器
        return {
            request: requestAnimationFrame,
            cancel: cancelAnimationFrame,
        }
    } else if (window.mozRequestAnimationFrame && window.mozCancelAnimationFrame) {
    //firfox浏览器
        return {
            request: mozRequestAnimationFrame,
            cancel: mozCancelAnimationFrame
        }
    } else if (window.webkitRequestAnimationFrame && webkitRequestAnimationFrame(String)) {
        return {
            request: function(callback) {
                return: window.webkitRequestAnimationFrame(function() {
                    return callback(new Date - 0); 
                    //修正部分webkit版本下没有给callback传time参数的bug
                });
            },
            cancel: window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame
        }
    } else { 
        //用setInterval模拟requestAnimationFrame
        var millisec = 25; //40fps;
        var callbacks = [];
        var id = 0,
            cursor = 0;
        var timerId = null;

        function playAll() {
            var cloned = callbacks.slice(0);
            cursor += callbacks.length;
            callbacks.length = 0;
            var hits = 0;
            for (var i = 0, callback; callback = cloned[i++];) {
                if (callback !== 'cancelled') {
                    callback(new Data - 0);
                    hits++;
                }
            };
            if (hits == cloned.length) {
                clearInterval(timerId);
            }
        }

        timerId = window.setInterval(playAll, millisec);
        return {
            request: function(handler) {
                callbacks.push(handler);
                return id++;
            },
            cancel: function() {
                callbacks[id - cursor] = 'cancelled';
            }
        }
    }
}

为动画节点创建新的渲染层

通过将动画节点与文档中的其他节点隔离开来,可以有效的减少重新布局(relayout)和重新绘制(repaint)的面积,从而提高页面的整体性能。隔离动画节点与文档中的其他节点方法通常是为动画节点创建新的渲染层(render layer)。下面是创建渲染层的常用方法:

使用3D变换

大家一定经常看到网上的文章说使用transform: translate3d(0, 0, 0)/translateZ(0)(详细了解点击这里)可以开启GPU加速,亲自试验以后发现其的确可以提高页面的渲染速度,我就曾经用它解决了一些低端机的闪烁问题。 那么其原理是什么呢?这种方式并非一定能够开启GPU加速。

W3C标准是这么说的。

Three-dimensional transforms can result in transformation matrices with a non-zero Z component (where the Z axis projects out of the plane of the screen). This can result in an element rendering on a different plane than that of its containing block. This may affect the front-to-back rendering order of that element relative to other elements, as well as causing it to intersect with other elements.

其主要意思就是3D变换会创建新的渲染层,而不是与其父节点在同一个渲染层中。在新的渲染层中修改节点不会干扰到其他节点,防止了对其他节点的重新布局(relayout)和重新绘制(repaint),自然也就加快了页面的渲染速度。除了transform: translate3d(0, 0, 0)/translateZ(0),我们还可以使用will-change。

使用will-change

我们可以使用will-change让浏览器提前了解预期的元素变换,它允许浏览器提前做好适当的优化,使之最后能够快速和流畅的渲染。will-change: transform同样也会为节点创建新的渲染层。

.animation-element{
     will-change: transform;
}

选择高效的动画属性

修改节点的大部分属性都会引起重新绘制,甚至是重新布局。而理想情况下,我们应避免重新绘制和重新布局。幸运的当仅仅修改transfrom属性或opacity属性,可以做到不重新绘制。具体的思路是:为需要创建动画的节点创建新的渲染层,并且在新渲染层中只修改transformopacity属性。只有做到以上两点才可以避免重新布局和重新绘制,真正使用GPU加速。

避免引起多余的渲染

我们在实现动画的过程中,经常需要获取某个元素的属性,然后对该属性做出修改:

function step(){
    var animationNode = doucment.getElementById('animation-node');
    for(var i = 1; i <= 20 ; i++){
        animationNode.width = animationNode.width + 1;
    }
}

上述的for循环语句将导致浏览器进行20次多余的渲染,严重影响页面性能。通常来讲JS对页面样式的多次修改只会在页面下次刷新时渲染一次,而通过DOM API获取样式时,会强制页面完成一次渲染以体现最新修改后的值。上述例子就是这样导致浏览器多次渲染的。而正确的写法应该是读写分离。

var animationNode = doucment.getElementById('animation-node');
var initialWidth = animationNode.style.width;
for(var i = 1; i <= 20 ; i++){
    initialWidth+=1;
}
animationNode.style.width = initialWidth; 

当我们在复杂页面上实现动画是,常常由于疏忽导致页面多余的渲染。这是我们可以借助fastdom来隔离对真实DOM的操作,fastdom将对节点样式的读写批量缓存、一次执行,防止多余的渲染。

参考资源

  1. 作者/胖花花的解析 iOS 动画原理与实现
  2. 逐帧动画和关键帧动画
  3. 前端高性能动画最佳实践

本文只是将网络上对动画系统和设计前端的动画方面做了简单的整合。
其中在前端高性能动画最佳实践一文中的尾部有几篇参考资料非常的好,对于理解前端动画有很好的帮助,推荐一下:

  1. 前端动画原理与实现,这篇文章通过PPT的方式介绍了前端动画,页面做的不错。
  2. How browsers work,这篇文章介绍了浏览器的工作原理和流程,对于想深入了解浏览器工作原理和流程的同学可以一试。
  3. Javascript高性能动画与页面渲染,这篇文章和前端高性能动画最佳实践一样,只不过是对于前端动画的另一个角度的解读,并且可能更详细。

你可能感兴趣的:(认识一下动画系统)