欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS前端可視化canvas動(dòng)畫原理及其推導(dǎo)實(shí)現(xiàn)

 更新時(shí)間:2022年08月03日 09:52:02   作者:尤水就下  
這篇文章主要為大家介紹了JS前端可視化canvas動(dòng)畫原理及其推導(dǎo)實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

到目前為止我們的 fabric.js 雛形已經(jīng)有了,麻雀雖小五臟俱全,我們不僅能夠在畫布上自由的添加物體,同時(shí)還實(shí)現(xiàn)了點(diǎn)選和框選,并且能夠?qū)λ鼈冏鲆恍┳儞Q,不過只有變換這個(gè)操作還不夠靈活,要是能夠讓物體動(dòng)起來就好了,于是就引入了這個(gè)章節(jié)的主題:動(dòng)畫,以及動(dòng)畫最核心的一個(gè)問題,如何保證在不同的電腦上達(dá)到同樣的動(dòng)畫效果?然后說干就干,立馬開擼??。

雖然我寫的是系列文章,但每個(gè)章節(jié)單獨(dú)食用是木問題的,所以,請(qǐng)放心大膽的看??。

動(dòng)畫的本質(zhì)

先來看看在 canvas 庫中調(diào)用動(dòng)畫的一般方式吧,比如我們要讓一個(gè)矩形動(dòng)起來,大體是下面這樣的用法:

rect.animate(
    { top: 50, left: 400, angle: 45 }, // 要?jiǎng)赢嫷膶傩?
    { duration: 1000, onChange: canvas.renderAll.bind(canvas) } // 動(dòng)畫執(zhí)行時(shí)間和手動(dòng)渲染
);

代碼淺顯易懂,然后我們來想想動(dòng)畫的本質(zhì)是什么,為什么我們能夠看到動(dòng)畫效果呢?這個(gè)大家應(yīng)該都有所了解,不就是畫布重新繪制了嗎,只要重繪的足夠多足夠快,根據(jù)人的視覺殘留效應(yīng),就形成了動(dòng)畫。

沒錯(cuò),大體就是這個(gè)原因,但我們可以更具體一點(diǎn),想想畫布為什么要重新繪制呢?不就是因?yàn)楫嫴贾心硞€(gè)物體的某個(gè)值改變了,所以我們才要更新一下畫面,以此來表示它動(dòng)了。這個(gè)物體狀態(tài)值的改變才是動(dòng)畫的根本原因??。

比如一個(gè)物體要花 1s 的時(shí)間從 left=100 的地方移動(dòng)到 left=200 的地方,只要我不斷修改 left 值,然后不斷 renderAll 就能看到物體從左往右移動(dòng)了。這很好理解,但是有個(gè)新問題出現(xiàn)了,它應(yīng)該怎樣移動(dòng)呢?勻速、加速還是減速?又或者是其他方式呢?其實(shí)都可以,具體要看你希望這個(gè) left 怎么變,以怎樣的規(guī)律變化。

動(dòng)畫的實(shí)現(xiàn)

既然動(dòng)畫的本質(zhì)就是值的改變,那這個(gè)值的改變和哪些因素有關(guān)呢?根據(jù)剛才的例子我們可以知道大概有以下四個(gè)因素:

  • 初始值:startValue
  • 結(jié)束值:endValue
  • 值的變化時(shí)間:duration
  • 怎么變(勻速、緩動(dòng)還是彈動(dòng)):easing(一個(gè)熟悉的單詞出現(xiàn)了)

顯然動(dòng)畫也是一個(gè)通用的東西,所以我們把它寫在 Util 工具類里,代碼不多,直接食用就行????:

interface IAnimationOption {
    /** 初始值 */
    startValue?: number;
    /** 最終值 */
    endValue?: number;
    /** 執(zhí)行時(shí)間 */
    duration?: number;
    /** 緩動(dòng)函數(shù) */
    easing?: Function;
    /** 動(dòng)畫一開始的回調(diào) */
    onStart?: Function;
    /** 屬性值改變都會(huì)進(jìn)行的回調(diào) */
    onChange?: Function;
    /** 屬性值變化完成進(jìn)行的回調(diào) */
    onComplete?: Function;
}
class Util {
    static animate(options: IAnimationOption) {
        window.requestAnimationFrame((timestamp: number) => { // requestAnimationFrame 會(huì)有個(gè)默認(rèn)參數(shù) timestamp,單位毫秒,表示開始去執(zhí)行回調(diào)函數(shù)的時(shí)刻
            // 初始化一些變量
            let start = timestamp || +new Date(), // 開始時(shí)間
                duration = options.duration || 500, // 動(dòng)畫時(shí)間
                finish = start + duration, // 結(jié)束時(shí)間
                time, // 當(dāng)前時(shí)間
                onChange = options.onChange || (() => {}), // 值改變進(jìn)行的回調(diào)
                easing = options.easing || ((t, b, c, d) => -c * Math.cos((t / d) * (Math.PI / 2)) + c + b), // 緩動(dòng)函數(shù),不用管名字,簡(jiǎn)單理解為一個(gè)普通函數(shù)即可,它會(huì)返回一個(gè)數(shù)值
                startValue = options.startValue || 0, // 初始值
                endValue = options.endValue || 100, // 結(jié)束值
                byValue = options.byValue || endValue - startValue; // 值的變化范圍
            function tick(ticktime: number) { // tick 的主要任務(wù)就是根據(jù)當(dāng)前時(shí)間更新值
                time = ticktime || +new Date();
                let currentTime = time > finish ? duration : time - start; // 當(dāng)前已經(jīng)執(zhí)行了多久時(shí)間(介于0~duration)
                onChange(easing(currentTime, startValue, byValue, duration)); // 根據(jù)當(dāng)前時(shí)間和 easing 函數(shù)算出當(dāng)前的動(dòng)畫值是多少,easing 理解成一個(gè)普通函數(shù)就行,它會(huì)返回一個(gè)值,就像這樣:curVal = f(x) = easing(currentTime)
                if (time > finish) { // 動(dòng)畫結(jié)束
                    options.onComplete && options.onComplete(); // 動(dòng)畫完成的回調(diào)
                    return;
                }
                window.requestAnimationFrame(tick); // 循環(huán)調(diào)用 tick,不斷更新值,從而形成了動(dòng)畫
            }
            options.onStart && options.onStart(); // 動(dòng)畫開始前的回調(diào)
            tick(start); // 開始動(dòng)畫
        });
    }
}

相信上面的注釋應(yīng)該解釋的清清楚楚、明明白白。不過還是要著重講下其中的兩個(gè)點(diǎn):

  • 一個(gè)是為什么使用 requestAnimationFrame 這個(gè) api 來完成動(dòng)畫,這應(yīng)該也是個(gè)老生常談的問題了,因?yàn)?setInterval 和 setTimeout 不準(zhǔn),很容易出問題,比如執(zhí)行時(shí)機(jī)不準(zhǔn)確、切換頁面回來會(huì)堆積執(zhí)行、不流暢等,并且它們也不是專門為動(dòng)畫而生(當(dāng)然如果你不習(xí)慣用 requestAnimationFrame 也可以直接把它換成 setTimeout,方便自己理解);
  • 而 requestAnimationFrame 是按幀率刷新的,跟著幀率走的期間我們就可以不用做很多無用功,能夠更好的知道繪制下一幀的最佳時(shí)機(jī),也比較流暢。它們的一個(gè)最主要的區(qū)別就是:
  • setInterval 和 setTimeout 是主動(dòng)告訴瀏覽器什么時(shí)候去繪制;
  • 而 requestAnimationFrame 則是瀏覽器在它覺得可以繪制下一幀的時(shí)候通知我們(你品,你細(xì)品,就有那味了)。

當(dāng)然我們肯定不能直接傻傻的像下面這樣調(diào)用????:

// 假設(shè)要從左到右運(yùn)動(dòng)
let left = 100;
function tick() {
    left++; // 更新值
    window.requestAnimationFrame(tick);
}
tick();

因?yàn)槊總€(gè)屏幕刷新頻率不一樣,如果像上面這樣寫,在有的電腦上就會(huì)快一些,有的電腦上就會(huì)慢一些,不僅如此在頁面切換到后臺(tái)的時(shí)候幀率也會(huì)降低,就會(huì)導(dǎo)致各種問題,這顯然不是我們期望的。

所以要怎么做呢?

我們應(yīng)該是以時(shí)間為維度來播放動(dòng)畫,因?yàn)闀r(shí)間對(duì)我們來說流逝的速度是一樣的,所以在動(dòng)畫一開始的時(shí)候需要記錄下開始時(shí)間 start,之后動(dòng)畫播放到哪里都會(huì)以這個(gè)開始時(shí)間為基準(zhǔn),回頭看看剛才代碼中計(jì)算當(dāng)前動(dòng)畫執(zhí)行了多長時(shí)間的方式:

let currentTime = time > finish ? duration : time - start;

就是以 start 為基準(zhǔn)的,這點(diǎn)很重要。

第二點(diǎn)是關(guān)于 easing 函數(shù),雖然好像接觸過,但還是會(huì)有很多同學(xué)對(duì)此感到疑惑,所以接下來我會(huì)專門講下這方面的內(nèi)容,比如:這個(gè)函數(shù)是干嘛的、是怎么推導(dǎo)的、最終又是得到什么結(jié)果、和我們平時(shí)說的緩動(dòng)函數(shù)是一個(gè)東西嗎等等之類的。

