這篇學習和回顧canvas系列筆記的第二篇,完整筆記詳見:canvas核心技術
通過上一篇canvas核心技術-如何繪製線段的學習,我們知道了如何去繪製線段。很多的線段的拼接就組成了圖形了,比如常見的三角形,矩形,圓形等。
常見圖形的繪製可以檢視我的線上示例:canvas shape
示例專案倉庫地址:canvas demo
圖形
三角形
先來看看如何繪製一個三角形。三角形就是由三條邊組成,我們可以理解為三個線段組成。確定了三角形的三個頂點的座標位置,然後用線連線起來。
let point1 = [100, 30]; //頂底1
let point2 = [50, 100]; //頂點2
let point3 = [180, 120]; //頂點3
ctx.beginPath(); //開始一段新路徑
ctx.moveTo(point1[0], point1[1]); //移動起點到頂點1
ctx.lineTo(point2[0], point2[1]); //連線頂點1與頂點2
ctx.lineTo(point3[0], point3[1]); //連線頂點2與頂點3
ctx.stroke(); //描邊
//繪製頂點座標顯示出來
ctx.textAlign=`center`; //繪製文字水平居中
ctx.fillText(`(${point1[0]},${point1[1]})`, point1[0], point1[1]-10); //繪製頂點1文字
ctx.fillText(`(${point2[0]},${point2[1]})`, point2[0]-25, point2[1]+5); //繪製頂點2文字
ctx.fillText(`(${point3[0]},${point3[1]})`, point3[0]+30, point3[1]+5); //繪製頂點3文字
複製程式碼
從圖可以看到,我們還有一條邊沒有連線起來,這是因為我們只顯示的連線了2個頂點。要想把第三條邊也連線起來,我們有2種方式。第一種方式是,我們顯示的連線頂點3與頂點1
//第一種方式,顯示的連線頂點3於頂點1
ctx.lineTo(point1[0], point1[1]);
複製程式碼
第二種方式是,我們呼叫ctx.closePath()
來按canvas自動幫我們連線未關閉的路徑。
//第二種方式,呼叫ctx.closePath()
ctx.closePath();
複製程式碼
無論哪一種都可以實現我們想要三角形。其中第二種方式會用的比較多,因為它會幫我們自動關閉當前路徑,也就是使當前路徑形成一個閉合的路徑,這個在填充時是非常有用的,下面會說的。最終,我們得到三角形圖形如下
四邊形
通過三個頂點,我們可以繪製一個三角形,那麼通過四個點,我們當然可以繪製出四邊形,我們照例來通過四個點來繪製一個矩形。
let point1 = [80, 30]; //p1
let point2 = [180, 30]; //p2
let point3 = [80, 110]; //p3
let point4 = [180, 110]; //p4
ctx.strokeStyle = `green`; //設定描邊顏色為綠色
ctx.beginPath(); //開始新的一段路徑
ctx.moveTo(point1[0], point1[1]); //移動起點到p1
ctx.lineTo(point2[0], point2[1]); //連線p1與p2
ctx.lineTo(point4[0], point4[1]); //連線p2與p4
ctx.lineTo(point3[0], point3[1]); //連線p4與p3
ctx.closePath(); //關閉當前路徑,隱士連線p3與p1
ctx.stroke(); //描邊
//繪製頂點
ctx.textAlign = `center`;
ctx.fillText(`p1`, point1[0] - 10, point1[1] - 10);
ctx.fillText(`p2`, point2[0] + 10, point2[1] - 10);
ctx.fillText(`p3`, point3[0] - 10, point3[1] + 10);
ctx.fillText(`p4`, point4[0] + 10, point4[1] + 10);
複製程式碼
注意,我們的順序是p1–>p2–>p4–P3,由於矩形是一種特殊的四邊形,在canvas中提供了一種方法可以快速建立一個矩形,如果知道了p1的座標和矩形的寬度和高度,那麼我們就可以確定了其他三個點的座標。
//快速建立矩形
ctx.rect(point1[0], point1[1], 100, 80);
複製程式碼
在建立矩形,我們總是使用ctx.rect(left,top,width,height)
,但是繪製非矩形的四邊形,還是得按照每個點去連線成線段來繪製。
圓與圓弧
圓形可以看作是無數個很小的線段連線起來的,但是通過去定頂點來繪製圓形,顯然不現實。canvas中提供了一個專門繪製圓形的方法ctx.arc(left,top,radius,startAngle,endAngle,antiClockwise)
。各個引數的順序意思是,圓心座標X值,圓心座標Y值,半徑,開始弧度,結束弧度,是否逆時針。通過指定startAngle=0
和endAngle=Math.PI*2
,就可以繪製一個完整的圓了。最後一個引數antiClockwise
對於圖片的填充時會非常有用,後面講填充時會詳細說到。
let center = [100, 75]; //圓心座標
let radius = 50; //半徑
let startAngle = 0; //開始弧度值
let endAngle = Math.PI * 2; //結束弧度值,360度=Math.PI * 2
let antiClockwise = false; //是否逆時針
ctx.strokeStyle = `blue`; //描邊顏色
ctx.lineWidth = 1;
ctx.arc(center[0], center[1], radius, startAngle, endAngle, antiClockwise);
ctx.stroke(); //將圓形描邊繪製出來
//繪製出圓心和半徑示意圖,讀者可以忽略下半部程式碼
ctx.beginPath();
ctx.fillStyle = `red`;
ctx.arc(center[0], center[1], 2, startAngle, endAngle, antiClockwise);
ctx.fill();
ctx.beginPath();
ctx.moveTo(center[0], center[1]);
ctx.lineTo(center[0] + radius, center[1]);
ctx.stroke();
ctx.fillStyle = `blue`;
ctx.font = `24px sans-serif`;
ctx.textAlign = `center`;
ctx.fillText(`r`, center[0] + radius / 2, center[1] - 10);
複製程式碼
我們還可以改變起始和結束弧度的值,來繪製不同角度的弧形。比如八分之一圓弧,四分之圓弧,半圓弧等。
let center = [50, 75]; //圓心座標
let radius = 20; //半徑
let startAngle = 0; //起始弧度為0
let antiClockwise = false; //是否逆時針
let angles = [1 / 8, 1 / 4, 1 / 2, 3 / 4]; //弧度長度
let colors = [`red`, `blue`, `green`, `orange`]; //描邊顏色
for (let [i, angle] of angles.entries()) {
let endAngle = Math.PI * 2 * angle; //計算結束弧度
ctx.strokeStyle = colors[i]; //設定描邊顏色
ctx.beginPath(); //開始新的路徑
ctx.arc(center[0] + i * radius * 3, center[1], radius, startAngle, endAngle, antiClockwise); //繪製圓弧
ctx.stroke(); //描邊
}
複製程式碼
任意多邊形
上面說的都是一些比較簡單和常見的圖形,我們如何可以繪製任意多邊形,比如五邊形,六邊形,八邊形等。其實,在繪製四邊形的時候就說過了,可以通過確定頂點座標,然後把這些頂點按照一定順序連線起來就可以了。下面,來實現一個通用的多邊形的繪製方法。
class Polygon {
constructor(ctx, points) {
this.ctx = ctx;
this.points = points;
}
draw() {
if (!this.ctx instanceof CanvasRenderingContext2D) {
throw new Error(`Polygon#ctx must be an CanvasRenderingContext2D instance`);
}
if (!Array.isArray(this.points)) {
throw new Error(`Polygon#points must be an Array`);
}
if (!this.points.length) {
return;
}
let firstPoint = this.points[0];
let restPoint = this.points.slice(1);
ctx.beginPath();
ctx.moveTo(firstPoint[0], firstPoint[1]);
for (let point of restPoint) {
ctx.lineTo(point[0], point[1]);
}
ctx.closePath();
}
}
複製程式碼
通過例項化這個Polygon
,並傳入多邊形的頂點座標,我們就可以繪製出不同的多邊形。例如下面的程式碼,分別繪製了五邊形,六邊形。
//繪製五邊形
let points = [[30, 40], [80, 40], [100, 80], [55, 120], [10, 80]];
let pentagon = new Polygon(ctx, points);
ctx.strokeStyle = `blue`;
pentagon.draw();
ctx.stroke();
//繪製六邊形
points = [[160, 40], [210, 40], [230, 80], [210, 120], [160, 120], [140, 80]];
let hexagon = new Polygon(ctx, points);
ctx.strokeStyle = `green`;
hexagon.draw();
ctx.stroke();
複製程式碼
填充
上面,我們都是用描邊把圖形繪製出來,還有一種用的比較多的就是填充了。填充就是用特定的顏色把圖形包圍的區域塗滿。
let point1 = [100, 30]; //頂底1
let point2 = [50, 100]; //頂點2
let point3 = [180, 120]; //頂點3
ctx.strokeStyle = `red`; //用紅色描邊
ctx.fillStyle = `yellow`; //用黃色填充
ctx.lineWidth = 2; //設定線段寬度為2
ctx.beginPath(); //開始一段新路徑
ctx.moveTo(point1[0], point1[1]); //移動起點到頂點1
ctx.lineTo(point2[0], point2[1]); //連線頂點1與頂點2
ctx.lineTo(point3[0], point3[1]); //連線頂點2與頂點3
ctx.closePath(); //關閉當前路徑
ctx.stroke(); //描邊
ctx.fill(); //填充
複製程式碼
需要注意的是,如果當前路徑沒有關閉,那麼會先預設關閉當前路徑,然後在進行填充 ,如下,我們把ctx.closePath()
註釋掉。
let point1 = [100, 30]; //頂底1
let point2 = [50, 100]; //頂點2
let point3 = [180, 120]; //頂點3
ctx.strokeStyle = `red`; //用紅色描邊
ctx.fillStyle = `yellow`; //用黃色填充
ctx.lineWidth = 2; //設定線段寬度為2
ctx.beginPath(); //開始一段新路徑
ctx.moveTo(point1[0], point1[1]); //移動起點到頂點1
ctx.lineTo(point2[0], point2[1]); //連線頂點1與頂點2
ctx.lineTo(point3[0], point3[1]); //連線頂點2與頂點3
// ctx.closePath(); //關閉當前路徑
ctx.stroke(); //描邊
ctx.fill(); //填充
複製程式碼
如果當前路徑是迴圈的,或者是包含多個相交的子路徑,那麼canvas何如進行填充呢?比如下面這樣的,為何在填充時,中間這一塊沒有被填充?
let point1 = [100, 30];
let point2 = [50, 100];
let point3 = [180, 120];
let point4 = [50, 60];
let point5 = [160, 80];
let point6 = [70, 120];
ctx.strokeStyle = `red`;
ctx.fillStyle = `yellow`;
ctx.lineWidth = 2;
ctx.beginPath(); //開始一段新路徑
//繪製三角形1, 順序:p1--p2--p3--p1
ctx.moveTo(point1[0], point1[1]);
ctx.lineTo(point2[0], point2[1]);
ctx.lineTo(point3[0], point3[1]);
ctx.lineTo(point1[0], point1[1]);
//繪製三角形2,順序:p4--p5--p6--p4
ctx.moveTo(point4[0], point4[1]);
ctx.lineTo(point5[0], point5[1]);
ctx.lineTo(point6[0], point6[1]);
ctx.lineTo(point4[0], point4[1]);
ctx.stroke(); //描邊
ctx.fill(); //填充
複製程式碼
我們來具體研究一下fill
函式,檢視MDN上的解釋,
The
CanvasRenderingContext2D
.fill() method of the Canvas 2D API fills the current or given path with the current fill style using the non-zero or even-odd winding rulevoid ctx.fill([fillRule]); void ctx.fill(path[, fillRule]); 複製程式碼
fillRule引數是可選的,可取值為nonzero
,evenodd
。也就是說,fill
函式可以給當前路徑或者給定的路徑,使用非零環繞規則或者奇偶規規則來填充。path 引數是一個Path2D
物件,是一個給定的路徑,canvas中預設的是當前路徑,這個引數並不是所有的瀏覽器都支援,目前看,還有IE系列和移動裝置上都沒有很好的支援,就不多說了,具體可以檢視Path2D。
非零環繞規則
對於路徑中的任意給定區域,從該區域內部畫出一條足夠長的線段,使此線段的終點完全落在路徑範圍之外。接下來,將計數器初始化為0,然後,每當這條線段與路徑上的直線或者曲線相交時,就改變計數器的值。如果是與路徑的順時針部分相交,則加1,如果是與路徑的逆時針部分相交,則減1。最後,如計數器的值不為0,則此區域就在路徑裡面,呼叫fill
時,該區域被填充。如果計數器的最終值為0,則此區域就不在路徑裡面,呼叫fill
時,該區域就不被填充。canvas的fill
預設使用的就是這種非零環繞規則。
再來看看上圖,為何中間交叉區域沒有被填充。我們繪製了2個三角形,第一繪製順序是p1–>p2–>p3–>p1,第二個繪製順序是p4–>p5–>p6–>p4 。可以看到第一個三角形在繪製是逆時針方向的,第二個三角形繪製是順時針方向的,中間相交區域的計數器最終值就為0了,所以不應該包含在這個路徑中。
非零環繞規則演示可以檢視我的示例:非零環繞示例
奇偶規則
跟非零環繞規則類似,都是從任意區域畫出一條足夠長的線,使此線段的終點完全落在路徑範圍之外。如果這個線段與路徑相交的個數位奇數,則此區域包含在路徑中,如果為偶數,則表示此區域不包含在路徑中。
例如,我們把上面的例子改下,繪製第二個三角形的順序改成逆時針p4–>p6–>p5–P4,然後分別用非零環繞規則和奇偶規則來填充,看看效果。
//繪製三角形2,注意順序變了:p4-p6-p5-p4
ctx.moveTo(point4[0], point4[1]);
ctx.lineTo(point6[0], point6[1]);
ctx.lineTo(point5[0], point5[1]);
ctx.lineTo(point4[0], point4[1]);
ctx.stroke(); //描邊
ctx.fill(); //填充, 預設就是非零環繞規則
複製程式碼
上面兩個三角形的順序都是逆時針,所以按照非零環繞規則,像個三角形的相交區域的計數器的最終值為-2,不為0,則包含在路徑中改,被填充了。
同樣的順序,我們在改用奇偶規則來填充。
ctx.fill(`evenodd`); //填充, 改用奇偶規則
複製程式碼
小結
這篇我們主要學習了canvas中如何繪製圖形,比如常見的三角形,四邊形,圓心,以及任意多邊形。在繪製圖形時,有些比如矩形,圓形等canvas已經提供了內建的函式,ctx.rect()
和ctx.arc
可以直接繪製,但是對於任意多邊形,我們則需要自己逐線段的繪製。
在繪製路徑時,是有順序的。理解canvas中路徑,和當前繪製的順序,就可以很好的理解了canvas中填充規則了。canvas中填充有非零環繞規則和奇偶規則。對於同樣的路徑,不同的規則可能會產生不同的填充區域,是使用時,注意路徑順序就好了。