為什么不能用速度與時間的關系去實現動畫


由於最近做了一些頁面的動畫效果,之前經驗不多,這次做的過程中碰到些問題,加之很早前就閱讀過一篇很好介紹動畫的博客《關於動畫,你需要知道的》,來自十年蹤跡,所以就思考了一些關於動畫的基本原理的問題,比如本文這個。這個問題要簡單也可以非常簡單,比如前面提到那篇博客里就有一個比較好的解釋,本文提供的是另外一種更詳細地方式,希望對有需要的人有所價值。

在客觀的物體運動中,以勻速直線運動為例,我們可以同時用速度與時間曲線或位移與時間曲線來描述物體的運動:

image

不管是用速度與時間的關系還是位移與時間的關系來描述客觀物體的直線運動,物體的狀態都是一致的,這是因為客觀物體的運動總是沿着人無法改變的客觀時間軸進行變化,在時間軸上的任意一點,總有特定的速度以及位移與之對應。

而在網頁動畫中,雖然它也呈現為運動,但是我們不能用客觀物體的運動規律去描述它。我認為原因主要是動畫的本質不是運動,僅僅是基於定時器對元素狀態進行的瞬間改變。以一個簡單的元素進行水平勻速偏移的動畫效果為例,要實現這個動畫,只要用一個定時器在一個固定的時間間隔,重新設置元素的x軸偏移量即可,大概用圖可以描述如下:

image

圖中t1~t6代表定時器回調函數執行的時刻。在這個效果中,元素的偏移位置將在定時器每次執行的時刻發生變化,而在相鄰的兩個執行時刻之間,元素的偏移位置是不變的。我們看到的動畫,僅僅是因為定時器間隔時間太短,從視覺上感知不到這段時間的過程,如果將定時器間隔加到足夠長,我們就能看到元素在間隔時間內的狀態了。

正因動畫不是運動,所以我們在嘗試理解一些動畫過程的時候,不能用運動規律去思考。比如我們該如何去理解動畫停止那一刻的狀態?還以前面提到的這個動畫效果為例,當把定時器清掉的時候,動畫瞬間停止,對於元素而言,它的動畫速度將驟變為0,如果我們類比到客觀的物體運動,總是會想當然地以為元素的動畫也應該先有個減速的過程才能停止下來,要是這樣想,就沒辦法理解元素動畫停止時驟停的原理了。但是當我們從動畫的本質去思考這個問題的時候,就很好理解了,因為定時器是元素在動畫過程中發生狀態改變的唯一要素,當定時器不起作用的時候,就沒有外在的力量去改變元素的狀態了,它還怎么能動呢?

盡管動畫不是運動,我們還是希望找到一個方式,能夠很好的控制動畫的快慢,以便打造更加流暢,更加逼近客觀世界的動畫效果。當提到快慢,就很容易想到速度,因為在客觀物體運動中,速度就是用來描述運動快慢的要素。而且用速度的規律來控制動畫的快慢,看起來也很好理解和實現。將前面的的例子再具體一點,假如我們想實現一個元素在1秒內往右勻速偏移120px的動畫效果,那么只要用定時器控制元素每次往右偏移固定的量即可,這里面定時器每次執行給元素添加的偏移量,就是我們用來控制動畫的速度。如果我們以16ms作為定時器的間隔,那么這個動畫的速度可以通過: 120px / (1000ms / 16ms) 求得(約等於 2px),也就是說只要定時器每次執行的時候將元素往右偏移2px就能實現我們要的效果。簡單代碼實現如下:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="box" style="width: 100px;height: 100px;background-color: goldenrod">
    </div>
    <br>
    <button type="button" onclick="start()">開始</button>
