第十三章 動畫引擎

動畫是咱們眼睛中的殘影,叫視覺暫留現象。這裏有兩個關鍵字,差別快速javascript

在網頁中,擴展樣式的任務早已經交由css處理,讓javascript第一次擁有視覺處理的api,setTimeout與setInterval早在css誕生前就已經出現。css


一:動畫的原理html

在標準瀏覽器中,可計算的樣式基本瀏覽器已經爲你轉化好,好比width,height,margin-x,border-x-width,padding-x.這些樣式單位爲px,color和background-color則被分解成RGB,這個很容易被格式化爲一個數組,透明度自不用說。java

不過css3新引進的變形樣式transfrom,一種是面向程序員,是rotate()/skew()/scale()/translate(),分別有x, y之分,如rotateX(),rotateY(),依次類推;css3

一種是面向計算機,傳入矩陣進去,matrix()。不管傳什麼,都會轉成矩陣。程序員

若是傳承是舊版本的IE,就得本身轉換了。好比你原來的單位是em,currentStyle就會返回em,原來填充的顏色是red,它不會返回rgb(255,0,0)web

所以,動畫引擎的第一步就是設法得到元素的精確樣式,這個在第九章:樣式模塊已經介紹,給出的$.css方法基本上除了顏色值都是已經轉換好的。chrome

如今咱們嘗試讓一個方塊運動起來,簡單說就是變化位置。通常來講,咱們變化一個元素的top,left便可,固然也能夠設置margin,這裏只說元素定位的移動方法,要取得元素的left值,直接可使用getComputedStyle,而後讓用戶傳值改變值。就是咱們須要一點點的變更值的大小。另外還要涉及到時長,就是動畫的總共執行時間,就是執行的總時間,另外一個就是每次變更的相隔時間這個一般由引擎來決定,固然也能夠暴露出來,它有個學名叫FPSapi

fps通俗的來講,叫刷新率,在1秒內更新多少次畫面,根據人視覺停留效應,停留多少秒最合適呢?咱們不但要照顧人的眼睛,還要顧及下顯示器的顯示速度與瀏覽器的渲染速度 ,根據國外的統計,25毫秒爲最佳數值。數組

所以,咱們建立一個新頁面,裏邊有一個方塊,它位於這個軌道上,它從一端到另一端,動畫時間爲2秒,fps爲30幀。

<style type="text/css">
    #way{width:800px;height:100px;background:#e8e8ff;position:relative;}
    #move{position:absolute;left:0px;width: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.

 (本章完結

上一章:第十二章:異步處理 下一章:第十四章: 插件化

相關文章
相關標籤/搜索