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,有這樣的場景,遊戲開發中,前景内容需要變化較快如每秒60幀,而背景可能速度較慢如每秒10幀,這樣便可利用人眼的一些視覺特性達到一定程度的立體感(遠遠看山水的效果),這樣會更吸引使用者的眼球。
離屏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(),沒有做個實驗對照。
請謹慎使用這一技巧,因為它很大程度上依賴于底層的canvas實作,是以很容易發生變化。
context.fillRect()//顔色填充
context.clearRect(0, 0, w, h)
canvas.width = canvas.width; // 一種畫布專用的技巧
避免使用陰影
減少使用 shadowBlur 效果,陰影渲染的性能開銷通常比較高
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = \'rgba(255, 0, 0, 0.5)\';
context.fillRect(20, 20, 150, 100);
坐标值盡量使用整數
避免使用浮點數坐标,使用非整數的坐标繪制内容,系統會自動使用抗鋸齒功能,嘗試對線條進行平滑處理,這又是一種性能消耗。
可以調用 Math.round 四舍五入取整,或者floor向上ceil向下取整,trunc直接丢棄小數位。對應的
當然性能最優越的方法莫過于将數值加0.5然後對所得結果進行移位運算以消除小數部分。
1 rounded = (0.5 + somenum) | 0;
2 rounded = ~~ (0.5 + somenum);
3 rounded = (0.5 + somenum) << 0;
避免大量計算造成阻塞
所謂「阻塞」,可以了解為不間斷運作時間超過 16ms 的 JavaScript 代碼,導緻頁面卡頓,丢幀,或者失去響應,這種問題能很快被使用者察覺到,造成很差的互動體驗。
是以我們要把與渲染無關的大量計算交給worker。大量計算可能造成渲染不流暢,但絕對不能讓使用者操作卡頓失去響應。
像下圖的效果,需要計算大量函數曲線上的點來繪制成曲線,我們移動的時候可以看到計算新點坐标值的過程是有延遲的,但是并不會讓使用者滑鼠拖拽卡頓失效,渲染的過程再跟随滑鼠移動。
總結
以上便是總結到的提升繪制效率的幾點建議!具體采用哪種需要在實際項目裡面根據情況來定,如果你知道這幾種方式至少不會大腦空白了!
還有幾點開發過程需要注意的:
- 盡可能使用計算代替canvas渲染
- 減少改變 context 的狀态,如果要改變請指派正确的類型,減少浏覽器的嘗試