</body>
<script>
    var box = document.getElementById('box');
    function start() {
        var duration = 1000;//動畫時長
        var s = 120;//總的偏移量
        var cur_s = 0;//當前偏移總量
        var p = 16;//定時器間隔
        var speed = s / (duration / p);//速度
        var count = 0;
        var start_time = new Date().getTime();
        var timer = setInterval(function(){
            if(cur_s >= s) {
                clearInterval(timer);
                console.log('動畫運行時間(ms): ' + (new Date().getTime() - start_time));
                return;
            }

            count++;
            cur_s = speed * count;
            box.style.transform = 'translateX(' + cur_s + 'px)';
        },p);
    }
</script>
</html>

在瀏覽器中運行以上代碼,動畫效果肯定是跟預期一致的,而且動畫的實際執行時間也與規定的時長相差很小:

image

至於為什么不完全等於1000ms,那是因為多的那20多毫秒都耗費在了代碼執行上。

通過這個例子,看起來,我們用速度去控制動畫的思路還比較可行。事實上,這種思路是很有局限性的,我不是說它不行,只是說局限性,就是只能用於小部分的場合,而不能適用更廣泛的動畫效果中。為什么呢,原因有多個方面。

先從定時器說起。

定時器給了我們一種通過代碼的方式來管理時間軸,但是這個時間軸與客觀時間軸是有差別的。假如我們把一個動畫的定時器間隔放大,放大到1000ms,讓這個定時器執行10次,定時器執行的真實時間間隔會等於1000ms嗎?

<script>
    var start = new Date().getTime(),count = 1;
    var timer = setInterval(function(){
        var end = new Date().getTime();
        console.log('' + count++ + '次執行,間隔:' + (end - start));
        start = end;

        if(count == 11) {
            clearInterval(timer);
        }
    },1000);
</script>

以上代碼模擬了一個動畫,並且放大了動畫的時間間隔,如果把它拿到瀏覽器中執行,我們會得到下面類似的結果:

image

從這個結果可以看出,雖然定時器的間隔設置為了1000ms,但是實際的執行間隔卻只能說在1000左右浮動。這是很正常的,假如我們把操作系統的時間看成是客觀的時間軸,那么瀏覽器里面定時器構建的時間軸只能是一個盡可能的接近客觀時間軸的模擬時間軸。操作系統的狀態,瀏覽器的狀態,定時器內外代碼的執行時間都會影響這根時間軸與客觀時間軸的差距,只考慮瀏覽器內部,定時器內外的代碼執行時間越長,這其中的差距越大。因為上面的代碼是在一個很簡單的網頁中測試出來的,所以定時器的實際間隔與客觀時間的偏差很小,要是一個頁面內容比較多的時候,這個偏差一定會比現在的大。

時間軸的不穩定性,會直接導致速度的不穩定性,也就是說勻速運動都無法達到理想狀態,更別說其它復雜的變速運動了。

單從這點來說,不管用什么方式控制運動,都會存在這個問題,所以它還並不能完全說明速度控制動畫的根本問題所在。這個根本問題在於無法確保動畫能夠按照規定的時長完成。在上面的例子的基礎上,我們想辦法把定時器的時間軸與客觀時間差的偏差放大,這個不難辦到,只要在定時器執行過程中,加入一些耗時任務即可,代碼如下:

<script>
    var start = new Date().getTime(), prev = start, count = 1;

    //在動畫模擬的第2和第3秒之間插入一個耗時任務
    setTimeout(function () {
        var i = 0;
        var cur = new Date().getTime();
        console.log('耗時任務開始,距動畫開始時間:' + (cur-start));
        while (++i < 3000000000);
        var cur2 = new Date().getTime();
        console.log('耗時任務結束,距動畫開始時間:' + (cur2-start) + ',耗時:' + (cur2-cur));
    }, 2400);

    //模擬一個動畫
    var timer = setInterval(function () {
        var end = new Date().getTime();
        console.log('' + count++ + '次執行,間隔:' + (end - prev));
        prev = end;

        if (count == 11) {
            clearInterval(timer);
        }
    }, 1000);
