【高級系列】Canvas繪制性能專題

總原則:

????????在移動APP中,因為手機硬件性能有限,其實不宜做太多特效,應該往簡潔突出重點的方向考慮。


1 性能建議

英文引文地址:

http://www.html5rocks.com/en/tutorials/canvas/performance/

提高HTML5 canvas性能的幾種方法!

http://blog.csdn.net/zyz511919766/article/details/7401792


1.1 預渲染

1.PRE-RENDER TO AN OFF-SCREEN CANVAS

????????預渲染即在一個或者多個臨時的不會在屏幕上顯示的canvas中渲染臨時的圖像,然后再把這些不可見的canvas作為圖像渲染到可見的canvas中。對于計算機圖形學比較熟悉的朋友應該都知道,這個技術也被稱做display list

????????沒有預渲染的情況:

//?canvas,?context?are?defined

function?render()?{

????drawMario(context);

????requestAnimationFrame(render);

}

????????預渲染的情況:

var?m_canvas?=?document.createElement('canvas');

m_canvas.width?=?64;

m_canvas.height?=?64;

var?m_context?=?m_canvas.getContext(‘2d’);

drawMario(m_context);

function?render()?{

????context.drawImage(m_canvas,?0,?0);

????requestAnimationFrame(render);

}??

????????關于requestAnimationFrame的使用方法將在后續部分做詳細的講述。當渲染操作(例如上例中的drawmario)開銷很大時該方法將非常有效。其中很耗資源的文本渲染操作就是一個很好的例子。

????????要確保臨時的canvas恰好適應你準備渲染的圖片的大小,否則過大的canvas會導致我們獲取的性能提升被將一個較大的畫布復制到另外一個畫布的操作帶來的性能損失所抵消掉。

????????上述的測試用例中緊湊的canvas相當的小:

can2.width?=?100;

can2.height?=?40;

????????如下寬松的canvas將導致糟糕的性能:

can3.width?=?300;??


1.2 多條指令一次載入

2.BATCH CANVAS CALLS TOGETHER

????????因為繪圖是一個代價昂貴的操作,因此,用一個長的指令集載入將繪圖狀態機載入,然后再一股腦的全部寫入到video緩沖區。這樣會會更佳有效率。

????????例如,當需要畫對條線條時先創建一條包含所有線條的路經然后用一個draw調用將比分別單獨的畫每一條線條要高效的多:

for?(var?i?=?0;?i?<?points.length?-?1;?i++)?{

??var?p1?=?points[i];

??var?p2?=?points[i+1];

? context.beginPath();

? context.moveTo(p1.x,?p1.y);

? context.lineTo(p2.x,?p2.y);

? context.stroke();

}

????????通過繪制一個包含多條線條的路徑我們可以獲得更好的性能:

context.beginPath();

for?(var?i?=?0;?i?<?points.length?-?1;?i++)?{

??var?p1?=?points[i];

??var?p2?=?points[i+1];

? context.moveTo(p1.x,?p1.y);

? context.lineTo(p2.x,?p2.y);

}

context.stroke();

????????這個方法也適用于HTML5 canvas。比如,當我們畫一條復雜的路徑時,將所有的點放到路徑中會比分別單獨的繪制各個部分要高效的多(jsperf):

????????然而,需要注意的是,對于canvas來說存在一個重要的例外情況:若欲繪制的對象的部件中含有小的邊界框(例如,垂直的線條或者水平的線條),那么單獨的渲染這些線條或許會更加有效(jsperf


1.3 避免不必要的狀態切換

3.AVOID UNNECESSARY CANVAS STATE CHANGES

????????HTML5 canvas元素是在一個狀態機之上實現的。狀態機可以跟蹤諸如fill、stroke-style以及組成當前路徑的previous points等等。在試圖優化繪圖性能時,我們往往將注意力只放在圖形渲染上。實際上,操縱狀態機也會導致性能上的開銷。

????????例如,如果你使用多種填充色來渲染一個場景,按照不同的顏色分別渲染要比通過canvas上的布局來進行渲染要更加節省資源。為了渲染一副條紋的圖案,你可以這樣渲染:用一種顏色渲染一條線條,然后改變顏色,渲染下一條線條,如此反復:

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);

}??

1.4 只重繪變化部分而不是全部重繪

