Canvas繪圖中的路徑描邊與填充

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,博客地址爲http://www.cnblogs.com/jasonnode/ 。網站上有對應每一小節的在線練習你們能夠去試試。

1.路徑、描邊與填充

迄今爲止,在本章之中咱們所繪製的惟一圖形,就是經過在Canvas的繪圖環境對象上調用strokeRect()方法所畫的矩形。咱們也經過調用fillRect()方法對其進行了填充。這兩個方法都是當即生效的。實際上,它們是Canvas繪圖環境中僅有的兩個能夠用來當即繪製圖形的方法(strokeText()與fillText()方法也是進行當即繪製的,但文本不算是圖形)。繪圖環境對象中還有一些方法,用於繪製諸如貝塞爾曲線(bézier curve)這樣更爲複雜的圖形,這些方法都是基於路徑(path)的。node

大多數繪製系統,例如Scalable Vector Graphics(可縮放向量圖形,簡稱SVG)、Apple的Cocoa框架,以及Adobe Illustrator等,都是基於路徑的。使用這些繪製系統時,你須要先定義一個路徑,而後再對其進行描邊(也就是繪製路徑的輪廓線)或填充,也能夠在描邊的同時進行填充。圖2-13演示了這三種繪製方式。canvas

圖 2-13 圖形的描邊與填充效果

該應用程序建立了9個不一樣的路徑,對左邊一列的路徑進行了描邊操做,對中間一列的路徑進行了填充,並對右邊一列的路徑同時進行描邊與填充。瀏覽器

 

第一行的矩形路徑與最後一行的圓弧路徑都是封閉路徑(closed path),而中間一行的弧形路徑則是開放路徑(open path)。請注意,不論一個路徑是開放或是封閉,你均可以對其進行填充。當填充某個開放路徑時,瀏覽器會把它當成封閉路徑來填充。圖中右邊一列的中間那個圖形,就是這種效果。框架

 

程序清單2-9列出了圖2-13中那個應用程序的代碼。函數

 

程序清單2-9 文本、矩形與圓弧的描邊及填充網站

var context=document.getElementById('drawingCanvas').getContext('2d');
//Functions...
function drawGrid(context,color,stepx,stepy){
//Listing omitted for brevity.See Example 2.13
//for a complete listing.
}
//Initialization...
drawGrid(context,'lightgray',10,10);
//Drawing attributes...
context.font='48pt Helvetica';
context.strokeStyle='blue';
context.fillStyle='red';
context.lineWidth='2';
//Line width set to 2 for text
//Text...
context.strokeText('Stroke',60,110);
context.fillText('Fill',440,110);
context.strokeText('Stroke&Fill',650,110);
context.fillText('Stroke&Fill',650,110);
//Rectangles...
context.lineWidth='5';
//Line width set to 5 for shapes
context.beginPath();
context.rect(80,150,150,100);
context.stroke();
context.beginPath();
context.rect(400,150,150,100);
context.fill();
context.beginPath();
context.rect(750,150,150,100);
context.stroke();
context.fill();
//Open arcs...
context.beginPath();
context.arc(150,370,60,0,Math.PI*3/2);
context.stroke();
context.beginPath();
context.arc(475,370,60,0,Math.PI*3/2);
context.fill();
context.beginPath();
context.arc(820,370,60,0,Math.PI*3/2);
context.stroke();
context.fill();
//Closed arcs...
context.beginPath();
context.arc(150,550,60,0,Math.PI*3/2);
context.closePath();
context.stroke();
context.beginPath();
context.arc(475,550,60,0,Math.PI*3/2);
context.closePath();
context.fill();
context.beginPath();
context.arc(820,550,60,0,Math.PI*3/2);
context.closePath();
context.stroke();
context.fill();

 

首先調用beginPath()方法來開始一段新的路徑,rect()與arc()方法分別用於建立矩形及弧形路徑。而後,應用程序在繪圖環境對象上調用stroke()與fill()方法,對剛纔那些路徑進行描邊或填充。spa

描邊與填充操做的效果取決於當前的繪圖屬性,這些屬性包括了lineWidth、strokeStyle、fillStyle以及陰影屬性等。好比,程序清單2-9中的這個應用程序,將lineWidth屬性值設置爲2,而後對文本進行描邊,其後又將其重置爲5,再對路徑進行描邊。rest

由rect()方法所建立的路徑是封閉的,然而,arc()方法建立的圓弧路徑則不封閉,除非你用它建立的是個圓形路徑。要封閉某段路徑,必須像程序清單2-9中那樣,調用closePath()方法才行。code

表2-5總結了本應用程序中與路徑相關的方法。對象

 