</script>

把以上代碼在瀏覽器中運行,我們可以得到下面的類似結果:

image

根據以上結果中的時間范圍,我們把這個例子的整個過程轉換為時間軸示意圖的話,就能看得更清晰了:

image

在這個圖中,忽略了臨界點之間的微小差距,因為只要觀察那些大的差距,就能發現問題。結合前面的代碼跟示意圖,我們能看出:

由於有耗時任務的加入,導致動畫的實際執行總時間接近於12s,比規定的動畫時長多出整整2s。

雖然說從上面的圖中也能看到另外一個問題,就是動畫第三次執行的時間間隔被延長為3.67,而第四次執行的時間間隔被縮短為0.33s,會導致動畫在這個時間段左右會看到不連貫不流暢的效果,但是這個問題不管用什么樣的方式都會存在,只要有其它耗時任務在處理,動畫定時器的回調就必須排隊等待耗時任務完成才能執行。

無法控制動畫在規定時長內完成,是不能用速度與時間的關系去實現動畫的最重要的原因。

綜上所述,為什么不用速度去控制動畫有兩個原因:

一是因為動畫的時間軸的不穩定性(耗時任務會加大這種不穩定性),導致速度的變化規律很難把握。即使是勻速動畫,我們也要考慮定時器的間隔,動畫的偏移量,動畫的時長三個參數才能計算出一個平均速度。如果是變速動畫呢,比如我們想要一個動畫先加速再勻速后減速,這種動畫快慢的控制要求顯然就無法輕易實現了。

二就是因為無法控制動畫時長。

那么用什么樣的方式來控制動畫,就能夠達到我們想要的輕易控制動畫速度的目標呢?

用偏移量(位移)跟時間的關系嗎?顯然也是不行的,因為僅僅是單純的速度控制改變為位移控制,並不會從根本上解決問題,因為速度與時間的關系還有位移與時間的關系是等價的。

速度無法控制動畫時長的原因在於,由於已知的動畫偏移量跟動畫時長,導致動畫定時器的執行次數也是固定的!所以只要某些次數定時器的實際執行時間超過理想的執行間隔,就會拉長動畫時間軸跟客觀時間軸的差距,就像上面示意圖所看到的那樣。

真正能解決動畫時長的控制問題在於我們一定要用客觀時間軸去控制動畫。這個能做到嗎?當然是可以的,來看看正確實現一個動畫的方式,還是以前面那個小方塊往右移動的動畫為例,代碼修改如下:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="box" style="width: 100px;height: 100px;background-color: goldenrod">
    </div>
    <br>
    <button type="button" onclick="start()">開始</button>
</body>
<script>
    var box = document.getElementById('box');
    function start() {
        var duration = 1000;//動畫時長
        var s = 120;//總的偏移量
        var start_time = Date.now();
        var timer = setInterval(function(){

            //percent表示動畫的進程
            var percent = (Date.now() - start_time) / duration;

            if(percent >= 1.0) {
                percent = 1;
                clearInterval(timer);
                console.log('動畫運行時間(ms): ' + (new Date().getTime() - start_time));
            }

            box.style.transform = 'translateX(' + (Math.floor(s * percent)) + 'px)';
        },16);
    }
</script>
</html>

實際執行結果如下:

image

接下來我們總結下這個方式的做法。首先在代碼中可以看到,我們用這種方式

image

引入了動畫進程的概念,通過動畫進程來控制動畫的完成:

image

由於percent這個動畫進程,我們是基於客觀時間軸得出的,這樣就能保證動畫一定能夠在規定的時間內完成,不會再出現速度控制動畫時,動畫執行時間被延長的問題了。(當然如果在動畫執行過程中,我們加入一個非常耗時的任務的話,不管什么動畫都無法在規定時間內完成)。

接着我們在處理動畫的偏移量的時候,就只需要將總的偏移量 * 動畫進程 就得到當前執行時刻的偏移量了。