4.RENDER SCREEN DIFFERENCES ONLY, NOT THE?WHOLE ?NEW STATE

????????在屏幕上繪制較少的東西要比繪制大量的東西節省資源。重繪時如果只有少量的差異你可以通過僅僅重繪差異部分來獲得顯著的性能提升。換句話說,不要在重繪前清除整個畫布。:

context.fillRect(0,?0,?canvas.width,?canvas.height);

????????跟蹤已繪制部分的邊界框,僅僅清理這個邊界之內的東西:

context.fillRect(last.x,?last.y,?last.width,?last.height);??

????????如果您對計算機圖形學比較熟悉,你或許應該知道這項技術也叫做“redraw technique”,這項技術會保存前一個渲染操作的邊界框,下一次繪制前僅僅清理這一部分的內容。這項技術也適用于基于像素的渲染環境。這篇名為JavaScript NIntendo emulator?tallk的文章說明了這一點。

1.5 使用多圖層繪制復雜場景

5.USE MUTIPLE LAYERED?CANVASES FOR COMPLEX SCENES

????????我們前邊提到過,繪制一副較大的圖片代價是很高昂的因此我們應盡可能的避免。除了前邊講到的利用另外得不可見的canvas進行預渲染外,我們也可以疊在一起的多層canvas。利用前景的透明度,我們可以在渲染時依靠GPU整合不同的alpha值。你可以像如下這么設置,兩個絕對定位的canvas一個在另一個的上邊:

????????相對于僅僅有一個canvas的情況來講,這個方法的優勢在于,當我們需要繪制或者清理前景canvas時,我們不需要每次都修改背景canvas。如果你的游戲或者多媒體應用可以分成前景和背景這樣的情況,那么請考慮分別渲染前景和背景來獲取顯著的性能提升。

????????你可以用相較慢的速度(相對于前景)來渲染背景,這樣便可利用人眼的一些視覺特性達到一定程度的立體感,這樣會更吸引用戶的眼球。比如,你可以在每一幀中渲染前景而僅僅每N幀才渲染背景。

????????注意,這個方法也可以推廣到包含更多canvas曾的復合canvas。如果你的應用利用更多的曾會運行的更好時請利用這種方法。

1.6 減少使用陰影效果

6.AVOID SHADOWBLUR

????????跟其他很多繪圖環境一樣,HTML5 canvas允許開發者對繪圖基元使用陰影效果,然而,這項操作是相當耗費資源的。

context.shadowOffsetX?=?5;

context.shadowOffsetY?=?5;

context.shadowBlur?=?4;

context.shadowColor?=?'rgba(255,?0,?0,?0.5)';

context.fillRect(20,?20,?150,?100);??


1.7 熟悉多種重繪方法

7.KNOW VARIOUS WAYS TO?CLEAR THE CANVAS

????????因為HTML5 canvas 是一種即時模式immediate mode)的繪圖范式(drawing paradigm),因此場景在每一幀都必需重繪。正因為此,清楚canvas的操作對于 HTML5 應用或者游戲來說有著根本的重要性。

????????正如在 避免 canvas 狀態變化的一節中提到的,清楚整個canvas的操作往往是不可取的。如果你必須這樣做的話有兩種方法可供選擇:調用

context.clearRect(0,?0,?width,?height)

????????或者使用 canvas特定的一個技巧

canvas.width?=?canvas.width

????????在書寫本文的時候,cleaRect方法普遍優越于重置canvas寬度的方法。但是,在某些情況下,在Chrome14中使用重置canvas寬度的技巧要比clearRect方法快很多(jsperf):

????????請謹慎使用這一技巧,因為它很大程度上依賴于底層的canvas實現,因此很容易發生變化,欲了解更多信息請參見 Simon Sarris 的關于清除畫布的文章

1.8 減少浮點坐標繪制

8. AVOID FLOATING POINT?COORDINATES

