摘要:最近的一個客戶項目中,簡化的需求是繪制按照行列繪制很多個圓圈。等等,客戶要求繪制的極限是萬個,而且每次繪制不能卡頓。然后通過通過創(chuàng)建對象,并把的繪制上下文的指定為該對象。另外繪制的效果其實(shí)是沒有繪制的效果好的,鋸齒嚴(yán)重。
最近的一個客戶項目中,簡化的需求是繪制按照行列繪制很多個圓圈。需求看起來不難,上手就可以做,寫兩個for循環(huán)。
原始繪制方法首先定義了很多Circle對象,在遍歷循環(huán)中調(diào)用該對象的draw方法。代碼如下:
for (var i = 0; i < column; i++) { for (var j = 0; j < row; j++) { var circle = new Circle({ x: 8 * i + 3, y: 8 * j + 3, radius: 3 }) box.push(circle); } } console.time("time"); for (var c = 0; c < box.length; c++) { var circle = box[c]; circle.draw(ctx); } console.timeEnd("time");
結(jié)果繪制出了按照行列排布的很多個圓圈了,如下圖所示:
恩,很簡單嘛,可以回家睡覺了。
等等,客戶要求繪制的極限是10萬個,而且每次繪制不能卡頓。先看下繪制10萬個圓圈的時間是多久,用console.time 統(tǒng)計繪制時間:
console.time("time"); // 實(shí)際繪制的代碼 console.timeEnd("time");
時間顯示為幾百毫秒(3到4百毫秒),如下圖所示:
幾百毫秒的繪制時間,必然是卡頓的。想要流暢操作,肯定還的優(yōu)化。
批量繪制首先想到的是批量繪制,前面的代碼中,每次變量都會調(diào)用circle.draw(ctx)方法,circle.draw方法代碼如下:
draw: function (ctx) { ctx.save(); ctx.lineWidth=this.lineWidth; ctx.strokeStyle=this.strokeStyle; ctx.fillStyle=this.fillStyle; ctx.beginPath(); this.createPath(ctx); ctx.stroke(); if(this.isFill){ctx.fill();} ctx.restore(); },
可以看出 每次遍歷都調(diào)用了一次beginPath和stroke方法。為了提高繪制效率,我們可以只調(diào)用beginPath和stroke方法一次,把所有的子路徑組織成為一個大的路徑,這就是所謂的批量繪制思路,代碼如下:
console.time("time"); ctx.beginPath(); for (var c = 0; c < box.length; c++) { var circle = box[c]; ctx.moveTo(circle.x + 3, circle.y); circle.createPath(ctx); } ctx.closePath(); ctx.stroke(); console.timeEnd("time");
調(diào)試發(fā)現(xiàn),確實(shí)效率有了很大的提升,時間減少到100毫秒左右,相當(dāng)于效率提高了3-4倍左右,如下圖所示:
。
需要注意的是上述代碼中的moveTo語句:
ctx.moveTo(circle.x + 3, circle.y);
這是因?yàn)椋?當(dāng)使用arc方法給路徑中添加子路徑的時候,arc所定義的路徑會自動和路徑集合中的最后一個路徑連接起來,如下圖所示:
此處的moveTo就是為了避免這種連接。
注意:arc 和arcTo都會有上述問題,但是rect定義的路徑卻不存在這種問題。Pattern 方式
通過以上優(yōu)化,客戶已經(jīng)覺得效率挺不錯了。 但是技術(shù)研究沒有止境,由于這個分布很規(guī)律,總感覺有更加快速的方法。最終突發(fā)靈感想到了一種方法,就是使用canvas 的Pattern功能:
canvas的fillStyle可以指定為一個pattern對象,而pattern可以實(shí)現(xiàn)一個簡單圖像的平鋪。基于這種思路,我們可以實(shí)現(xiàn)如下代碼:
var tempCanvas = document.createElement("canvas"); var ctx2 = tempCanvas.getContext("2d"); var w = 5,h = 5; tempCanvas.width = w; tempCanvas.height = h; dpr(tempCanvas); ctx2.fillStyle = "red"; ctx2.arc(w/2,h/2,w/2 - 1,0,Math.PI * 2); ctx2.stroke(); ctx.save(); ctx.beginPath(); var width = tempCanvas.width * 500,height = tempCanvas.height * 200; var pattern = ctx.createPattern(tempCanvas, "repeat"); ctx.clearRect(100,100,width,height); ctx.rect(100,100,width,height); ctx.fillStyle = pattern; ctx.fill(); ctx.restore();
代碼首先定義一個小的canvas,命名為tempCanvas,在tempCanvas上面繪制一個圓,需要注意的是tempCanvas的尺寸要設(shè)置為正好繪制下這個圓圈。
然后通過通過tempCanvas創(chuàng)建pattern對象,并把canvas的繪制上下文ctx的fillStyle指定為該pattern對象。
之后通過rect方法指定要fill的區(qū)域大小,改區(qū)域大小應(yīng)該是所有最終要繪制的圓圈的大小的總和:var width = tempCanvas.width 500,height = tempCanvas.height 200;
最后調(diào)用畫筆的fill方法,用tempCanvas填充區(qū)域。最終繪制的效果和繪制消耗的時間如下圖所示:
通過上圖可以看出,效率極高,可以達(dá)到零點(diǎn)幾毫秒的級別。
新的需求如果客戶需求只是這么簡單,相信使用canvas pattern對象這種方式,效率是最高的。但是,客戶的實(shí)際需求是,先繪制10萬個的圓圈,然后可以用擦除工具,擦除一些區(qū)域的圓圈,如下圖所示:
原始繪制方法和批量繪制方法要是實(shí)現(xiàn)上述效果,都很容易,只要把不需要繪制圓圈的位置,直接忽略掉即可以。
比如用一個map記錄需要忽略的圓圈的坐標(biāo),遍歷的時候判斷在map記錄中的地方就直接跳過不進(jìn)行繪制操作。canvas pattern + 裁剪
如果是canvas pattern的方式,應(yīng)該怎么實(shí)現(xiàn)上圖的效果呢? 經(jīng)過思索發(fā)現(xiàn)可以通過ctx.clip方法。
clip,裁剪。如果通過ctx.clip定義了裁剪區(qū)域,繪制的圖形只會在裁剪區(qū)域的部分顯示出來,裁剪區(qū)域之外的,則不會顯示。
沒一個圓圈都會占用一個矩形區(qū)域,本案例中,可以把要顯示的的圓圈所占的矩形區(qū)域都定義到裁剪區(qū)域里面,而不要顯示的圓圈的矩形區(qū)域則排除到裁剪區(qū)域之外,如下圖所示,繪制圓圈的矩形區(qū)域用實(shí)線表示出來,不繪制圓圈的區(qū)域用虛線表示:
只需要把所有實(shí)線表示的矩形區(qū)域都添加到要clip的路徑中去,然后調(diào)用fill方法,則只會在實(shí)現(xiàn)定義的矩形區(qū)域顯示出來圓圈。以下是示例代碼:
for(var i = 0;i < 400; i ++){ for(var j = 0;j < 400;j ++){ var r = Math.random(); if(r <0.2){ templateMap[i+":" + j] = true; continue; } var x = 10 + j * tempCanvas.width; var y = 10 + i * tempCanvas.height; var rect = { x : x, y : y, width : tempCanvas.width, height:tempCanvas.height }; ctx.rect(rect.x,rect.y,rext.width,rect.height); } ctx.clip();
首先遍歷所有的圓圈坐標(biāo),為了演示效果,用Math.random為了模擬隨機(jī)產(chǎn)生一個數(shù),如果這個數(shù)小于0.2,則當(dāng)前圓圈的矩形區(qū)域不會被加入裁剪區(qū)域,也就是該圓圈不會顯示出來。
通過上面裁剪操作后,“擦除后的效果”算是實(shí)現(xiàn)了。但是,經(jīng)過測試,性能卻低回去了,為什么,因?yàn)樵黾恿撕芏鄏ect操作。測試下來,一幁的繪制時間大概在80多毫秒,比批量繪制還是高一點(diǎn),但是感覺還是不夠好。
觀察上面 “裁剪區(qū)域” 這個圖,以第一行為例,第一、第二、第三個矩形區(qū)域是連在一塊的,完全沒有必要調(diào)用三次ctx.rect方法,而是先用算法把三個區(qū)域合并為一個矩形區(qū)域,然后調(diào)用一次ctx.rect方法即可,如下圖:
下面是合并裁剪區(qū)域的算法,目前只是實(shí)現(xiàn)了同一行的合并,更加優(yōu)化的合并算法并沒有實(shí)現(xiàn),代碼如下:
function calRectMap (tempCanvas){ if(rectMap != null){ return; } rectMap = rectMap || []; for(var i = 0;i < 400; i ++){ for(var j = 0;j < 400;j ++){ var r = Math.random(); if(r <0.2){ templateMap[i+":" + j] = true; continue; } var x = 10 + j * tempCanvas.width; var y = 10 + i * tempCanvas.height; var rect = { x : x, y : y, width : tempCanvas.width, height:tempCanvas.height }; lineRectMap[i] = lineRectMap[i] || []; lineRectMap[i][j] = rect; } unionLineRects(lineRectMap[i],rectMap); } } function unionLineRect(rect1,rect2){ return { x: rect1.x, y : rect1.y, width:rect1.width + rect2.width, height:rect1.height } } function unionLineRects(lineRectMap,rectMap){ var lastRect = null,lastNotNullIndex = null; for(var j = 0;j < 400;j ++){ var currentRect = lineRectMap[j]; if(lastRect == null){ lastRect = currentRect; }else{ if( lastNotNullIndex == j - 1 && currentRect){ lastRect = unionLineRect(lastRect,currentRect); } } if(currentRect != null){ lastNotNullIndex = j; }else if (lastRect){ rectMap.push(lastRect); lastNotNullIndex = null; lastRect = null; } } if(lastRect){ rectMap.push(lastRect); } }
相關(guān)合并的算法,此處不再詳細(xì)說明。 合并之后,測試?yán)L制的時間降低到了10幾毫秒,算是比較好的繪制效果了:
由于筆者本人也長期研究webgl的技術(shù),所以嘗試著用webgl實(shí)線了2d的繪制,相關(guān)細(xì)節(jié)不在此處贅述,后面會寫專門的文章如何用webgl繪制2d圖形。最終測試的效率不是很理想,差不多100多毫秒,和上面的批量繪制差不多。 因?yàn)橛脀ebgl繪制,單次的繪制效率應(yīng)該不會太差,但是由于需要遍歷調(diào)用10萬次繪制命令,必然效率不高。另外webgl繪制的效果其實(shí)是沒有2d繪制的效果好的,鋸齒嚴(yán)重。 要實(shí)現(xiàn)好的效果,還需要引入去鋸齒相關(guān)技術(shù)。 繪制的效果如下:
用webgl繪制2d圖形的相關(guān)主題,回頭會另外寫一篇文章介紹。敬請關(guān)注。
webgl2 引入了實(shí)例化數(shù)組,通過這個功能,可以實(shí)現(xiàn)把很多次的繪制調(diào)用合并為一個繪制調(diào)用,這會極大提高繪制效率。
有關(guān)實(shí)例化數(shù)組的功能,參考https://www.jianshu.com/p/d40...
繪制10萬個圓形的效率大概在每幀零點(diǎn)零幾毫秒,簡直就是大boss級別的快,如下圖:
后記通過這篇文章,除了想給讀者傳遞相關(guān)知識點(diǎn)之外,其實(shí)還想表達(dá)一個觀點(diǎn):
相比于知識點(diǎn),程序員更加需要鍛煉的是底層思維能力。在我看來,底層思維能力包括:學(xué)習(xí)力、創(chuàng)造力、判斷力和思考力。而勤于思考的人,不拘泥于司空見慣,都能夠從日??菰锏娜蝿?wù)中發(fā)現(xiàn)很多有趣的東西,啟發(fā)更多深入的思路。
勤于思索是很重要的。 知識是死的,人是活的,同樣的知識點(diǎn),在思考力強(qiáng)的人手上,就能延伸出很多好的解決方案。
這就要求人勤于探索,不要滿足于把任務(wù)完成,而是要多深入思考,多總結(jié),探索更多的方案和可能性。這本身有助于鍛煉思考力和創(chuàng)造力,而思考力和創(chuàng)造力又會反過來幫助你解決更多的問題。
其實(shí)IT行業(yè)的知識更新越來越快,能夠以不變應(yīng)萬變的人,就是擁有良好的學(xué)習(xí)力、創(chuàng)造力、判斷力和思考力的人。這些能力會讓你在變換萬千的技術(shù)海洋中,屹立不倒,不被淹沒。
當(dāng)然,標(biāo)書可能有點(diǎn)好為人師了。 在日常的工作中,彪叔更喜歡做的事情,就是啟迪下屬的思考,而不僅僅是某個問題的解決方案。這是比學(xué)習(xí)知識更加重要的素質(zhì)。彪叔也會在我的其他文章中,分享底層能力的相關(guān)認(rèn)知。有興趣的猿們可以關(guān)注彪叔的公號:ITman彪叔
歡迎關(guān)注公眾號:
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/99380.html
摘要:貝塞爾曲線方法可以繪制一種類似的曲線。不同的是貝塞爾曲線需要兩個控制點(diǎn)而不是一個,線段的每一個端點(diǎn)都需要一個控制點(diǎn)。下面是描述貝塞爾曲線的簡單示例。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Drawing on Canvas 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2...
摘要:游戲開發(fā)實(shí)戰(zhàn)主要講解使用來開發(fā)和設(shè)計各類常見游戲的思路和技巧,在介紹相關(guān)特性的同時,還通過游戲開發(fā)實(shí)例深入剖析了其內(nèi)在原理,讓讀者不僅知其然,而且知其所以然。HTML5 Canvas游戲開發(fā)實(shí)戰(zhàn)主要講解使用HTML5 Canvas來開發(fā)和設(shè)計各類常見游戲的思路和技巧,在介紹HTML5 Canvas相關(guān)特性的同時,還通過游戲開發(fā)實(shí)例深入剖析了其內(nèi)在原理,讓讀者不僅知其然,而且知其所以然。在本書...
摘要:支持設(shè)置圓角,并且能夠精確的控制圓角位置。如果你想要設(shè)置的顯示出來,必須設(shè)置為。如此驚艷的效果得益于內(nèi)置的動畫驅(qū)動,你能夠結(jié)合來實(shí)現(xiàn)難以置信的動畫效果。一切只需要在你合理的編寫好后,調(diào)用和來啟動停止動畫。 showImg(https://segmentfault.com/img/remote/1460000009148011); 簡介 歡迎使用SuperTextView,這篇文檔將會向...
摘要:圓弧二次貝塞爾曲線及三次貝塞爾曲線繪制二次貝塞爾曲線,為一個控制點(diǎn),為結(jié)束點(diǎn)。二次貝塞爾曲線三次貝塞爾曲線色彩設(shè)置圖形的填充顏色。線段末端以圓形結(jié)束。例如,表示顏色會出現(xiàn)在正中間。操控動畫當(dāng)設(shè)定好間隔時間后,會定期執(zhí)行。 矩形 fillRect(x, y, width, height) 填充矩形 strokeRect(x, y, width, height) 繪制矩形邊框 clear...
閱讀 1644·2023-04-26 01:54
閱讀 1659·2021-09-30 09:55
閱讀 2682·2021-09-22 16:05
閱讀 1898·2021-07-25 21:37
閱讀 2655·2019-08-29 18:45
閱讀 1920·2019-08-29 16:44
閱讀 1911·2019-08-29 12:34
閱讀 1383·2019-08-23 14:02