來寫個無限手套的分解特效吧
利用 Canvas 結合 CSS 實現分解特效
《復仇者聯盟 4:終局之戰》(Avangers: endgame)現在正於全球火熱上映中,Google 前陣子也提供了一個有趣的彩蛋功能:只要透過 Google 搜尋『薩諾斯』或『Thanos』,就會在搜尋結果旁邊看到一個『無限手套』的圖示,點下去後網頁就會隨機把一些搜尋結果分解為塵埃。
正好六角學院的 JS地下城 最新的關卡也出了一個類似的題目叫『死亡筆記本』:題目的要求是寫一個類似相簿的網頁,在下方會有一個輸入相片名子的欄位,輸入完後按下 Enter 就會把對應的相片分解掉。
於是我花了幾天研究了一下怎麼做出這種分解特效,以下紀錄了我在挑戰這關時的技術心得,其實大部分的想法都是參考國外高手的文章,我會把連結放在最後面,有興趣的朋友也建議點進去看看。
原理
我們先考慮一個簡單的例子: 假如希望一張圖片由上而下逐漸消失,該怎麼達成呢?
做法可能不只一種,但大概是這樣做:先想辦法取得圖片最上層區塊的像素,讓它們逐漸 fade out;接者再取得次上層區塊的像素,一樣讓它們逐漸 fade out … 依此類推,這樣一層層的移除像素後,就可以達成圖片由上而下逐漸消失的動畫效果。
我們可以在動畫開始前先決定好每一層區塊的像素是哪些。例如,假設只有 3 層像素,我們可以把這三層像素分到三張一樣大小的圖片,疊加在一起就會是一張完整的圖:
不過為了產生顆粒效果,我們不能只像上面簡單切成三塊,必須用隨機取樣的方式來決定每一層的像素。
這邊可以利用加權取亂數的方式來決定每個像素在哪一層區塊。例如,圖片最上方的像素最靠近第一層,設定加權值為 10; 距離第二層的相素有點遠,設定加權值為 6; 距離第三層的距離最遠,設定加權值為 2。因此最上方的像素有 10/18 的機率屬於第一層; 6/18 的機率屬於第二層; 2/18的機率屬於第三層。
利用這個概念,就可以把每層區塊取樣的像素做得更有顆粒感:
接下來只要增加區塊的數量,然後在不同時間點結合 CSS 的 fade out 及 transform 等特效依序移除代表各區塊的圖片,就可以做出不錯的動畫特效。
以上我們考慮的例子是由上而下逐漸分解,那如果要做的動畫是由左至右分解呢? 那每個區塊取樣的像素就會是橫向分佈的。同理,如果是由外向內分解的方式,那取樣出的像素就會是類似由外向內的圓形分佈。
程式碼
在網頁中如果要處理圖片的像素,可以靠 HTML5 的 Canvas 來達成。整個程式的流程會是這樣:
Step 1: 將 DOM 元素繪製到 Canvas 上。
Step 2: 將 Canvas 的像素取樣後繪製到其他 Canvas 上。
Step 3: 將所有取樣後的 Canvas 疊在一起。
Step 4: 在不同的時間點,利用 CSS 特效依序移除疊在一起的 Canvas。
這裡比較困難的步驟到大概就是 Step 1 & Step 2,尤其是 Step 2 需要設計適當的取樣函數才會有比較理想的效果。以下就重點介紹一下這些地方。
🔹 如何把 DOM 元素繪製到 Canvas 上?
最簡單的做法是利用這個函式庫: html2canvas,它可以把指定元素內的DOM 結構直接轉換成對應的 canvas,非常實用。
如果需要轉換的元素只是圖片,也可以直接用 Canvas 的 drawImage
API,例如:
如果繪製到 Canvas 上的圖片要有類似 CSS 的 background-size: cover;
的效果,可以參考這篇文章。
🔷 如何把 Canvas 的部分像素繪製到其他 Canvas 上?
只要碰到 Canvas 像素等級的操作,都可以藉由 ImageData
來解決,因為 ImageData
可以存取 Canvas 的像素資料。
大致上的流程會是這樣:先擷取原始 Canvas 的 ImageData,然後對其中的像素資料做取樣或修改,最後再把修改的像素資料設定到目標 Canvas 的 ImageData上。
這邊附上一個簡單的例子:把一個繪製在 Canvas 上的 Twitter Icon 的像素複製到另一個 Canvas 上,並且在複製過程中調高了所有像素的紅色值。
🔷 如何設計取樣函式?
在介紹原理的小節,有提到說要利用加權取亂數的方式來決定每個像素要位於哪個 Canvas 區塊。
這邊我使用了一個叫 chance.js 的函式庫,它提供了一個方便計算加權亂數的方法: chance.weighted
。它的使用方法很簡單,例如,如果執行 chance.weighted(['a', 'b], [100, 1])
其回傳值是 'a'
的機率比是 'b'
的機率高了 100 倍。詳細的說明可以參考官方文件。
那要怎麼決定取樣值呢?這其實沒有標準答案,會根據想做出的動畫不同而不同。
假如考慮由上而下分解的動畫,那我們會希望每個區塊取樣到的像素會是由上而下的分布。換句話說,可以根據每個像素垂直方向的座標與各區塊的垂直距離來決定加權值:
這個函數中的參數 h
代表像素在圖片中的 y座標, height
代表整張圖片的高度, chunkNum
代表區塊的數量。
在程式中我定義 index 由 0 到 chunkNum -1 的區塊就是動畫中 fade out 的順序,因此 index 越小的區塊就要收集到更靠近上方的像素,也就是 h 值越小的像素。
所以可以根據 h 與區塊 index 的差值來決定加權值,但是 h 跟 index 的區間範圍不一樣,因此我將 h 轉換成 dataIndex
才能正確計算與區塊 index 的相對距離。
至於這邊多用了一個 Math.pow(value, 6)
是希望加權的分佈更明顯一點,但不一定要用這個算法,這只是用 try and error 的方式試出來的,剛好這樣計算出來的結果比較符合我的期望。
結語
最後,結合上述的內容我寫了一個把蘋果分解的範例。如果還有任何問題或其他更好的做法都歡迎留言討論喔 🤘