canvas核心技術-如何繪製圖形

三隻小羊發表於2019-03-03

這篇學習和回顧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文字
複製程式碼

canvas核心技術-如何繪製圖形

從圖可以看到,我們還有一條邊沒有連線起來,這是因為我們只顯示的連線了2個頂點。要想把第三條邊也連線起來,我們有2種方式。第一種方式是,我們顯示的連線頂點3與頂點1

//第一種方式,顯示的連線頂點3於頂點1
ctx.lineTo(point1[0], point1[1]);
複製程式碼

第二種方式是,我們呼叫ctx.closePath()來按canvas自動幫我們連線未關閉的路徑。

//第二種方式,呼叫ctx.closePath()
ctx.closePath();
複製程式碼

無論哪一種都可以實現我們想要三角形。其中第二種方式會用的比較多,因為它會幫我們自動關閉當前路徑,也就是使當前路徑形成一個閉合的路徑,這個在填充時是非常有用的,下面會說的。最終,我們得到三角形圖形如下

canvas核心技術-如何繪製圖形

四邊形

通過三個頂點,我們可以繪製一個三角形,那麼通過四個點,我們當然可以繪製出四邊形,我們照例來通過四個點來繪製一個矩形。

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);
複製程式碼

canvas核心技術-如何繪製圖形

注意,我們的順序是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=0endAngle=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);
複製程式碼

canvas核心技術-如何繪製圖形
我們還可以改變起始和結束弧度的值,來繪製不同角度的弧形。比如八分之一圓弧,四分之圓弧,半圓弧等。

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(); //描邊
}
複製程式碼

canvas核心技術-如何繪製圖形

任意多邊形

上面說的都是一些比較簡單和常見的圖形,我們如何可以繪製任意多邊形,比如五邊形,六邊形,八邊形等。其實,在繪製四邊形的時候就說過了,可以通過確定頂點座標,然後把這些頂點按照一定順序連線起來就可以了。下面,來實現一個通用的多邊形的繪製方法。

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();
複製程式碼

canvas核心技術-如何繪製圖形

填充

上面,我們都是用描邊把圖形繪製出來,還有一種用的比較多的就是填充了。填充就是用特定的顏色把圖形包圍的區域塗滿。

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核心技術-如何繪製圖形

需要注意的是,如果當前路徑沒有關閉,那麼會先預設關閉當前路徑,然後在進行填充 ,如下,我們把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核心技術-如何繪製圖形

如果當前路徑是迴圈的,或者是包含多個相交的子路徑,那麼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(); //填充
複製程式碼

canvas核心技術-如何繪製圖形

我們來具體研究一下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 rule

void 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(); //填充,  預設就是非零環繞規則
複製程式碼

canvas核心技術-如何繪製圖形

上面兩個三角形的順序都是逆時針,所以按照非零環繞規則,像個三角形的相交區域的計數器的最終值為-2,不為0,則包含在路徑中改,被填充了。

同樣的順序,我們在改用奇偶規則來填充。

ctx.fill('evenodd'); //填充,  改用奇偶規則
複製程式碼

canvas核心技術-如何繪製圖形

小結

這篇我們主要學習了canvas中如何繪製圖形,比如常見的三角形,四邊形,圓心,以及任意多邊形。在繪製圖形時,有些比如矩形,圓形等canvas已經提供了內建的函式,ctx.rect()ctx.arc可以直接繪製,但是對於任意多邊形,我們則需要自己逐線段的繪製。

在繪製路徑時,是有順序的。理解canvas中路徑,和當前繪製的順序,就可以很好的理解了canvas中填充規則了。canvas中填充有非零環繞規則奇偶規則。對於同樣的路徑,不同的規則可能會產生不同的填充區域,是使用時,注意路徑順序就好了。

相關文章