亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

JS前端可視化canvas動畫原理及其推導實現(xiàn)

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

前言

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

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

動畫的本質(zhì)

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

rect.animate(
    { top: 50, left: 400, angle: 45 }, // 要動畫的屬性
    { duration: 1000, onChange: canvas.renderAll.bind(canvas) } // 動畫執(zhí)行時間和手動渲染
);

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

沒錯,大體就是這個原因,但我們可以更具體一點,想想畫布為什么要重新繪制呢?不就是因為畫布中某個物體的某個值改變了,所以我們才要更新一下畫面,以此來表示它動了。這個物體狀態(tài)值的改變才是動畫的根本原因??。

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

動畫的實現(xiàn)

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

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

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

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

相信上面的注釋應該解釋的清清楚楚、明明白白。不過還是要著重講下其中的兩個點:

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

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

// 假設要從左到右運動
let left = 100;
function tick() {
    left++; // 更新值
    window.requestAnimationFrame(tick);
}
tick();

因為每個屏幕刷新頻率不一樣,如果像上面這樣寫,在有的電腦上就會快一些,有的電腦上就會慢一些,不僅如此在頁面切換到后臺的時候幀率也會降低,就會導致各種問題,這顯然不是我們期望的。

所以要怎么做呢?

我們應該是以時間為維度來播放動畫,因為時間對我們來說流逝的速度是一樣的,所以在動畫一開始的時候需要記錄下開始時間 start,之后動畫播放到哪里都會以這個開始時間為基準,回頭看看剛才代碼中計算當前動畫執(zhí)行了多長時間的方式:

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

就是以 start 為基準的,這點很重要。

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

動畫的推導

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

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

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

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

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

可能有同學還是不懂這個緩動函數(shù),其實是因為被上面的公式唬住了,公式都是推導之后的簡便寫法,直接去看式子是很難理解的,單憑公式在腦海中想象出動畫效果也不太現(xiàn)實,所以這里給大家簡單推導一下這種式子怎么來的,以最簡單的勻速運動為例子,看看下面這張圖????:

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

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

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

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

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

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

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

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

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

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

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

小結(jié)

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

然后再補充兩個小點:

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

然后這里是簡版 fabric.js 的代碼

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

相關(guān)文章

  • Three.js?Interpolant實現(xiàn)動畫插值

    Three.js?Interpolant實現(xiàn)動畫插值

    這篇文章主要為大家介紹了Three.js?Interpolant實現(xiàn)動畫插值示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02
  • webpack安裝配置及使用教程詳解

    webpack安裝配置及使用教程詳解

    這篇文章主要為大家介紹了webpack的安裝配置及使用的教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06
  • Express框架兩個內(nèi)置中間件方法詳解

    Express框架兩個內(nèi)置中間件方法詳解

    這篇文章主要為大家介紹了Express框架兩個內(nèi)置中間件方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • 微信小程序 location API接口詳解及實例代碼

    微信小程序 location API接口詳解及實例代碼

    這篇文章主要介紹了微信小程序 location API接口相關(guān)資料,這里詳細介紹了location API接口并附簡單實例代碼,需要的朋友可以參考下
    2016-10-10
  • 前端取消請求及取消重復請求方式

    前端取消請求及取消重復請求方式

    這篇文章主要為大家介紹了前端取消請求及取消重復請求方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • uniapp自定義相機實現(xiàn)示例詳解

    uniapp自定義相機實現(xiàn)示例詳解

    這篇文章主要為大家介紹了uniapp自定義相機實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • 插件導致ECharts被全量引入的坑示例解析

    插件導致ECharts被全量引入的坑示例解析

    這篇文章主要為大家介紹了插件導致ECharts被全量引入的坑示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-09-09
  • 微信小程序 動畫的簡單實例

    微信小程序 動畫的簡單實例

    這篇文章主要介紹了微信小程序 動畫的簡單實例的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下
    2017-10-10
  • electron渲染進程主進程相互傳值示例解析

    electron渲染進程主進程相互傳值示例解析

    這篇文章主要為大家介紹了electron渲染進程主進程相互傳值示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02
  • 10分鐘內(nèi)講解Npm腳本使用教程

    10分鐘內(nèi)講解Npm腳本使用教程

    這篇文章主要為大家介紹了10分鐘內(nèi)講解Npm腳本使用教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10

最新評論