iT邦幫忙

2021 iThome 鐵人賽

DAY 27
0
Modern Web

從零開始打造網頁遊戲-造輪子你也辦的到!系列 第 27

Chapter5 - 當一個勤勞的園丁,來修剪我們美麗的樹(II)Canvas素材 修圖、壓縮、效能優化

樹葉問題

先前在第三章畫樹時,就有發現把樹葉畫上去時,系統工作時間會增加而導致掉偵,原圖是300x300,50kb左右,不是很大,但是我們要畫一大堆落葉的話,計算量就會很大,因為影像處理的底層就是去運算300x300個像素點,然後再根據要求的大小,比如50x50,再進行縮放,而context.drawImage方法每次都會經歷這一個步驟,因此重複多次相當花費效能,為此我們有兩種方法做搭配:

  1. 編輯原本的PNG圖檔,再拿去像TinyPng這樣的網站做影像壓縮
  2. 在另一個迷你的Canvas(50x50)中以drawImage方法完成一次縮放後,之後以Canvas作為drawImage的圖片來源,可以參考該方法的說明,包括圖片、影片、SVG也都是可以繪製

處理素材

預估每個葉子最大的大小為100x100像素,我們就依此來裁切原檔,在修圖時有個重點,因為在Canvas在繪製圖形時,都是以左上角為準,如果我們的素材不對齊左上角,就還要額外去做平移和計算,因此為了JS處理具有一致性和方便性,我們把所有葉子的根部都對齊左上:
https://ithelp.ithome.com.tw/upload/images/20211004/20135197uaXQbdUaiB.png

接著就是基本操作:拉輔助線、旋轉縮放、調整版面尺寸

值得注意的是,100x100和300x300的大小可是差了約9倍,相當可觀,即使調整微小,也是會省去不少效能,實際上裁切完的大小是:50kb >>> 12kb,接著再進一步拿去影像壓縮:
https://ithelp.ithome.com.tw/upload/images/20211004/201351978bkGJIkm2o.png
又變得更小,剩下4kb了!不過提醒大家,一定要保留原檔,因為所謂的壓縮,本質上是一種對於檔案的毀損,同時它也失去再修圖的可能,只不過是肉眼看不太出來而已:
https://ithelp.ithome.com.tw/upload/images/20211004/20135197vA78eNH8je.png

繪製樹葉

在樹建立之初在Stick的原型上設置min變數為20,如此以來每一個節點都可以由node.min來取得該變數,而不會被其他無關的物件來取用到:Stick.prototype.min = 20;

if(node.r < node.min){
    // 在樹枝建立之初就有設置過node.img,但為了避免pngImg存有的canvas還未被建立,而重新指向
    // 也可以在更源頭的地方解決,即圖片全部載入完畢,才開始遊戲
    if(node.img == undefined) node.img = pngImg[1 + Math.floor(random(2))];
    else{
        context.strokeStyle = 'rgba(120, 215, 140, 1)';
        context.save();
        context.translate(x, y);
        context.rotate(-Math.PI / 4 - theta2);
        context.drawImage(node.img, 0, 0,
                          node.r * node.grow * 1.5,
                          node.r * node.grow * 1.5);
        context.restore();
    }
}

在canvas的座標中,不指Y是反過來的,包括角度也是反過來(順時針為正),一開始原點為圖片的左上角,表示葉子面向右下角,即45度(Math.PI/4),因此要先減去該角度,再減去theta2(該節點的生長方向)

搭配第二個方法試試

壓縮圖片之後,對效能的確有幫助,但似乎不太夠,畢竟大約有近千片樹葉等著繪製,在console中可觀察到Tree.Draw()花了特別多的時間,其範圍為22.74-31.24秒不等,會造成嚴重的掉偵問題:
https://ithelp.ithome.com.tw/upload/images/20211004/20135197l3Yulxm1u2.png

因此我們先嘗試剛剛說的先將圖片進行壓縮,放到更小的畫布上:

let leafImg = new Array();
let pngImg = new Array();
for(let N = 0; N < 4; N++){
    leafImg[N] = new Image();
    // 等待每一張圖片讀取好
    leafImg[N].onload = () => {
        // 每張圖片創建一個對應的畫布
        pngImg[N] = document.createElement('canvas');
        pngImg[N].width = 30;
        pngImg[N].height = 30;
        let ctx = pngImg[N].getContext("2d");
        // 畫一次就可以了,以後就拿pngImg[N]來當圖片(N為0~3之間)
        ctx.drawImage(leafImg[N], 0, 0, 30, 30);
    }
}
leafImg[0].src = "../images/tinyPng/Leave2O.png";
leafImg[1].src = "../images/tinyPng/Leave2Y.png";
leafImg[2].src = "../images/tinyPng/LeaveRY.png";
leafImg[3].src = "../images/tinyPng/LeaveRR.png";
// 設定完檔案路徑網頁端才會開始讀取,接著觸發剛剛寫的onload

實際嘗試過後,似乎對於效能沒有顯著的幫助,至少還是需要21.64秒,僅快了1秒左右
https://ithelp.ithome.com.tw/upload/images/20211004/20135197kUgRCgq9te.png

不過值得注意的是,不只是樹葉花了很多時間,其實當初在設計並繪製樹枝時,也浪費了很多效能,如果把繪製樹枝的貝茲曲線去掉,只保留節點的計算和樹葉,便會發現時間落在11.03-13.34秒左右:
https://ithelp.ithome.com.tw/upload/images/20211004/20135197X7NvFZbqCQ.png
由此可知,演算約1400個樹枝花了近10秒,演算約1000個樹葉花了近12秒,算是相當接近了,

而只繪製樹葉的效能表現也是好很多,因此之後設計遊戲的時候,會考慮只有在遊戲讀取畫面時,會有讓樹長大動畫,而在玩遊戲時,只有一個靜置的樹在後面(可能是另外一個canvas),不做重複繪製的動作。

當然,我們也可以把RATIO改回1,事情就會單純很多:
(更正:由於Ratio改變,min=20就從原本的10px變成20px,使得樹葉少遞迴1~2次,以下兩個例子實際節點只有450個,約為原本的1/3)

100x100 - 4.92秒(更正:使min=10px,則約為10~11秒)

https://ithelp.ithome.com.tw/upload/images/20211005/20135197aFtDX9RvJF.png

30x30 - 2.60秒(更正:使min=10px,則約為8~9秒)

https://ithelp.ithome.com.tw/upload/images/20211005/201351976ejcT7sGYk.png

相較之下,就沒什麼效能問題,不過就沒有原本想要的高解析度了,畢竟少了Ratio增加畫布大小,就只是單純拿30x30的圖片去放大

成果

再想想看怎麼辦吧!以下是用RATIO=2繪製出來的,是真的比上面兩張圖精緻好看很多。

https://ithelp.ithome.com.tw/upload/images/20211004/20135197oQzu84QgMA.png
https://ithelp.ithome.com.tw/upload/images/20211004/20135197Ret4Pfwslz.png

今天都就先用現有素材來做,不特別去找紅花綠葉了xd,
不知道大家覺得哪個比較好看呢?


上一篇
Chapter5 - 當一個勤勞的園丁,來修剪我們美麗的樹(I)Canvas繪圖 Y型樹枝(愛心型) + 控制分支的變化
下一篇
Chapter5 - 當一個勤勞的園丁,來修剪我們美麗的樹(III)Canvas動畫 讓樹隨著讀取畫面長大
系列文
從零開始打造網頁遊戲-造輪子你也辦的到!31

尚未有邦友留言

立即登入留言