zoukankan      html  css  js  c++  java
  • 第十三章 动画引擎

    动画是我们眼睛中的残影,叫视觉暂留现象。这里有两个关键字,差异快速

    在网页中,扩展样式的任务早已经交由css处理,让javascript第一次拥有视觉处理的api,setTimeout与setInterval早在css诞生前就已经出现。


    一:动画的原理

    在标准浏览器中,可计算的样式基本浏览器已经为你转化好,比如width,height,margin-x,border-x-width,padding-x.这些样式单位为px,color和background-color则被分解成RGB,这个很容易被格式化为一个数组,透明度自不用说。

    不过css3新引进的变形样式transfrom,一种是面向程序员,是rotate()/skew()/scale()/translate(),分别有x, y之分,如rotateX(),rotateY(),依次类推;

    一种是面向计算机,传入矩阵进去,matrix()。无论传什么,都会转成矩阵。

    如果传承是旧版本的IE,就得自己转换了。比如你原来的单位是em,currentStyle就会返回em,原来填充的颜色是red,它不会返回rgb(255,0,0)

    因此,动画引擎的第一步就是设法获得元素的精确样式,这个在第九章:样式模块已经介绍,给出的$.css方法基本上除了颜色值都是已经转换好的。

    现在我们尝试让一个方块运动起来,简单说就是变化位置。一般来说,我们变化一个元素的top,left即可,当然也可以设置margin,这里只说元素定位的移动方法,要取得元素的left值,直接可以使用getComputedStyle,然后让用户传值改变值。就是我们需要一点点的变动值的大小。另外还要涉及到时长,就是动画的总共执行时间,就是执行的总时间,另外一个就是每次变动的相隔时间这个通常由引擎来决定,当然也可以暴露出来,它有个学名叫FPS

    fps通俗的来说,叫刷新率,在1秒内更新多少次画面,根据人视觉停留效应,停留多少秒最合适呢?我们不但要照顾人的眼睛,还要顾及下显示器的显示速度与浏览器的渲染速度 ,根据国外的统计,25毫秒为最佳数值。

    因此,我们创建一个新页面,里边有一个方块,它位于这个轨道上,它从一端到另外一端,动画时间为2秒,fps为30帧。

    <style type="text/css">
        #way{800px;height:100px;background:#e8e8ff;position:relative;}
        #move{position:absolute;left:0px;100px;height:100px;background:#a9ea00;}
    </style>
    <div id="way">
        <div id="move"></div>
    </div>
    
    <script type="text/javascript">
        window.onload = function () {
            var el = document.getElementById("move");
            var parent = document.getElementById("way");
            var distance = parent.offsetWidth - el.offsetWidth; //总移动距离
            
            var bengin = parseFloat(window.getComputedStyle(el, null).left) //开始位置
            console.log(bengin)
            var end = bengin + distance; //结束位置
            var fps = 30; //刷新率
            var interval = 1000/fps ; //相隔多少ms刷新一次
            var duration = 2000  ; //时长
            var times = duration /1000 * fps ; //一共刷新的次数
            var step = distance /times; //每次移动的距离。
    
            el.onclick = function() {
                var now = new Date();
                var id = setInterval(function(){
                    if (bengin >= end) {
                        el.style.left = end + "px";
                        clearInterval(id)
                        console.log(new Date - now)
                    } else {
                        bengin += step;
                        el.style.left = bengin + "px"
                    }
                },interval)
            }
    
        }
    </script>


    上面,我们用了最简单累加来实现,现在我们改写下,加入进度这个变量,让其更具有广泛性。

            el.onclick = function(){
                var benginTime = new Date();
                var id = setInterval(function(){
                    var t = new Date - benginTime; //当时已经用到的时间
                    if (t >= duration) {
                        el.style.left = end + "px";
                        clearInterval(id);
                        console.log(t)
                    } else {
                        var per = t / duration; //当前进度
                        el.style.left = bengin + per * distance + "px";
                    }
                },interval)
            }

    如果我们能随意控制per这个数值,那么就能轻易实现加速减速,于是,发明了缓动公式,所谓的缓动公式,就是来自数学上的三角函数,二次项方程式,高阶方程式。它们最初是由flash界的Robert Penner整理而来(http://robertpenner.com/easing/)。

    有了缓动公式,我们就能轻松的模拟加速,减速,急刹车,重力,弹簧,来回摆动等效果。

    二,缓动公式

    经过这么多的发展,缓动公式各项规范都稳定下来,虽然现在还有人源源不断的发掘新公式,但一般业务中,绝对数人选择使用默认的easein,linear.因此,对库的大小有顾虑,那么久将它们独立成一个模块吧。

    现在所有的缓动公式,基本上除了linear外(但也被称为easeNone),它们都以ease开头命名,添加三种后缀,In表示加速,Out表示减速,InOut表示加速到中途又开始减速,于是就有了easeIn,easeOut,easeInOut之分,如果单是这样命名,说明它们没有介入高阶函数与三角函数。linear就是匀速。

    然后在以实现的方式与指数或开根进行区分。Sine表示由三角函数实现Quad是二次方Cubic是三次方Quart是四次方Quint是五次方Circ使用开平方根的Match.sqit,Expo使用开立方根的Math.pow,Elastic则是结合三角函数与开立三方根的初级弹簧效果Back是使用一个1.70158常数来计算的回退效果Bounce则是高级弹簧效果

    http://hosted.zeh.com.br/tweener/docs/en-us/misc/transitions.html(有更多的ease效果)

    这些我们可以在AS库,jQuery.easing.js,mootools等库里拔下来,基本上大同小异。其中,jq的最规范,mootools使用循环生成的方式实现,代码最简,读者像实现自己的动画库,可以参考这两者

     jQuery标准库里只有这两条

        linear : function(p){
            return p
        },
        swing: function(p){
            return 0.5 - Math.cos{p*Math.PI} / 2;
        }
        //很显然与其它库的缓动参数兴盛不一样
        var easing = {
            esaeInQuad : function (t, b, c, d){
                return c * (t /= d) * t + b;
            },
            esaeOutQuad : function(t, b, c, d){
                return -c (t /= d) * (t - 2) + b; 
            },
            esaeInOutQuad : function(t, b, c, d){
                if ((t /= d /2 ) < 1) 
                    return c / 2 * t * t + b;
                return -c / 2 * ((--t) * (t - 2) - 1) + b
            }
            //...
        }

    这是因为jQuery在上面的计算过程中已经做了这一步,我们看看四个参数分别代表什么:
    T: timestamap, 指缓动效果开始执行到当前帧所经过的时间段,单位ms
    B: beginning val 起始值
    C: change, 要变化的总量
    D: duration ,动画持续的时间

    返回的是直接可用的数值,或许我们只要加个单位进行赋值就行了
    而jQuery那一个参数的风格,其实是当前时间减去动画开始时间除以总时间的比值,一个0到1的小数,它用于乘以总变化量,然后加上起始值,就是现在此样式的情况。

    下面是缓动应用,结合上例子:

        window.onload = function () {
            var el = document.getElementById("move");
            var parent = document.getElementById("way");
            var change = parent.offsetWidth - el.offsetWidth;
            //var distance = parent.offsetWidth - el.offsetWidth; //总移动距离
            
            var bengin = parseFloat(window.getComputedStyle(el, null).left) //开始位置
            console.log(bengin)
            var end = bengin + change; //结束位置
            var fps = 30; //刷新率
            var interval = 1000/fps ; //相隔多少ms刷新一次
            var duration = 2000  ; //时长
            var bounce = function(per) {
                if (per < (1 / 2.75)) {
                    return (7.5625 * per * per);
                } else if (per < (2 / 2.75)) {
                    return (7.5625 * (per -= (1.5 / 2.75)) * per + .75)
                } else if (per < (2.5 / 2.75)) {
                    return (7.5625 * (per -= (2.25 / 2.75))* per + .9375);
                } else {
                    return (7.5625 * (per -= (2.625 / 2.75)) * per + .984375);
                }
            }
            el.onclick = function(){
                var benginTime = new Date()
                var id = setInterval(function(){
                    var per  = (new Date - benginTime) / duration; //进度
                    if (per >= 1) {
                        el.style.left = end + "px";
                        clearInterval(id)
                    } else {
                        el.style.left = bengin + bounce(per) * change + "px";
                    }
                },interval)
            }
    
        }


    三,API设计

    现在我们看如何写动画引擎。由于选择器的流行,注定这个API到用里也是集化操作,一下子处理N个元素。但这也无所谓,关键是函数名与如何传参。

    jQuery的API易用性很好,我们看一下它的animate。它有两种用法。

        .animate(proprtties[,duration][,easing][,complete])
        .animate(proprtties,options)

    第一个参数恒为要进行动画的属性映射,在第一种情况下,其它参数都是可选的,因为duration除了show,fast,default这三个字符串就是数字,esaing为特殊的缓动公式的名字,complete的函数,options是对象。不过尽管说的轻松,jQuery在这里也化了几十行进行转换,最后转换两个对象的形式与css3keyframe animation的定义此吻合。

    我们还是利用上例子的方块,价个类名,就能看到效果了。

        #way{width:800px;height:100px;background:#e8e8ff;position:relative;}
        #move{position:absolute;left:0px;width:100px;height:100px;background:#a9ea00;}
        .animate{
            animation-duration:3s;
            animation-name:slidein;
            animation-timing-function:ease-in-out;
            animation-fill-mode:forwards;
        }
        @keyframes slidein{
            from {left:0%;background:white;}
            to {left:700px;background:red;}
        }

    这个动画分为两部分:一个是普通样式规则,用于描述动画所需的时长,缓动公式,结束后保留状态,重复多少次,及关键帧的动画引用名字(slidein),第二个是关键帧的规则。这里只有两个关键帧,实际上可以插入更多,最开始与结尾可以 用from和to命名,其实当你用javascript对应jq的anmimate方法的第一个参数。至于初始值,浏览器会自动计算,如果我们使用纯javascript实现方式,这个需要我们自己来计算了。动画引擎的强大与否,取决于css模块的强大。anmimate方法的第二个参数等对应animation-duration,animation-timing-function, anmation-fill-mode.

    此外,jQuery还提供一个queue参数,目的让作用于同一个元素的动画进行排队,在处理完这个在处理后一个,要不,同时用于浏览器的定时器就太多了。加之集化操作,会把它放大化,队列的重要性就更突出,为此jQuery把这个参数默认为true.

    从实现上,jQuery是放在元素对应的缓存体上,里边是一个promise对象,complete之后,会自动弹出下一个动画对象。所有动画对象都有自己的setInterval驱动,在YUI,kissy则有一个中央队列,所有不排队的动画全部放在这个数组中,然后有一个setInterval来驱动它们,排队的动画作为它的兄弟属性而存在(有的中央队列可能是一条二维数组),当前面的动画执行完,后面的动画就翻身。

    四,requestAnimationFrame

    如果一个页面运行许多定时器,那么你无论怎么优化,最后肯定是超过指定的时间才能完成动画,定时器越多,延迟越严重。为此,YUI,kissy等采用中央队列的方式,将定时器减少至一个,浏览器厂商firefox在4.0就推出mozRequestAnimationFrame,由于浏览器在维护队列,它内部掌握DOM渲染,事件队列等情况,所以大抵能保证fps在60左右。

    谷歌发现这个思路不错,整合到自己的chrome中去,相对而言,精简很多。与最后定案的标准相差很小。webkitRequestAnimationFrame有点像定时器,第一个是回调,第二个可选,传执行动画的元素节点进去,返回一个ID,然后允许像clearTimeout一样有个weibkitCancelRequest-AnimationFrame函数进行中止动画。不过名字有点长了,后来模仿firfox一样使用cancelAnimationFrame,不同的只是私有前缀。

    <script type="text/javascript">
        // chrome 10-23
        var startTime, 
            duration = 3000;
        function animate(now) {
            var per = (now - startTime) / duration;
            if (per >= 1) {
                window.webkitCancelRequestAnimationFrame(requestID);
            } else {
                document.getElementById("test").style.left = Math.round(600 * per) + "px";
                window.webkitRequestAnimationFrame(animate);//不断地归抵调用animate
            }
        }
        function start(){
            startTime = Date.now();
            requestID = window.webkitRequestAnimationFrame(animate)
        }
    </script>
    
    <button onclick="start()">click</button>
    <div id="test" style="position:absolute;left:10px;background:red">go</div>

    IE与Opera起步最晚,IE到10才开始支持,不过这时标准已经形成,没有私有前缀,没有兼容性问题。

    网上有很多非常不负责任的写法,因此,总结过很多写法,这里就不写出了。

    这里参考司徒正美,网友屈屈,月影的版本改进。

        //司徒正美从,网友屈屈,月影的版本改进。
        function getAnimationFrame(){
            //不存在msRequestAnimationFrame,IE10和chrome24直接使用requestAnimationFrame
            if (window.requestAnimationFrame) {
                return {
                    request:requestAnimationFrame,
                    cancel:cancelRequestAnimationFrame
                }
                //firefox11没有实现cancelRequestAnimationFrame
                //并且mozRequestAnimationFrame与标准出入过大
            } else if (window.mozCancelRequestAnimationFrame && window.mozCancelAnimationFrame) {
                return {
                    request:mozRequestAnimationFrame,
                    cancel:mozCancelAnimationFrame
                }
            } else if (window.webkitRequestAnimationFrame && webkitRequestAnimationFrame(String)) {
                return { //修正某个特异的webkit下没有time的参数
                    request:function(callback) {
                        return window.webkitRequestAnimationFrame(
                            function(){
                                return callback(new Date - 0);
                            });
                    },
                    cancel:window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame
                }
            } else {
                var millisec = 25; //40fps
                var callbacks = [];
                var id = 0, cursor = 0;
                function playAll(){
                    var cloned = callbacks.slice(0);
                    cursor += callbacks.length;
                    callbacks.length = 0 ;//清空队列
                    for (var i = 0; callback; callback = cloned[i++]) {
                        if (callback !== "cancelled") {
                            callback(new Date - 0);
                        }
                    }
                }
                window.setInterval(playAll, millisec) ;
                return {
                    request:function(handler){
                        callbacks.push(handler);
                        return id++;
                    },
                    cancel: function(id){
                        callbacks[id - cursor] = "cancelled"
                    }
                };
            }
        }

    当然,requestAnimationFrame不是没有缺点,它不能控制fps,比如我们做一些慢动作,许多回调都是做无用功。另外一个极端,在动作,枪战等动态场景,如果帧数不够高,换就会发虚,模糊。利用原始的setTimeout(在IE9 10 firefox10,chrome等浏览器中,它的最短时隔已经压缩到4ms,能轻松跑100帧以上的动画),让动画更逼真,特写镜头丝丝入扣。

    另外我们可以尝试postMessage这个异步方法,能实现超高度的动画(IE10 有setImmediate,速度也相当不错)。
    (实验略。。)

    视电脑性能而言,结果大致如下

    类型 setTimeout requestAnimationFrame loop postMessage
    平均帧数 200 60 200-300 900-1000

    微软官方,也放出使用高性能异步的方法setImmediate,与原始的setTimeout的对比实验,流程度很好。

    在现实中,尤其是游戏中的,结合多种异步API是很有必要的。如作为背景的树木,流水,NPC用requestAnimationFrame实现,而玩家的角色,由于需要点击,再配合速度,体力,耐力等元素,其走路的速度是可变的,用setTimeout实现比较合适。一些非常炫酷的动画,可能就需要postMessage,image.one, setImmediate, MessageChancel等API.

     (本章完结

    上一章:第十二章:异步处理 下一章:第十四章: 插件化

  • 相关阅读:
    C# WinForm 只允许运行一个实例
    C# WinForm 获得文字的像素宽度
    Windows 下使用命令行升级 Npm 和 NodeJS
    每日踩坑 2020-04-15 C# 与 Java 对应 DES 加密算法
    robot自动化测试(二)--- template使用
    robot自动化测试(一)---安装
    linux io优化
    python 远程统计文件
    编程类学习资料整合,更多干货
    两份安卓学习资料,我建议你看完
  • 原文地址:https://www.cnblogs.com/ahthw/p/4751769.html
Copyright © 2011-2022 走看看