canvas的主要功能就是用來繪製內容,有時候為了給使用者流暢的視覺感受,需要繪製的頻率要求很高,這樣對繪製的效能就有要求,那麼怎麼才能寫出高效能的繪製程式碼呢。
儘可能少呼叫api
例如我們繪製一段線條,如果用如下程式碼的話,每移動一次就stroke一次:
1 for (var i = 0; i < points.length - 1; i++) { 2 var p1 = points[i]; 3 var p2 = points[i + 1]; 4 context.beginPath(); 5 context.moveTo(p1.x, p1.y); 6 context.lineTo(p2.x, p2.y); 7 context.stroke(); 8 }
優化後程式碼如下,這樣beginPah和stroke就少呼叫了n次。
1 context.beginPath(); 2 for (var i = 0; i < points.length - 1; i++) { 3 var p1 = points[i]; 4 var p2 = points[i + 1]; 5 context.moveTo(p1.x, p1.y); 6 context.lineTo(p2.x, p2.y); 7 } 8 context.stroke();
儘量少改變CANVAS狀態機
我們可以改變 context 的若干狀態,而幾乎所有的渲染操作,最終的效果與 context 本身的狀態有關係。例如當對context.lineWidth賦值的話,開銷遠遠大於對一個普通物件賦值的開銷。
Canvas 上下文不是一個普通的物件,當呼叫了 context.lineWidth = 5 時,瀏覽器會需要立刻地做渲染上下文環境的工作,這樣你下次呼叫諸如 stroke 或 strokeRect 等 API 時,畫出來的線就正好是 5 個畫素寬了。其實這也是瀏覽器自身的一種優化,否則如果等到stroke呼叫時再臨時準備渲染環境,會更加影響正常繪製情況下的效能。
下面對比優化前後的程式碼:
for (var i = 0; i < STRIPES; i++) { context.fillStyle = (i % 2 ? COLOR1 : COLOR2); context.fillRect(i * GAP, 0, GAP, 480); }
context.fillStyle = COLOR1; for (var i = 0; i < STRIPES / 2; i++) { context.fillRect((i * 2) * GAP, 0, GAP, 480); } context.fillStyle = COLOR2; for (var i = 0; i < STRIPES / 2; i++) { context.fillRect((i * 2 + 1) * GAP, 0, GAP, 480); }
上面兩段程式碼,對fillStyle的呼叫時機做了改變,提高了效能。
分層canvas
繪製場景複雜的情況下,一般採用多個canvas,可依據繪製內容的頻率高低來劃分。
如遊戲中的背景繪製頻率低可以放在一層canvas上,上面的小人等繪製頻率高放在一層canvas上,兩層canvas的疊加效果達到完整效果。
如下圖中繪製過程中的圓形在一層canvas上,不斷清除不斷繪製,而下面的已經繪製出來的筆跡內容放在另外一層canvas上,不需要清除重繪。
離屏canvas
也叫作預渲染,在離屏canvas上繪製好一整塊圖形,繪製好後在放到檢視canvas中,適合每一幀畫圖運算複雜的圖形。
比如我們有時候為了儘可能少的請求網路資源,會用到精靈圖,這樣在繪製精靈圖某一塊內容時,需要利用繪圖api的裁剪。
實際發現,使用 drawImage 繪製一張大尺寸圖片到較小畫布區域上,比起繪製一張和繪製區域尺寸一樣大的圖片的情形,開銷要大一些。可以認為,兩者相差的開銷正是「裁剪」這一個操作的開銷。下面三種繪製方式,效能開銷依次增加。
// 將image放到目標canvas指定位置,大小按照原圖大小渲染 void ctx.drawImage(image, dx, dy); // 將image放到目標canvas指定位置,指定寬高渲染 void ctx.drawImage(image, dx, dy, dWidth, dHeight); // 將image裁剪之後放到目標canvas指定位置,指定寬高渲染 void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
而離屏渲染就可以讓我們先把圖片裁剪成想要的尺寸內容儲存起來,等到真正繪製的時候就可以使用第一種寫法簡單的把圖片繪製出來。
// 在離屏 canvas 上繪製 var offscreencanvas = document.createElement('canvas'); // 寬高賦值為想要的圖片尺寸 offscreencanvas.width = dWidth; offscreencanvas.height = dHeight; // 裁剪 offscreencanvas.getContext('2d').drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); // 在檢視canvas中繪製 viewcontext.drawImage(canvas, x, y);
有時候,遊戲物件是多次呼叫 drawImage 繪製而成,或者根本不是圖片,而是使用路徑繪製出的向量形狀,那麼離屏繪製還能幫你把這些操作簡化為一次 drawImage 呼叫。
組合圖形組合了多個圖形將它們繪製存放到離屏canvas中,下次未變化的時候直接繪製一次離屏canvas。
裁剪
Canvas (大小一般小於等於螢幕寬高)只是整個大場景下的一個「可視視窗」,如果我們在每一幀中,都把全部內容畫出來,勢必就會有很多東西畫到 Canvas 外面去了,同樣呼叫了繪製 API,但是並沒有任何效果。
那麼視口外的內容是不需要繪製的,但如果繪製對效能影響有多少呢?進行這樣一個實驗,繪製一張 320x180 的圖片 104 次,當每次都繪製在 Canvas 內部時,消耗了 40ms,而每次都繪製在 Canvas 外時,僅消耗了 8ms。雖然繪製在canvas外時,消耗的時間較短。
但考慮到計算的開銷與繪製的開銷相差 2~3 個數量級,所以一般情況下通過計算來過濾掉哪些畫布外的物件,仍然是很有必要的。
區域性重繪
由於 Canvas 的繪製方式是畫筆式的,在 Canvas 上繪圖時每呼叫一次 API 就會在畫布上進行繪製,一旦繪製就成為畫布的一部分。繪製圖形時並沒有物件儲存下來,一旦圖形需要更新,需要清除整個畫布重新繪製。
如下圖僅對紅邊框的平行四邊形做改變,如果每次重繪整個畫布內容就不太合適
Canvas 區域性重新整理的方案:
- 清除指定區域的顏色,並設定 clip
- 所有同這個區域相交的圖形重新繪製
要實現區域性渲染時,需要考慮的兩個因素是:
- 單次重新整理時影響的範圍最小
- 重新整理的圖形不會影響其他圖形的正確繪製
清除畫布內容
有地方說這三種方法效能依次提高,我目前只是使用了clearRect(),沒有做個實驗對照。
context.fillRect()//顏色填充 context.clearRect(0, 0, w, h) canvas.width = canvas.width; // 一種畫布專用的技巧
座標值儘量使用整數
避免使用浮點數座標,使用非整數的座標繪製內容,系統會自動使用抗鋸齒功能,嘗試對線條進行平滑處理,這又是一種效能消耗。
可以呼叫 Math.round 四捨五入取整。
避免大量計算造成阻塞
所謂「阻塞」,可以理解為不間斷執行時間超過 16ms 的 JavaScript 程式碼,導致頁面卡頓,丟幀,或者失去響應,這種問題能很快被使用者察覺到,造成很差的互動體驗。
所以我們要把與渲染無關的大量計算交給worker。大量計算可能造成渲染不流暢,但絕對不能讓使用者操作卡頓失去響應。
像下圖的效果,需要計算大量函式曲線上的點來繪製成曲線,我們移動的時候可以看到計算新點座標值的過程是有延遲的,但是並不會讓使用者滑鼠拖拽卡頓失效,渲染的過程再跟隨滑鼠移動。
總結
以上便是總結到的提升繪製效率的幾點建議!具體採用哪種需要在實際專案裡面根據情況來定,如果你知道這幾種方式至少不會大腦空白了!
還有幾點開發過程需要注意的:
- 儘可能使用計算代替canvas渲染
- 減少改變 context 的狀態,如果要改變請賦值正確的型別,減少瀏覽器的嘗試
- 減少使用 shadowBlur 效果,陰影渲染的效能開銷通常比較高