動(dòng)畫的推導(dǎo)

在講解 onChange(easing(currentTime, startValue, byValue, duration)) 這個(gè)東西之前,我們先來看看如何讓每個(gè)物體都具有動(dòng)畫的方法,就是在物體基類中擴(kuò)展就行了,瞟一眼就行????:

class FabricObject { // 物體基類
    _animate(property, to, options: IAnimationOption = {}) { // 某個(gè)屬性要變化到哪里
        options = Util.clone(options);
        let currentValue = this.get(property); // 獲取初始值
        if (!options.from) options.from = currentValue; // 一般不傳初始值的話就默認(rèn)取當(dāng)前屬性值
        Util.animate({
            startValue: options.from,
            endValue: to,
            easing: options.easing, // 決定了值如何變化,常用的就緩動(dòng)和彈動(dòng)
            duration: options.duration,
            onChange: (value) => { // value 是 easing 函數(shù)的返回值,本質(zhì)就是值的計(jì)算,value = easing()
                this.set(property, value); // 重新設(shè)置屬性值
                options.onChange && options.onChange(); // 值改變之后,調(diào)用 onChange 回調(diào)就會(huì)重新渲染畫布,數(shù)據(jù)和視圖分開的優(yōu)點(diǎn)又體現(xiàn)了出來
            },
            onComplete: () => {
                this.setCoords(); // 更新物體自身的一些坐標(biāo)值等
                options.onComplete && options.onComplete(); // 動(dòng)畫結(jié)束的回調(diào)
            },
        });
    }
}

然后再強(qiáng)調(diào)一下,動(dòng)畫的核心就是值的變化,Util.animate 中的 easing 函數(shù)其實(shí)就是計(jì)算動(dòng)畫播放到 (0, duration) 中間某一時(shí)刻的值是多少,僅此而已。再來簡(jiǎn)單說下 easing 函數(shù)吧,一般可以叫它緩動(dòng)函數(shù)。

它是首先是一個(gè)函數(shù),并且會(huì)返回一個(gè)數(shù)值,類似于 y = f(x),在我們的例子中就是 value = easing(time, beginValue, changeValue, duration)。這個(gè)函數(shù)有四個(gè)參數(shù)(當(dāng)前時(shí)間、初始值、變化量 = 結(jié)束值-初始值、動(dòng)畫時(shí)間),返回的是當(dāng)前時(shí)間點(diǎn)所對(duì)應(yīng)的值 value,顯然后面三個(gè)參數(shù)是已知的,也是固定的,唯一會(huì)變化的就是當(dāng)前時(shí)間,它的取值范圍就是從 0 到 duration。

執(zhí)行動(dòng)畫的時(shí)候其實(shí)就是改變這個(gè)當(dāng)前時(shí)間,根據(jù)當(dāng)前時(shí)間我們代入 easing 函數(shù)就能夠得到對(duì)應(yīng)的 value 值。

可能有同學(xué)還是不懂這個(gè)緩動(dòng)函數(shù),其實(shí)是因?yàn)楸簧厦娴墓交W×?,公式都是推?dǎo)之后的簡(jiǎn)便寫法,直接去看式子是很難理解的,單憑公式在腦海中想象出動(dòng)畫效果也不太現(xiàn)實(shí),所以這里給大家簡(jiǎn)單推導(dǎo)一下這種式子怎么來的,以最簡(jiǎn)單的勻速運(yùn)動(dòng)為例子,看看下面這張圖????:

上面這個(gè)過程很顯然,也不用怎么推導(dǎo),下面我們來看另一個(gè)更加通用的例子,首先隨便拿一個(gè)函數(shù) y = x * x(其他的也行),順便簡(jiǎn)單畫下函數(shù)圖像????:

綠色代表起點(diǎn),也就是動(dòng)畫起始值,紅色代表終點(diǎn),也就是動(dòng)畫結(jié)束值。x 軸就是動(dòng)畫時(shí)間,y 軸就是當(dāng)前的動(dòng)畫值,為了方便和統(tǒng)一,我們需要把時(shí)間換算成 [0, 1] 的范圍,0 就是起點(diǎn),1 就是終點(diǎn),y 軸代表的值也是一樣的道理。

然后我們的起點(diǎn)和終點(diǎn)就是(0,0)和(1,1)點(diǎn)

(注意:雖然xy的范圍都是0到1,看起來是個(gè)正方形,但它們的單位或者說表達(dá)的意思是不一樣的,不要混淆了),起點(diǎn)和終點(diǎn)是固定不變的,中間的曲線可以隨便怎么畫,那怎么將它寫成一個(gè)緩動(dòng)函數(shù)呢?

