前言
由於本人最近在做一些 growth hacking 的工作,業務上以後可能也會涉及去做一些能夠在朋友圈火爆分享的 H5 頁面,突然想到去年看到一個網易娛樂年度新聞盤點的 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,
如圖:(原諒我不知道使用什麼工具畫圖,只好動手了)
我們假設大圖 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
微信掃碼體驗