canvas 繪製雙線技巧

weixin_33670713發表於2018-12-18

楔子

最近一個專案,需要繪製雙線的效果,雙線效果表示的是軌道(類似鐵軌之類的),如下圖所示:

負責這塊功能開發的小夥,姑且稱之為L吧,最開始是通過數學計算的方式來實現這種雙線,也就是在原來的路徑的基礎上,計算出兩條路徑。但是這個過程的計算算挺複雜,而是最終實現的效果很耗效能,效能損耗估計主要在於路徑的計算上。

優化技巧

後來他找到我來看這個問題,我在分析了專案背景的情況下,給予了一個簡單的繪製技巧,就是先用較粗的線條繪製路徑,然後再用較細的線條繪製路徑,較細線條的顏色正好是背景顏色。
之所以能夠使用這個技巧,是因為該專案的繪製背景是純色的,而不是漸變色或者圖片。
示例程式碼如下:

               ctx.beginPath();
               ctx.fillStyle = 'blue';
               ctx.rect(10,10,1000,1000);
               ctx.fill();

              ctx.save();
              ctx.strokeStyle = 'red';
              ctx.lineWidth = 10;
              ctx.lineCap = "round";
              ctx.beginPath();
              ctx.moveTo(200,100);  //起始點
              ctx.lineTo(400,100);
              ctx.quadraticCurveTo(500,100,500,200);   
              ctx.lineTo(500,400);
              ctx.quadraticCurveTo(500,500,400,500);   
              ctx.lineTo(200,500);
              ctx.quadraticCurveTo(100,500,100,400);   
              ctx.lineTo(100,200);
              ctx.quadraticCurveTo(100,100,200,100);   
              ctx.stroke();

              ctx.strokeStyle= 'blue'
              ctx.lineWidth = 4;
              ctx.stroke();
              ctx.restore();
              

程式碼的思路是,首先使用純色blue繪製了一個背景,然後使用線條顏色red繪製一條線,然後使用較小的線寬,並把線條顏色改成背景顏色blue,繪製另外一個條線段。最終的繪製效果如下:

6271001-ef866ee9604e62b7.png
double_line

到此,專案的這個技術難點問題,算是被解決了。這種解決方法,不僅演算法簡單,不用構思數學方法來構造雙線,而且輕量,不會有效能負擔。

背景不是純色情況

前面說到:之所以能夠使用這個技巧,是因為該專案的繪製背景是純色的,而不是漸變色或者圖片。
那如果背景是圖片或者漸變顏色情況下,用這種技巧,肯定就是失效的了。

之所以會思考這個問題,是得益於公司的技術分享會。我會要求員工定期組織分享會,分享一些經驗。在此打個小廣告,可以看出我們公司的技術氛圍是很好的,所以有興趣的小夥伴可以抓緊時間投簡歷。怎麼投簡歷呢,關注微訊號ITman彪叔。
過程中,當時小夥伴L也分享了前面提到這種思路。在分享的過程中,我提出了進一步的問題,如果背景不是純色,而是漸變色或者圖片怎麼辦?並且靈感乍現,想到了一個解決方法,就是使用ctx.globalCompositeOperation。

有關globalCompositeOperation的說明,可以參考如下連結的說明:
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
http://www.w3school.com.cn/tags/canvas_globalcompositeoperation.asp

globalCompositeOperation的定義和用法

globalCompositeOperation 屬性設定或返回如何將一個源(新的)影象繪製到目標(已有)的影象上。其中:

  • 源影象 = 您打算放置到畫布上的繪圖。
  • 目標影象 = 您已經放置在畫布上的繪圖
    下圖顯示了globalCompositeOperation的不同的值的解釋:


    6271001-b7746eeee52239ab.png
    globalCompositeOperation的不同的值的解釋

要實現雙線的繪製,就要求用同樣的路徑,不同的線寬繪製兩條線路
(我們稱之為目標線路和源線路)。並要達到一條線路摳出另外一條線路的效果。
結合上圖,我們可以看出destination-out,source-out,xor可以達到效果。下面以destination-out舉例說明。