最后當動畫進程為1的時候,動畫結束,並且元素被設置為了動畫規定的總的偏移量。

總的來說,這個方式就是把位移與時間的關系,轉換成了偏移量與動畫進程的關系。通過動畫進程,同時控制動畫時長和動畫偏移的完成度。

更重要的是,偏移量與動畫進程的關系,可以經由客觀的運動規律推導出來:

比如上面的例子中,由於是勻速動畫,所以它的規律是:

動畫進程 p = t / T; (t = Date.now() – start_time ; T = duration)

偏移量 Sp = S * p; (S為總的偏移量;Sp為當前偏移量)

如果是其它的動畫,比如勻加速動畫,勻減速動畫,圓周動畫,我們也能得到類似的規律。而且所有動畫效果動畫進程計算方式都是一樣的,唯一不同的是偏移量跟動畫進程的關系而已:

勻加速:Sp=S * P2

勻減速:Sp=S * P * (2−P)

圓周x軸: Sp=S * cos(ω * P)

圓周y軸: Sp=S * sin(ω * P)

(以上四種關系的推導我也沒有仔細研究,早先的數學知識忘了不少,感興趣的可以去研究《關於動畫,你需要知道的》)

同一個動畫,應用以上不同的規律,就可以看到不同速度的動畫變化效果,最終就實現了我們想要用動畫模擬現實世界物體運動的目的。

再研究這些偏移量跟動畫進程的關系,我們發現,總的偏移量S在這個關系中,僅僅是一個參數的作用,當把S去掉時,我們就得到一個跟S完全無關,僅僅跟動畫進程有關系的方程:

勻速:ep = p

勻加速:ep= P2

勻減速:ep= P * (2−P)

圓周x軸: ep= cos(ω * P)

圓周y軸: ep= sin(ω * P)

用一個函數來表示以上所有規律就是:ep = E(P),P∈[0,1],P代表動畫進程,ep代表偏移量的完成百分比。需要再補充的是,這個關系還必須滿足一個條件就是當P=0的時候,ep 必須為0;P=1的時候,ep必須為1。這個應該很好理解了,因為P=0和P=1,以及ep=0和ep=1分別代表動畫的開始跟結束狀態。

也就是說,只要找到一個函數滿足上一段文字的所有條件,比如前面的那些,那么這個函數就可以作為我們控制動畫快慢的方法。這個函數

就是所謂的動畫算子ease。下面的這些函數圖像都可以作為動畫的算子:

image

有了這個規律,就賦予了動畫效果控制無限的可能性,因為能滿足前面那些條件的函數是無窮的。而這些看起來無窮盡的函數,我們能夠輕松地通過貝塞爾曲線工具繪制出來,並且在css里面我們可以直接把這個工具的參數直接應用於transition跟animation里面。js里面也有bezier-easing 庫可以使用這個工具的參數,然后應用到我們用js寫的動畫里面。比如:

<script>
    var box = document.getElementById('box');
    function start() {
        var duration = 1000;//動畫時長
        var s = 120;//總的偏移量
        var start_time = Date.now();

        var easing = BezierEasing(0.86, 0, 0.07, 1);

        var timer = setInterval(function(){

            //percent表示動畫的進程
            var percent = (Date.now() - start_time) / duration;

            if(percent >= 1.0) {
                percent = 1;
                clearInterval(timer);
                console.log('動畫運行時間(ms): ' + (new Date().getTime() - start_time));
            }

            box.style.transform = 'translateX(' + (Math.floor(s * easing(percent))) + 'px)';
        },16);
    }
</script>

總之,有了ease跟貝塞爾曲線工具,要實現不同的動畫速度控制效果,就變成一件特別容易的事情了。

最后,希望這篇文章能幫助到一些朋友更好理解動畫的原理以及動畫速度控制的正確方式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM