Canvas 實現畫中畫動畫效果--網易娛樂年度盤點H5動畫解密

viewer_w發表於2018-11-26

前言

由於本人最近在做一些 growth hacking 的工作,業務上以後可能也會涉及去做一些能夠在朋友圈火爆分享的 H5 頁面,突然想到去年看到一個網易娛樂年度新聞盤點的 H5 頁面非常的新穎,採用畫中畫的形式依次串聯十多個手繪娛樂圖片,加上洗腦的“好運來”音樂,讓人有很大的分享的慾望。

手機掃碼體驗網易年度娛樂盤點:

Canvas 實現畫中畫動畫效果--網易娛樂年度盤點H5動畫解密

一步步實現

接下來我們來一步步實現這樣的一個 H5 頁面,首先,我們需要搞懂這個頁面用到了那些前端的知識點。

css3 動畫 animation

首屏有很多動畫,其中大多數是用雪碧圖+animation 的 step 動畫函式實現的,包括底部的鼓,右上角的鑔,中間人物飄動的頭髮。腳下來回滾動的浪花就是普通的 animation 動畫。除了首屏的這些動畫,後面切換到某些場景的時候也會有動畫,這些動畫也是用的雪碧圖動畫。

背景音樂

這個就是使用 audio 元素即可,設定 audio 為迴圈播放,當點選右上角鑔的動圖的時候,呼叫 audio.pause()即可。

場景切換

在首屏,當長按鼓的時候,頁面的 animation 動畫會停止,靜態畫面一點點的縮小,直至出現第一個完整的畫中畫。此時過渡動畫停止,頁面 animation 動畫(白百何一指禪)開始出現。

我們先來分析這一小段,我們程式碼上要做哪些工作。

首先,我們需要兩個圖層,一個 canvas 圖層用來展示場景過渡動畫,z-index 較低;一個展示場景動畫的圖層,我們叫做 gif 圖層,z-index 較高;

drawImage

在 canvas 圖層裡,我們使用 drawImage()這個方法來繪製每一幀的過渡圖片,我們先來看看這個方法的使用方式:

context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

引數值

引數 描述
img 規定要使用的影像、畫布或視訊。
sx 可選。開始剪下的 x 座標位置。
sy 可選。開始剪下的 y 座標位置。
swidth 可選。被剪下影像的寬度。
sHeight 可選。被剪下影像的高度。
x 在畫布上放置影像的 x 座標位置。
y 在畫布上放置影像的 y 座標位置。
width 可選。要使用的影像的寬度。(伸展或縮小影像)
height 可選。要使用的影像的高度。(伸展或縮小影像)。

過渡動畫的每一幀,我們都要在 canvas 上面使用 drawImage 繪製兩張圖片,一張是大圖,一張是畫中畫裡的小圖,以第一個過渡動畫為例,大圖是 P2,小圖是 P1,

如圖:(原諒我不知道使用什麼工具畫圖,只好動手了)

Canvas 實現畫中畫動畫效果--網易娛樂年度盤點H5動畫解密

我們假設大圖 P2 是長方形 ABCD,小圖 P1 是長方形 IJKL,動畫過程中某一時刻的手機螢幕是長方形 EFGH,我們有個前提條件就是這三個長方形都是寬高比為 750:1206 的長方形,而且,所有的圖片寬高畫素大小是相等的(網易的場景圖片大小統一為:1875*3015)這也意味著 iPhone X 等全面屏手機的適配會有問題,在 iPhone678 手機上表現良好。(看看今年網易會不會解決這個問題,畢竟全面屏手機越來越多)。

那麼,在這樣的一個時刻,我們需要在 canvas 上面畫兩張圖片,

drawImage(P2,ME,NE,EF,EH,0,0,750,1206)
drwaImage(P1,0,0,AB,AD,OI,PI,IJ,IL)
複製程式碼

那我們知道了某一時刻的情況,但是如何將畫面動起來,有一個收縮畫面的效果呢?

現在開始寫我們的 render 函式:

const render = () => {
    this.radio = this.radio * this.scale;
    this.timer = requestAnimationFrame(render);
    this.draw();// 繪製兩個圖片
};

draw() {
    if (this.index + 1 != this.imgList.length) {
        if (
            this.radio <
            this.imgList[this.index + 1].areaW / this.imgList[this.index + 1].imgW
        ) {
            if (this.willPause) {
                this.radio =
                    this.imgList[this.index + 1].areaW / this.imgList[this.index + 1].imgW;
                cancelAnimationFrame(this.timer);
            }
            this.index++;
            this.radio = 1;
            if (!this.imgList[this.index + 1]) {
                this.showEnd();
            }
        }
        this.imgNext = this.imgList[this.index + 1];
        this.imgCur = this.imgList[this.index];
        this.containerImage = this.domList[this.index + 1];
        this.innerImage = this.domList[this.index];
        this.drawImgOversize(
            this.containerImage,
            this.imgNext.imgW,
            this.imgNext.imgH,
            this.imgNext.areaW,
            this.imgNext.areaH,
            this.imgNext.areaL,
            this.imgNext.areaT,
            this.radio,
        ),
            this.drawImgMinisize(
                this.innerImage,
                this.imgCur.imgW,
                this.imgCur.imgH,
                this.imgNext.imgW,
                this.imgNext.imgH,
                this.imgNext.areaW,
                this.imgNext.areaH,
                this.imgNext.areaL,
                this.imgNext.areaT,
                this.radio,
            );
    }
}
複製程式碼

render 函式裡面有兩個變數 radio 和 scale,radio = IJ/EF,所以在一個場景切換動畫中,我們只需要改變 radio 的值,使其從 1 逐漸變小到等於 IJ/AB 即可。scale 就是這樣一個用來表示 radio 變化速率的常量。這裡我們可以定義為 0.99,因為 requestAnimationFrame 的回撥在瀏覽器裡面大約一秒會執行 60 次, 而 o.99^240 = 0.08 所以大約 4s 左右,我們就可以完成一個場景切換,這個速度還是比較適中的。

從而,在動畫中的任一時刻,EF 的大小可以表示為 IJ/this.radio,另外,因為所有的圖片都是我們的畫師製作的,所以,每張圖的畫素大小(imgW、imgH)、小圖在大圖中的偏移位置 SI(areaL)、TI(areaT)、小圖的寬高 IJ(areaW)、IL(areaH),都是已知的,根據這些已知的資料,我們可以輕鬆的(對於數學好的同學)將 drawImage 中未知變數用 this.radio 表示。

這樣,我們一個切換動畫算是搞定了,但是我們如何將多個切換動畫串聯起來呢,很簡單,看看 draw()的程式碼,我們只需要在 this.radio 達到臨界值時候,將 index++,重新給 imgNext 和 imgCur 賦值。

最後將 render 函式寫到 touchHandler 裡面即可。

touchHandler(e) {
    e.stopPropagation();
    // e.preventDefault();
    const render = () => {
        this.radio = this.radio * this.scale;
        this.timer = requestAnimationFrame(render);
        this.draw();
    };
    cancelAnimationFrame(this.timer);
    this.willPause = false;
    // clearInterval(this.gif_timer);
    this.timer = requestAnimationFrame(render);
}
複製程式碼

gif 動畫

說是 gif 動畫,但是實現上還是用雪碧圖+step 實現的。如果某一場景中有動畫展示的環節,那麼在過渡動畫結束時,我就可以將 gif 圖層展示出來,gif 圖層有兩部分構成,一個是背景圖片,一個是動畫區域。背景圖片將動畫區域留白,動畫區域採用雪碧圖+step 的方式,實現動畫。這樣做是為了減少圖片資源大小,加快載入速度。

載入圖片

這個 H5 頁面需要載入大量的圖片,而這些圖片一定要保證在使用者互動之前載入完成,所以我們要給頁面初始化時候一個載入態,當所有圖片載入完成後,我們才展示可互動的頁面。所以,我們需要知道什麼時候圖片已經載入好了,上程式碼:

loadGifImg() {
    const loadPromises = this.gifImgs.map(
        item =>
            new Promise((resolve, reject) => {
                const img = new Image();
                img.src = item;
                img.onload = () => resolve(img);
                img.onerror = () => reject();
            }),
    );
    return Promise.all(loadPromises);
}

loadPageImg() {
    const loadPromises = this.imgList.map(
        (item, index) =>
            new Promise((resolve, reject) => {
                const img = new Image();
                img.src = item.link;
                img.i = index;
                img.name = index;
                img.className = 'item';
                item.image = img;
                img.onload = () => {
                    $('.collection').append(item.image);
                    resolve();
                };
                img.onerror = () => reject();
            }),
    );
    return Promise.all(loadPromises);
}
複製程式碼

所以,我們只需要等這兩個 Promise resolve 了就載入完成了。

最後

完整的程式碼 github 歡迎 star

微信掃碼體驗

Canvas 實現畫中畫動畫效果--網易娛樂年度盤點H5動畫解密

如果覺得文章不錯話,不妨點個關注再走啊~

相關文章