我們先看看 x 軸代表什么,x 是一個(gè)取值范圍從0到1的變量,看看我們的緩動(dòng)函數(shù)有啥變量呢,就一個(gè) currentTime,但是 currentTime 的取值范圍是從 [0, duration],所以我們需要把它映射成[0, 1],其實(shí)也就是把 currentTime / duration 就行,然后用 currentTime / duration 代替 x;

那 y 呢,y 根據(jù) x 算出來的值,代表的是當(dāng)前這個(gè)時(shí)間點(diǎn)所對(duì)應(yīng)的值,也就是我們緩動(dòng)函數(shù)的 value 值,它的取值范圍在 [startValue, startValue + byValue] 之間,所以我們也需要將其變成[0, 1],所以 value 的值變成了這樣(value - startValue) / byValue,那么現(xiàn)在 y 值也有了,我們就可以將它們直接代入 y = x * x 這個(gè)初始公式,就像這樣:

① y = x * x
???? 代入 x、y
② (value - startValue) / byValue = (currentTime / duration) * (currentTime / duration)
???? 整理一下
③ value = (currentTime / duration) * (currentTime / duration) * byValue + startValue
???? 簡(jiǎn)化一下(簡(jiǎn)化英文單詞而已??)
④ value = (t, b, c, d) => ((t/d) * (t/d) * c + b)

這個(gè)效果其實(shí)就是 easeInQuad 先慢后快的緩入效果,其他函數(shù)也是一樣的推導(dǎo)方式,只要你能寫出來。不過即便知道了怎么推導(dǎo),你也很難有個(gè)直觀的效果,其實(shí)常見和常用的就那么幾個(gè),網(wǎng)上也有大把封裝好的和演示的,有個(gè)印象就行(比如可以搜一下 Tween.js)。

當(dāng)然你也可以看函數(shù)圖像簡(jiǎn)單猜一下效果,具體就是看每一點(diǎn)的斜率,斜率越趨近于水平就越慢,斜率越趨近于豎直就越快;如果你的函數(shù)曲線中有 y 值超出了 1,就說明中間點(diǎn)在某一時(shí)刻會(huì)超過終點(diǎn),如果有 y 值小于 0,就說明有中間點(diǎn)有某一時(shí)刻會(huì)小于起始點(diǎn),大概是這么個(gè)意思??。

緩動(dòng)函數(shù)有個(gè)很大的特點(diǎn),就是起點(diǎn)和終點(diǎn)位置是確定的,中間位置你可以隨便算,可快可慢,可以超出終點(diǎn),也可以小于起點(diǎn),具體什么效果,你可以隨便寫個(gè)方程運(yùn)行試試,然后再根據(jù)效果調(diào)試。相信你肯定見過下面這種類型的圖:

現(xiàn)在再看看,不知道會(huì)不會(huì)感到稍微親切一點(diǎn)點(diǎn)嘞???

小結(jié)

本章我們主要講解了 canvas 中動(dòng)畫的實(shí)現(xiàn),其中最重要的一點(diǎn)就是如何在不同幀率達(dá)到同樣的動(dòng)畫效果,那就是要以時(shí)間為維度來進(jìn)行度量,用 canvas 做的游戲也是一樣,時(shí)間每向前 tick 一次(滴答的意思,挺形象的叫法,古老時(shí)鐘的那種感覺),畫布就會(huì)向前推進(jìn)一次(重新繪制)。

然后再補(bǔ)充兩個(gè)小點(diǎn):

  • 通常情況下動(dòng)畫的發(fā)生總是伴隨著畫布的重新繪制,但是默認(rèn)情況下 fabric.js 并不會(huì)自動(dòng)幫我們重新繪制,需要我們手動(dòng)調(diào)用(可以看看開篇代碼中 onChange 的回調(diào)是咋寫的),這是因?yàn)槿绻嫴贾杏泻芏辔矬w在運(yùn)動(dòng),默認(rèn)自動(dòng)重新繪制的話會(huì)降低性能。
  • 動(dòng)畫不僅僅可以作用于位置,還可以作用于各種屬性,比如透明度、顏色等,其實(shí)只要是個(gè)數(shù)值就能夠進(jìn)行動(dòng)畫。并且歸功于我們之前將數(shù)據(jù)和視圖分離的架構(gòu),這個(gè)章節(jié)所做的一切也僅僅是改變數(shù)據(jù)而已,并不涉及畫布繪制的內(nèi)容。

然后這里是簡(jiǎn)版 fabric.js 的代碼

以上就是JS前端可視化canvas動(dòng)畫原理及其推導(dǎo)實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于JS前端可視化canvas動(dòng)畫的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論