????????HTML5 canvas 支持子像素渲染(sub-pixel rendering),而且沒有辦法關閉這一功能。如果你繪制非整數坐標他會自動使用抗鋸齒失真以使邊緣平滑。以下是相應的視覺效果(參見Seb Lee-Delisle的關于子像素畫布性能的文章

? ? ? ? 如果平滑的精靈并非您期望的效果,那么使用 Math.floor方法或者Math.round方法將你的浮點坐標轉換成整數坐標將大大提高運行速度(jsperf):

????????為使浮點坐標抓換為整數坐標你可以使用許多聰明的技巧,其中性能最優越的方法莫過于將數值加0.5然后對所得結果進行移位運算以消除小數部分。

//?With?a?bitwise?or.

rounded?=?(0.5?+?somenum)?|?0;

//?A?double?bitwise?not.

rounded?=?~~?(0.5?+?somenum);

//?Finally,?a?left?bitwise?shift.

rounded?=?(0.5?+?somenum)?<<?0;

????????兩種方法性能對比如下(jsperf):

1.9 盡量使用requeatAnimationFrame方法執行動畫

9.OPTIMIZE YOUR?ANIMATIONS WITH ‘REQUESTANIMATIONFRAME’

????????相對較新的 requeatAnimationFrame API是在瀏覽器中實現交互式應用的推薦標準。與傳統的以固定頻率命令瀏覽器進行渲染不同,該方法可以更友善的對待瀏覽器,它會在瀏覽器可用的時候使其來渲染。這樣帶來的另外一個好處是當頁面不可見的時候,它會很聰明的停止渲染。

????????requestAnimationFrame調用的目標是以60幀每秒的速度來調用,但是他并不能保證做到。所以你要跟蹤從上一次調用導線在共花了多長時間。這看起來可能如下所示:

var?x?=?100;

var?y?=?100;

var?lastRender?=?new?Date();

function?render()?{

? ??var?delta?=?newDate()?-?lastRender;

????x?+=?delta;

????y?+=?delta;

????context.fillRect(x,?y,?W,?H);

????requestAnimationFrame(render);

}

render();?

????????注意requestAnimationFrame不僅僅適用于canvas 還適用于諸如WebGL的渲染技術。

????????在書寫本文時,這個API僅僅適用于Chrome,Safari以及Firefox,所以你應該使用這一代碼片段


1.10 職責分離

????????與渲染無關的計算交給worker,復雜的計算交給引擎(自己寫,或者用開源的),比如3D、物理 。緩存load好的圖片,canvas上畫canvas,而不是畫image。


2 圖層優化

2.1 多層半透明優化處理

2.1.1 范例1——模擬波浪性能優化

2.1.1.1 繪制機制

????????在最近這個項目中,有一個模擬波浪的特效,繪制原理是用多層半透明Canvas進行疊加:

????1、全局用三層半透明Canvas疊加,各層透明度分別為0.5、0.6、0.8;

????2、每層Canvas中利用濾鏡功能截取上邊沿圖形,而截取的高度則是利用正弦函數結合振幅值與頻率進行繪制,再通過一定刷新頻率將繪圖刷新以及平移,以形成動態效果;

????3、各層的振幅與頻率不同,但刷新頻率一致,故各層疊加在一起后即形成三道波浪圖形;

????結語:

????????這樣做出來的效果比較逼真,但是性能損壞很大,在iPhone4s上,因為屏幕渲染開銷太大,已經導致界面響應事件失效了。因為屏幕繪制時,每個像素點上的顏色計算,需要集合三層Canvas的透明度來計算,非常損耗CPU性能。

2.1.1.2 代碼

2.1.1.2.1 HTML代碼:

2.1.1.2.2 CSS代碼

.control_panel_wrap .filter_info_wrap .controlPanel_animation_wrap .waves_wrap #waves1

{

??? position: absolute;

??? top: 100px;

???filter: alpha(Opacity=80);

??? opacity:.8

}


.control_panel_wrap .filter_info_wrap .controlPanel_animation_wrap .waves_wrap #waves2

{

??? position: absolute;

??? top:100px;

???filter: alpha(Opacity=70);

??? opacity:.7

}


.control_panel_wrap .filter_info_wrap .controlPanel_animation_wrap .waves_wrap #waves3

{

??? position: absolute;

??? top:100px;

???filter: alpha(Opacity=50);

??? opacity:.5

}

2.1.1.2.3 JS代碼:

function createWave(canvasId, amplitude, frequency) {

??? if (typeof(Humble)== 'undefined') window.Humble = {};

??? Humble.Trig = {};

??? Humble.Trig.init =init;


??? var canvas, context, height, width, xAxis, yAxis, draw;


??? //設置振幅和頻率

??? var amplitude = amplitude || 10;

??? var frequency = frequency || 0.2;


??? /**

???? * Init function.

???? *

???? * Initialize variables and begin the animation.

???? */

??? function init() {

????? canvas = document.getElementById(canvasId);

????? canvas.width = 320;

????? canvas.height = 120;//320

????? context = canvas.getContext("2d");

????? height = canvas.height;

????? width = canvas.width;

????? xAxis =Math.floor(height/2);

????? yAxis = 0;

????? context.save();

????? draw();

??? }


??? /**

???? * Draw animation function.

???? *

???? * This function draws one frame of the animation, waits 20ms, and then calls

???? * itself again.

???? */

??? draw = function (){

????? // Clear the canvas

?????context.clearRect(0, 0, width, height);


????? // Set styles for animated graphics

????? var waveGradient= context.createLinearGradient(0, 0, 0, canvas.height);

????? waveGradient.addColorStop(0, 'rgba(255,255,255,1)');

? ? ? waveGradient.addColorStop(1, 'rgba(255,255,255,0)');

????? context.save();

? ? ? context.strokeStyle = waveGradient;

? ? ? context.fillStyle = waveGradient;

? ? ? context.lineWidth = 1;


????? // Draw the sine curve at time draw.t, as well as the circle.

? ? ? context.beginPath();

? ? ? drawSine(draw.t);

? ? ? context.stroke();


? ? ? ?// Restore original styles

? ? ? context.restore();


????? // Update the time and draw again

????? draw.seconds = draw.seconds - .007;

????? draw.t = draw.seconds*Math.PI;

????? setTimeout(draw,35);

??? };

??? draw.seconds = 0;

??? draw.t = 0;


??? /**

???? * Function to draw sine

???? *

???? * The sine curve is drawn in 10px segments starting at the origin.

???? */

??? function drawSine(t) {

????? // Set the initial x and y, starting at 0,0 and translating to the origin on

????? // the canvas.

????? var x = t;

????? var y = Math.sin(x);

?????context.moveTo(yAxis, amplitude*y+xAxis);


????? // Loop to draw segments

????? for (i = yAxis; i <= width; i += 10) {

? ? ? ? ?x =t+(-yAxis+i)/amplitude*frequency;

? ? ? ? ?y =Math.sin(x);

? ? ? ? context.lineTo(i, amplitude*y+xAxis);

????? }

? ? ? context.lineTo(canvas.width, canvas.height);

? ? ? context.lineTo(0, canvas.height);

//?????context.stroke();

? ? ? ?context.fill();

??? }


??? Humble.Trig.init()

? }


?? //測試到底是哪個效果好計算資源

? //生成三條波浪,比較耗資源,至少有50%的幾率造成事件阻塞

? createWave('waves1');

? createWave('waves2',15, 1/11);

? createWave('waves3',20, 1/4);

2.1.1.3 優化措施1——減少不變部分范圍:

? ? ? ? 特效其實只是要求上邊沿有濾鏡效果,變動部分相對較少,下半部分都是不會變動,故考慮縮減Canvas高度,將下半部分分離出來做純色出來或者只貼一層半透明Canvas,透明度值直接計算出來。

? ? ? ? 本APP中簡單處理,將原來320*320尺寸的Canvas縮減為320*120,再將Canvas下移200px,直接縮減疊加層范圍,較少渲染計算范圍,提高APP性能。

2.1.1.4 優化措施2——直接計算半透明度

性能優化思路:

? ? ? ? 三個半透明層疊加,是否可以在一層中處理,對于不同坐標值,直接計算出顏色值,然后渲染,變量包括時間t、橫坐標x,計算分支判斷依據是縱坐標y。

3 參考鏈接

Immediate mode?vs.?retained?mode.

Other HTML5 Rocks?canvas articles.

The?Canvas section?ofDive into HTML5.

JSPerf?lets developers create JS performance tests.

Browserscope?stores browser performance data.

JSPref view, which renders JSPerf tests as charts.

Simon's blog post?on clearing the canvas.

Sebastian's blog post?on sub-pixel rendering performance.

Paul's blog post?on using the?requestAnimationFrame.

Ben's talk?about optimizing a JS NES emulator.

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容