提示:路徑與隱形墨水

有一個很恰當的比喻,能夠用來講明「建立路徑隨後對其進行描邊或填充」這個操做。咱們能夠將該操做比做「使用隱形墨水來繪圖」。

你用隱形墨水所繪製的內容並不會馬上顯示出來,必須進行一些後續操做,像是加熱、塗抹化學藥品、照射紅外線等,才能夠將你所畫的內容顯示出來。若是讀者關注這個話題,能夠在http://en.wikipedia.org/wiki/Invisible_ink讀到全部關於隱形墨水的知識。

使用rect()與arc()這樣的方法來建立路徑,就比如使用隱形墨水來進行繪製同樣。這些方法會建立一條不可見的路徑,稍後能夠調用stroke()或fill()令其可見。

2. 路徑與子路徑

在某一時刻,canvas之中只能有一條路徑存在,Canvas規範將其稱爲「當前路徑」(current path)。然而,這條路徑卻能夠包含許多子路徑(subpath)。而子路徑,又是由兩個或更多的點組成的。比方說,能夠像這樣繪製出兩個矩形來:

 

context.beginPath();
//Clear all subpaths from
//the current path
context.rect(10,10,100,100);//Add a subpath with four points
context.stroke();
//Stroke the subpath containing
//four points
context.beginPath();
//Clear all subpaths from the
//current path
context.rect(50,50,100,100);//Add a subpath with four points
context.stroke();
//Stroke the subpath containing
//four point

 

以上這段代碼經過調用beginPath()來開始一段新的路徑,該方法會將當前路徑中的全部子路徑都清除掉。而後,這段代碼調用了rect()方法,此方法向當前路徑中增長了一個含有4個點的子路徑。最後,調用stroke()方法,將當前路徑的輪廓線描繪出來,使得這個矩形出如今canvas之中。

接下來,這段代碼又一次調用了beginPath()方法,該方法清除了上一次調用rect()方法時所建立的子路徑。而後,再一次調用rect()方法,此次仍是會向當前路徑中增長一段含有4個點的子路徑。最後,對該路徑進行描邊,使得第二個矩形也出如今了canvas之中。

如今考慮一下,若是將第二個beginPath()調用去掉,會怎麼樣呢?像是這樣:

 

context.beginPath();
//Clear all subpaths from the
//current path
context.rect(10,10,100,100);//Add a subpath with four points
context.stroke();
//Stroke the subpath containing
//four points
context.rect(50,50,100,100);//Add a second subpath with
//four points
context.stroke();
//Stroke both subpaths

上面這段代碼在一開始與剛纔那段是同樣的:先調用beginPath()來清除當前路徑中的全部子路徑,而後調用rect()來建立一條包含矩形4個點的子路徑,再調用stroke()方法使得這個矩形出如今canvas之上。

接下來,這段代碼再次調用了rect()方法,不過這一次,因爲沒有調用beginPath()方法來清除原有的子路徑,因此第二次對rect()方法的調用,會向當前路徑中增長一條子路徑。最後,該段代碼再一次調用stroke()方法,此次對stroke()方法的調用,將會使得當前路徑中的兩條子路徑都被描邊,這意味着它會重繪第一個矩形。

填充路徑時所使用的「非零環繞規則」

若是當前路徑是循環的,或是包含多個相交的子路徑,那麼Canvas的繪圖環境變量就必需要判斷,當fill()方法被調用時,應該如何對當前路徑進行填充。Canvas在填充那種互相有交叉的路徑時,使用「非零環繞規則」(nonzero winding rule)來進行判斷。圖2-14演示了該規則的運用。

「非零環繞規則」是這麼來判斷有自我交叉狀況的路徑的:對於路徑中的任意給定區域,從該區域內部畫一條足夠長的線段,使此線段的終點徹底落在路徑範圍以外。圖2-14中的那三個箭頭所描述的就是上面這個步驟。

圖 2-14 在填充路徑時運用「非零環繞規則」來進行判斷

接下來,將計數器初始化爲0,而後,每當這條線段與路徑上的直線或曲線相交時,就改變計數器的值。若是是與路徑的順時針部分相交,則加1,若是是與路徑的逆時針部分相交,則減1。若計數器的最終值不是0,那麼此區域就在路徑裏面,在調用fill()方法時,瀏覽器就會對其進行填充。若是最終值是0,那麼此區域就不在路徑內部,瀏覽器也就不會對其進行填充了。