destination-out繪製原理說明

比如首先通過 css 設定背景圖,並去掉繪製背景顏色,程式碼如下:

 <body onload="init()" style="background: url(../test/images/diffuse.png);">

然後繪製程式碼如下:

  ctx.save();
              ctx.strokeStyle = 'red';
              ctx.lineWidth = 10;
              ctx.lineCap = "round";
              ctx.beginPath();
              ctx.moveTo(200,100);  //起始點
              ctx.lineTo(400,100);
              ctx.quadraticCurveTo(500,100,500,200);   
              ctx.lineTo(500,400);
              ctx.quadraticCurveTo(500,500,400,500);   
              ctx.lineTo(200,500);
              ctx.quadraticCurveTo(100,500,100,400);   
              ctx.lineTo(100,200);
              ctx.quadraticCurveTo(100,100,200,100);   
              ctx.stroke();

              
              ctx.globalCompositeOperation = 'destination-out';
              ctx.lineWidth = 4;
              ctx.stroke();
              ctx.restore();

首先設定路徑,然後設定線寬為10,呼叫stroke方法繪製一條線寬為10的路線A。
之後設定globalCompositeOperation為 'destination-out',調整線寬為4,呼叫stroke方法繪製一條線寬為4的路線B。
看下destination-out的解釋:

在源影象外顯示目標影象。只有源影象外的目標影象部分會被顯示,源影象是透明的。

繪製了線路A的canvas影象是目標影象,線路B是源影象。根據上面解釋,只有源影象之外的目標影象能夠被顯示。最終繪製的效果如下:


6271001-7df93d00905d692d.png
destination-out.png

xor 和 source-out

把上面的程式碼的globalCompositeOperation修改成xor,發現效果也是可以的,xor的解釋如下:

使用異或操作對源影象與目標影象進行組合。 英文解釋如下:
Shapes are made transparent where both overlap and drawn normal everywhere else.

意思源和目標的畫素重疊(overlap)的部分會被變成透明畫素,其他部分正常繪製。 所以上面示例中,線條A和線條B重疊的部分會被變成透明。繪製的效果也是線條A的被挖空。

對於source-out,其效果正好和destination-out的效果相反:

在目標影象之外顯示源影象。只會顯示目標影象之外源影象部分,目標影象是透明的。

應此只需要取反操作即可,先用寬度4繪製線條A,然後用寬度10繪製線條B,其結果也是一樣的。

背景不是純色情況2

前面的背景是通過css的方式設定上去的,如果是通過canvas的drawImage直接繪製上去,效果就不一樣了。還是以destination-out為例說明,首先繪製了image,然後繪製線路A,此時的目標影象不在是線路A組成的圖形,而是image和線路A組合成的圖形,此時用destination-out的方式繪製線路B,不僅會挖空線路A,背景也會被挖空,如下圖所示:

6271001-564850aeb4fb622f.png
背景不是純色情況2

應此要想達到真正的雙線效果,要麼背景只能是用css設定,要麼用兩個canvas疊加,一個繪製背景圖片,一個繪製路徑。
當然還有一種方式,就是繪製雙線總是在一個臨時的canvas上面進行,然後把這個臨時的canvas繪製結果再次繪製到工作canvas上面,相關實踐留給讀者自己進行。

後記

在網路上面搜尋canvas double line,搜尋到stackoverflow上的一條結果如下:
https://stackoverflow.com/questions/13441610/double-line-stroke-in-html5-canvas
其中的答案也是採用了globalCompositeOperation設定為destination-out的方式。

歡迎關注公眾號“ITman彪叔”。彪叔,擁有10多年開發經驗,現任公司系統架構師、技術總監、技術培訓師、職業規劃師。熟悉Java、JavaScript、Python語言,熟悉資料庫。熟悉java、nodejs應用系統架構,大資料高併發、高可用、分散式架構。在計算機圖形學、WebGL、前端視覺化方面有深入研究。對程式設計師思維能力訓練和培訓、程式設計師職業規劃有濃厚興趣。


6271001-aad6ae8e5175418f.png
ITman彪叔公眾號

相關文章