能夠從圖2-14中看出「非零環繞規則」是如何運用的。左邊的那個帶箭頭的線段,先穿過了路徑的逆時針部分,而後又穿過了路徑的順時針部分。這意味着其計數值是0,因此該線段起點所在的那個區域就不在範圍內,在調用fill()方法時,瀏覽器也就不會對其進行填充。然而,其他兩條帶箭頭的線段,其計數值都不是0,因此它們的起點所在的區域就會被瀏覽器填充。

3. 剪紙效果

咱們來運用一下所學到的路徑、陰影以及非零環繞原則等知識,實現如圖2-15所示的剪紙(cutout)效果。

圖 2-15 用兩個圓形作出的剪紙效果

圖2-15所示的應用程序,其JavaScript代碼列在了程序清單2-10之中。

這段JavaScript代碼建立了一條路徑,它由兩個圓形所組成,其中一個圓形在另外一個的內部。經過設定arc()方法的最後一個參數值,該應用程序以順時針方向繪製了內部的圓形,而且以逆時針方向繪製了外圍的圓形。繪製效果如圖2-16上方所示。

圖 2-16 運用「非零環繞規則」來實現剪紙效果

在建立好路徑以後,圖2-15中的那個應用程序對該路徑進行了填充。瀏覽器運用「非零環繞規則」,對外圍圓形的內部進行了填充,不過填充的範圍並不包括裏面的圓,這就產生了一種剪紙圖案的效果。你也能夠利用此技術來剪出任意想要的形狀來。

程序清單2-10 圖2-15所示應用程序的JavaScript代碼

var context=document.getElementById('canvas').getContext('2d');
//Functions...
function drawGrid(color,stepx,stepy){
//Listing omitted for brevity.See Example 2.13
//for a complete listing.
function drawTwoArcs(){
context.beginPath();
context.arc(300,190,150,0,Math.PI*2,false);//Outer:CCW
context.arc(300,190,100,0,Math.PI*2,true);//Inner:CW
context.fill();
context.shadowColor=undefined;
context.shadowOffsetX=0;
context.shadowOffsetY=0;
context.stroke();
function draw(){
context.clearRect(0,0,context.canvas.width,
context.canvas.height);
}
}
drawGrid('lightgray',10,10);
context.save();
context.shadowColor='rgba(0,0,0,0.8)';
context.shadowOffsetX=12;
context.shadowOffsetY=12;
context.shadowBlur=15;
drawTwoArcs();
context.restore();
}
//Initialization...
context.fillStyle='rgba(100,140,230,0.5)';
context.strokeStyle=context.fillStyle;
draw();

圖2-16之中的例子是對圖2-15所示應用程序的一種擴展,它會告訴你若是兩個圓形子路徑都在同一個方向上,繪製效果會如何,同時它也增長了一些註釋信息,用以顯示圓形子路徑的繪製方向以及「非零環繞規則」的計算過程。並且,這個程序還顯示了建立圓形子路徑所調用的arc()方法。

提示:圖2-16之中的那條橫線是怎麼回事

請注意圖2-16中兩個圓之間的那條橫線。在圖2-15之中也有這樣一條線,不過圖2-16使用了更深一些的描邊顏色,把它畫得更加明顯了。

根據Canvas規範,當使用arc()方法向當前路徑中增長子路徑時,該方法必須將上一條子路徑的終點與所畫圓弧的起點相連。

製做剪紙圖形

圖2-17之中的應用程序在矩形內剪出了三個圖形。與上一小節中所討論的那個程序不一樣,圖2-17所示的應用程序採用了徹底不透明的顏色來填充這個包含剪紙圖形的矩形。

 

圖 2-17 各類剪紙圖形

該應用程序有兩個值得注意的地方。首先,包圍剪紙圖形的是一個矩形而不是圓形。這個矩形的使用向你代表,能夠用任意形狀的路徑來包圍剪紙圖形,並不必定非要用圓形。該程序創建剪紙圖形所用的代碼以下:

 

function drawCutouts(){
context.beginPath();
addOuterRectanglePath();//Clockwise(CW)
addCirclePath();
//Counter-clockwise(CCW)
addRectanglePath();
//CCW
addTrianglePath();
//CCW
context.fill();//Cut out shapes
}

addOuterRectanglePath()、addCirclePath()、addRectanglePath()及addTrianglePath()方法分別向當前路徑中添加了表示剪紙圖形的子路徑。

圖2-17中的應用程序還有一個有意思的地方,就是其中的矩形剪紙圖案。arc()方法可讓調用者控制圓弧的繪製方向,然而rect()方法則沒有那麼方便,它老是按照順時針方向來建立路徑。但是,在本例這種狀況下,須要的是一條逆時針的矩形路徑,因此咱們本身建立了一個rect()方法,此方法像arc()同樣,可讓調用者控制矩形路徑的方向:

 

function rect(x,y,w,h,direction){
if(direction){//CCW
context.moveTo(x,y);
context.lineTo(x,y+h);
context.lineTo(x+w,y+h);
context.lineTo(x+w,y);
}
else{
context.moveTo(x,y);
context.lineTo(x+w,y);
context.lineTo(x+w,y+h);
context.lineTo(x,y+h);
}
context.closePath();
}

上述代碼使用moveTo()與lineTo()方法來建立順時針或者逆時針的矩形路徑。在2.8節中咱們將詳細講述這些方法。

該應用程序在創建路徑時,分別使用了兩種不一樣的方式來建立外圍矩形及內部的矩形剪紙圖形:

function addOuterRectanglePath(){
context.rect(110,25,370,335);
}
function addRectanglePath(){
rect(310,55,70,35,true);
}

addOuterRectanglePath()方法使用了繪圖環境對象的rect()方法,此方法老是按照順時針方向來繪製矩形的,並無提供逆時針繪製的選項。addRectanglePath()方法建立了矩形剪紙圖形的路徑,它使用上面列出的那個rect()方法來繪製逆時針的矩形路徑。

圖2-17所示應用程序的JavaScript代碼列在了程序清單2-11之中。

小技巧:路徑方向真的很重要

arc()方法的最後一個boolean參數用於控制所繪圓弧路徑的方向。若是該參數是默認值true,那麼瀏覽器就會以順時針方向來繪製路徑,不然,瀏覽器就按照逆時針(counterclockwise)方向來繪製(或者按照Canvas規範中的說法,「反時針方向」(anti-clockwise))。

提示:arc()方法能夠控制路徑方向,而rect()方法則不行

arc()方法與rect()方法均可以向當前路徑中添加子路徑,然而arc()方法可讓調用者來控制路徑的繪製方向。幸虧能夠很是容易地實現一個函數,用它來建立具備特定方向的矩形路徑。程序清單2-11中的rect()方法就演示了這種作法。

小技巧:去掉由arc()方法所產生的那條不太美觀的鏈接線

若是在當前路徑中存在子路徑的狀況下調用arc()方法,那麼此方法就會從子路徑的終點向圓弧的起點畫一條線。一般狀況下,你並不想看到這條線段。

若是不想讓這條連線出現,能夠在調用arc()方法來繪製圓弧以前,先調用beginPath()方法。調用此方法會將當前路徑下的全部子路徑都清除掉,這樣一來,arc()方法也就不會再繪製那條鏈接線了。

程序清單2-11 繪製剪紙圖形的代碼

var context=document.getElementById('canvas').getContext('2d');
//Functions...
function drawGrid(color,stepx,stepy){
//Listing omitted for brevity.See Example 2.13
//for a complete listing.
}
function draw(){
context.clearRect(0,0,context.canvas.width,
context.canvas.height);
drawGrid('lightgray',10,10);
context.save();
context.shadowColor='rgba(200,200,0,0.5)';
context.shadowOffsetX=12;
context.shadowOffsetY=12;
context.shadowBlur=15;
drawCutouts();
strokeCutoutShapes();
context.restore();
}
function drawCutouts(){
context.beginPath();
addOuterRectanglePath();//CW
addCirclePath();
//CCW
addRectanglePath();//CCW
addTrianglePath();//CCW
context.fill();//Cut out shapes
}
function strokeCutoutShapes(){
context.save();
context.strokeStyle='rgba(0,0,0,0.7)';
context.beginPath();
addOuterRectanglePath();//CW
context.stroke();
context.beginPath();
addCirclePath();
addRectanglePath();
addTrianglePath();
context.stroke();
context.restore();
function rect(x,y,w,h,direction){
if(direction){//CCW
context.moveTo(x,y);
context.lineTo(x,y+h);
context.lineTo(x+w,y+h);
context.lineTo(x+w,y);
context.closePath();
}
else{
context.moveTo(x,y);
context.lineTo(x+w,y);
context.lineTo(x+w,y+h);
context.lineTo(x,y+h);
context.closePath();
}
function addOuterRectanglePath(){
context.rect(110,25,370,335);
}
function addCirclePath(){
context.arc(300,300,40,0,Math.PI*2,true);
}
function addRectanglePath(){
rect(310,55,70,35,true);
}
function addTrianglePath(){
context.moveTo(400,200);
context.lineTo(250,115);
context.lineTo(200,200);
context.closePath();
//Initialization...
context.fillStyle='goldenrod';
draw();
}
}
}
相關文章
相關標籤/搜索