使用 canvas 繪製形狀

既然我們已經設定好 canvas 環境,我們就可以深入瞭解如何在 canvas 上繪圖了。透過本文,你將學會如何繪製矩形、三角形、線條、弧線和曲線,熟悉一些基本圖形。在使用 canvas 繪製物件時,路徑是必不可少的,我們將瞭解如何完成這項工作。

網格

在我們開始繪圖之前,我們需要討論 canvas 網格或座標空間。上一頁的 HTML 骨架中有一個 canvas 元素,寬 150 畫素,高 150 畫素。

Canvas grid with a blue square demonstrating coordinates and axes.

通常,網格中的 1 個單位對應於 canvas 上的 1 個畫素。此網格的原點位於左上角,座標為 (0,0)。所有元素都相對於此原點放置。因此,藍色方塊左上角的座標為 (x,y),即距離左側 x 畫素,距離頂部 y 畫素。在本教程的後面,我們將看到如何將原點平移到不同的位置,旋轉網格,甚至縮放它,但現在我們將堅持使用預設設定。

繪製矩形

SVG 不同,<canvas> 只支援兩種基本形狀:矩形和路徑(由線段連線的點列表)。所有其他形狀都必須透過組合一個或多個路徑來建立。幸運的是,我們有各種路徑繪製函式,可以建立非常複雜的形狀。

首先讓我們看看矩形。有三個函式可以在 canvas 上繪製矩形

fillRect(x, y, width, height)

繪製一個填充矩形。

strokeRect(x, y, width, height)

繪製一個矩形輪廓。

clearRect(x, y, width, height)

清除指定的矩形區域,使其完全透明。

這三個函式都接受相同的引數。xy 指定矩形左上角在 canvas 上的位置(相對於原點)。widthheight 提供矩形的大小。

下面是上一頁的 draw() 函式,但現在它使用了這三個函式。

矩形形狀示例

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  ctx.fillRect(25, 25, 100, 100);
  ctx.clearRect(45, 45, 60, 60);
  ctx.strokeRect(50, 50, 50, 50);
}

此示例的輸出如下所示。

fillRect() 函式繪製一個邊長為 100 畫素的大黑色正方形。然後 clearRect() 函式從中心清除一個 60x60 畫素的正方形,然後呼叫 strokeRect() 在清除的正方形內建立一個 50x50 畫素的矩形輪廓(概念上是 50x50;實際上是 52x52,下一節將解釋)。

在接下來的頁面中,我們將看到 clearRect() 的兩種替代方法,我們還將看到如何更改渲染形狀的顏色和描邊樣式。

與我們將在下一節中看到的路徑函式不同,所有三個矩形函式都立即繪製到 canvas 上。

邊緣模糊?

在上面的矩形示例以及所有即將到來的示例中,你可能會注意到形狀的邊緣可能比用 SVG 或 CSS 繪製的等效形狀顯得模糊。這並不是因為 canvas API 無法繪製銳利的邊緣,而是因為 canvas 網格對映到螢幕上的實際畫素的方式,以及在某些情況下,因為瀏覽器縮放 canvas 的方式。如果上述示例不夠明顯,讓我們使用 CSS 放大 canvas

html
<canvas id="canvas" width="15" height="15"></canvas>
css
#canvas {
  width: 300px;
  height: 300px;
}
js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  ctx.strokeRect(2, 2, 10, 10);
  ctx.fillRect(7, 7, 1, 1);
}

在此示例中,我們建立了一個非常小的 canvas (15x15),但隨後使用 CSS 將其放大到 300x300 畫素。結果,每個 canvas 畫素現在由一個 20x20 的螢幕畫素塊表示。我們從 (2,2) 繪製一個描邊矩形到 (12,12),並從 (7,7) 繪製一個填充矩形到 (8,8)。它看起來真的很模糊。這是因為預設情況下,當瀏覽器縮放柵格影像時,它會使用平滑演算法來插值額外的畫素。這對於照片或具有捲曲邊緣的 canvas 圖形非常有用,但對於直邊形狀則不太理想。為了解決這個問題,我們可以將 image-rendering 設定為 pixelated

css
#canvas {
  image-rendering: pixelated;
}

現在,當瀏覽器縮放 canvas 時,它會盡可能地保留原始影像的畫素化。

注意: image-rendering: pixelated 作為一種保留清晰邊緣的技術並非沒有問題。當 CSS 畫素與裝置畫素不一致時(如果 devicePixelRatio 不是整數),某些畫素可能會繪製得比其他畫素大,從而導致外觀不均勻。然而,這不是一個容易解決的問題,因為當 CSS 畫素無法準確對映到裝置畫素時,不可能精確地填充裝置畫素。

但現在出現了另一個問題,你也可以在原始矩形示例中觀察到:描邊矩形不僅寬 2 畫素而不是 1 畫素,而且看起來是灰色而不是預設的黑色。這是因為座標被解釋為形狀邊界的方式。

如果再次檢視上面的網格圖,你會發現像 212 這樣的座標並不是標識一個畫素,而是標識兩個畫素之間的邊緣。在下面的影像中,網格表示 canvas 座標網格。網格線之間的方塊是螢幕上的實際畫素。在下面的第一個網格影像中,一個從 (2,1) 到 (5,5) 的矩形被填充。它們之間的整個區域(淺紅色)落在畫素邊界上,因此最終填充的矩形將具有清晰的邊緣。

Three coordinate grids. The grid lines are actual pixels on the screen. The top left corner of each grid is labeled (0,0). In the first grid, a rectangle from (2,1) to (5,5) is filled in light-red color. In the second grid, (3,1) to (3,5) is joined with a 1-pixel thick royal blue line. The royal-blue line is centered on a grid line, extends from 2.5 to 3.5 on the x access, halfway into the pixels on either side of the graph line, with a light blue background on either side extending from 2 to 4 on the x-access. To avoid the light blue blur extension of the line in the second coordinate grid, the path in, the third coordinate grid is a royal-blue from line (3.5,1) to (3.5,5). The 1 pixel line width ends up completely and precisely filling a single pixel vertical line.

如果您考慮一條從 (3,1) 到 (3,5) 的路徑,線寬為 1.0,您將遇到第二張圖片中的情況。要填充的實際區域(深藍色)只延伸到路徑兩側畫素的一半。必須渲染此區域的近似值,這意味著這些畫素僅部分著色,並導致整個區域(淺藍色和深藍色)填充的顏色僅為實際描邊顏色的一半深。這就是在上面的矩形示例中 strokeRect() 呼叫中 1.0 寬度線發生的情況。

為了解決這個問題,你必須在建立路徑時非常精確。知道 1.0 寬度的線條將向路徑兩側延伸半個單位,從畫素的中心建立路徑將導致第三張圖片中的情況——1.0 線寬最終將完全精確地填充單個畫素的垂直線。

注意:請注意,在我們的垂直線示例中,Y 位置仍然引用了一個整數網格線位置——如果不是這樣,我們會在端點處看到半覆蓋的畫素。

所以這就是我們前面說矩形示例中的 strokeRect(50, 50, 50, 50) 呼叫在概念上是 50x50,但實際上是 52x52 的原因。輪廓的實際填充區域從 (49.5, 49.5) 開始,到 (100.5, 100.5) 結束,由於部分填充的畫素,實際填充區域是從 (49,49) 到 (101,101),即 52x52,並且邊緣寬 2 畫素。要獲得一個完全 50x50 的實心 1 畫素寬輪廓,你需要將矩形縮小輪廓的厚度 (1px),並將其移動輪廓厚度的一半 (0.5px)

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  ctx.strokeRect(2.5, 2.5, 9, 9);
  ctx.fillRect(7, 7, 1, 1);
}

對於偶數寬度的線條,每一半最終都是整數個畫素,因此您需要一條位於畫素之間的路徑(即 (3,1) 到 (3,5)),而不是位於畫素中間的路徑。

儘管最初處理可伸縮 2D 圖形時會有些痛苦,但關注畫素網格和路徑位置可確保無論涉及縮放或任何其他變換,您的繪圖都看起來正確。在正確位置繪製的 1.0 寬度垂直線在放大 2 倍時將變為清晰的 2 畫素線,並出現在正確位置。

這種部分填充畫素的現象也延伸到不與畫素網格對齊的形狀。例如,考慮一個旋轉的矩形(你將在下一節學習如何繪製它)。為了看看有和沒有 image-rendering: pixelated 的效果,我們並排放置了兩個 canvas,還有一個以全尺寸繪製的 canvas,帶網格線

js
function draw(canvasId) {
  const canvas = document.getElementById(canvasId);
  const ctx = canvas.getContext("2d");
  ctx.beginPath();
  ctx.moveTo(3, 2);
  ctx.lineTo(9, 4.5);
  ctx.lineTo(6.5, 10.5);
  ctx.lineTo(0.5, 8);
  ctx.closePath();
  ctx.fill();
}

function drawFullScale() {
  const canvas = document.getElementById("canvas3");
  const ctx = canvas.getContext("2d");
  ctx.beginPath();
  ctx.moveTo(60, 40);
  ctx.lineTo(180, 90);
  ctx.lineTo(130, 210);
  ctx.lineTo(10, 160);
  ctx.closePath();
  ctx.fill();
  ctx.strokeStyle = "lightgray";
  for (let i = 0; i < 16; i++) {
    ctx.moveTo(i * 20, 0);
    ctx.lineTo(i * 20, 300);
    ctx.moveTo(0, i * 20);
    ctx.lineTo(300, i * 20);
    ctx.stroke();
  }
}

如果放大影像會使其看起來比預期更模糊,那麼縮小影像會使其看起來更清晰。例如,如果您想讓 canvas 在螢幕上顯示為 300x150 畫素,您可以將其建立為 600x300 畫素,然後使用 CSS 將其縮小。這在高畫質螢幕(例如蘋果的 Retina 顯示器)上特別有用,其中一個 CSS 畫素由多個螢幕畫素表示,因此如果您忠實地繪製一個 300x150 畫素的 canvas,它將不會具有與頁面上其他元素相同的畫素解析度。

繪製路徑

現在讓我們看看路徑。路徑是點的列表,由不同形狀的線段連線,可以是彎曲的或不彎曲的,具有不同的寬度和不同的顏色。路徑,甚至子路徑,都可以閉合。要使用路徑建立形狀,我們需要採取一些額外的步驟

  1. 首先,建立路徑。
  2. 然後使用繪圖命令在路徑中繪圖。
  3. 路徑建立後,您可以描邊或填充路徑以進行渲染。

以下是用於執行這些步驟的函式

beginPath()

建立一個新路徑。建立後,未來的繪圖命令將指向該路徑並用於構建該路徑。

路徑方法

設定物件不同路徑的方法。

closePath()

向路徑新增一條直線,連線到當前子路徑的起點。

stroke()

透過描邊輪廓來繪製形狀。

fill()

透過填充路徑內容區域繪製實心形狀。

建立路徑的第一步是呼叫 beginPath()。在內部,路徑儲存為子路徑(直線、弧線等)列表,它們共同構成一個形狀。每次呼叫此方法時,列表都會重置,我們可以開始繪製新形狀。

注意:噹噹前路徑為空時,例如在呼叫 beginPath() 之後或在新建立的 canvas 上,第一個路徑構造命令始終被視為 moveTo(),無論它實際是什麼。因此,在重置路徑後,您幾乎總是希望明確設定起始位置。

第二步是呼叫實際指定要繪製路徑的方法。我們很快就會看到這些。

第三步(可選)是呼叫 closePath()。此方法嘗試透過從當前點到起點繪製一條直線來閉合形狀。如果形狀已閉合或列表中只有一個點,則此函式不執行任何操作。

注意:當您呼叫 fill() 時,任何開放形狀都會自動閉合,因此您不必呼叫 closePath()。但當您呼叫 stroke() 時,情況並非如此

繪製三角形

例如,繪製三角形的程式碼可能看起來像這樣

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.moveTo(75, 50);
  ctx.lineTo(100, 75);
  ctx.lineTo(100, 25);
  ctx.fill();
}

結果如下所示

移動畫筆

一個非常有用的函式,它實際上不繪製任何東西,但成為上面描述的路徑列表的一部分,是 moveTo() 函式。你最好把它想象成把鋼筆或鉛筆從一張紙上的一個點抬起來,然後放到下一個點。

moveTo(x, y)

將畫筆移動到由 xy 指定的座標。

當 canvas 初始化或呼叫 beginPath() 時,您通常會希望使用 moveTo() 函式將起點放置在其他位置。我們也可以使用 moveTo() 繪製不連線的路徑。看看下面的笑臉。

要自己嘗試一下,您可以使用下面的程式碼片段。只需將其貼上到我們前面看到的 draw() 函式中即可。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // Outer circle
  ctx.moveTo(110, 75);
  ctx.arc(75, 75, 35, 0, Math.PI, false); // Mouth (clockwise)
  ctx.moveTo(65, 65);
  ctx.arc(60, 65, 5, 0, Math.PI * 2, true); // Left eye
  ctx.moveTo(95, 65);
  ctx.arc(90, 65, 5, 0, Math.PI * 2, true); // Right eye
  ctx.stroke();
}

結果如下所示

如果你想看到連線線,可以刪除呼叫 moveTo() 的行。

注意:要了解有關 arc() 函式的更多資訊,請參閱下面的弧線部分。

直線

要繪製直線,請使用 lineTo() 方法。

lineTo(x, y)

從當前繪圖位置到由 xy 指定的位置繪製一條直線。

此方法接受兩個引數,xy,它們是線段終點的座標。起點取決於先前繪製的路徑,前一個路徑的終點是後續路徑的起點,依此類推。起點也可以透過使用 moveTo() 方法進行更改。

下面的示例繪製了兩個三角形,一個填充,一個描邊。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  // Filled triangle
  ctx.beginPath();
  ctx.moveTo(25, 25);
  ctx.lineTo(105, 25);
  ctx.lineTo(25, 105);
  ctx.fill();

  // Stroked triangle
  ctx.beginPath();
  ctx.moveTo(125, 125);
  ctx.lineTo(125, 45);
  ctx.lineTo(45, 125);
  ctx.closePath();
  ctx.stroke();
}

這透過呼叫 beginPath() 開始一個新的形狀路徑。然後我們使用 moveTo() 方法將起點移動到所需位置。在這下面,繪製了兩條線,構成三角形的兩條邊。

您會注意到填充三角形和描邊三角形之間的區別。如上所述,這是因為當路徑填充時形狀會自動閉合,但當它們被描邊時則不會。如果我們遺漏了描邊三角形的 closePath(),那麼只會繪製兩條線,而不是一個完整的三角形。

弧線

要繪製弧線或圓形,我們使用 arc()arcTo() 方法。

arc(x, y, radius, startAngle, endAngle, counterclockwise)

繪製一個以 (x, y) 位置為中心,半徑為 r,從 startAngle 開始,到 endAngle 結束的弧,方向由 counterclockwise 指示(預設為順時針)。

arcTo(x1, y1, x2, y2, radius)

繪製一個帶有給定控制點和半徑的弧,透過直線連線到前一個點。

讓我們更詳細地瞭解一下 arc 方法,它接受六個引數:xy 是弧線所在圓的中心的座標。radius 不言自明。startAngleendAngle 引數以弧度定義弧線在圓曲線上的起點和終點。這些是從 x 軸測量的。counterclockwise 引數是一個布林值,當為 true 時,弧線逆時針繪製;否則,弧線順時針繪製。

注意:arc 函式中的角度以弧度而不是度數測量。要將度數轉換為弧度,您可以使用以下 JavaScript 表示式:radians = (Math.PI/180)*degrees

下面的例子比我們上面看到的例子稍微複雜一些。它繪製了 12 個具有不同角度和填充的不同弧。

這兩個 for 迴圈用於遍歷弧的行和列。對於每個弧,我們透過呼叫 beginPath() 開始一個新路徑。在程式碼中,弧的每個引數都放在一個變數中以提高畫質晰度,但您在實際生活中不一定會這樣做。

xy 座標應該足夠清楚。radiusstartAngle 是固定的。endAngle 在第一列從 180 度(半圓)開始,並以 90 度步長增加,最終在最後一列形成一個完整的圓。

clockwise 引數的語句導致第一行和第三行繪製為順時針弧,第二行和第四行繪製為逆時針弧。最後,if 語句使上半部分為描邊弧,下半部分為填充弧。

注意:此示例需要比本頁其他示例稍大的 canvas:150 x 200 畫素。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 3; j++) {
      ctx.beginPath();
      const x = 25 + j * 50; // x coordinate
      const y = 25 + i * 50; // y coordinate
      const radius = 20; // Arc radius
      const startAngle = 0; // Starting point on circle
      const endAngle = Math.PI + (Math.PI * j) / 2; // End point on circle
      const counterclockwise = i % 2 !== 0; // clockwise or counterclockwise

      ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);

      if (i > 1) {
        ctx.fill();
      } else {
        ctx.stroke();
      }
    }
  }
}

貝塞爾曲線和二次曲線

下一種可用的路徑型別是 貝塞爾曲線,有三次和二次兩種型別。這些通常用於繪製複雜的有機形狀。

quadraticCurveTo(cp1x, cp1y, x, y)

從當前畫筆位置到由 xy 指定的終點繪製二次貝塞爾曲線,使用由 cp1xcp1y 指定的控制點。

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

從當前畫筆位置到由 xy 指定的終點繪製三次貝塞爾曲線,使用由 (cp1x, cp1y) 和 (cp2x, cp2y) 指定的控制點。二次貝塞爾曲線和三次貝塞爾曲線比較。

這兩者之間的區別在於,二次貝塞爾曲線有一個起點和終點(藍色點)和一個控制點(由紅色點表示),而三次貝塞爾曲線使用兩個控制點。

這兩個方法中的 xy 引數是終點的座標。cp1xcp1y 是第一個控制點的座標,cp2xcp2y 是第二個控制點的座標。

使用二次和三次貝塞爾曲線可能相當具有挑戰性,因為與 Adobe Illustrator 等向量繪圖軟體不同,我們沒有直接的視覺反饋來了解我們正在做什麼。這使得繪製複雜形狀變得相當困難。在下面的示例中,我們將繪製一些簡單的有機形狀,但如果您有時間,最重要的是有耐心,則可以建立更復雜的形狀。

這些例子沒什麼特別難的。在這兩種情況下,我們都看到了一系列曲線的繪製,最終形成了一個完整的形狀。

二次貝塞爾曲線

此示例使用多個二次貝塞爾曲線渲染一個語音氣泡。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  // Quadratic curves example
  ctx.beginPath();
  ctx.moveTo(75, 25);
  ctx.quadraticCurveTo(25, 25, 25, 62.5);
  ctx.quadraticCurveTo(25, 100, 50, 100);
  ctx.quadraticCurveTo(50, 120, 30, 125);
  ctx.quadraticCurveTo(60, 120, 65, 100);
  ctx.quadraticCurveTo(125, 100, 125, 62.5);
  ctx.quadraticCurveTo(125, 25, 75, 25);
  ctx.stroke();
}

三次貝塞爾曲線

此示例使用三次貝塞爾曲線繪製一個心形。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  // Cubic curves example
  ctx.beginPath();
  ctx.moveTo(75, 40);
  ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
  ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
  ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
  ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
  ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
  ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
  ctx.fill();
}

矩形

除了我們在繪製矩形中看到的三種直接在畫布上繪製矩形的方法外,還有 rect() 方法,它將一個矩形路徑新增到當前開放的路徑中。

rect(x, y, width, height)

繪製一個矩形,其左上角由 (x, y) 指定,並具有指定的 widthheight

在執行此方法之前,會自動呼叫 moveTo() 方法,引數為 (x,y)。換句話說,當前畫筆位置會自動重置為預設座標。

組合

到目前為止,此頁面上的每個示例都只使用每種形狀的一種路徑函式。然而,您可以使用建立形狀的路徑的數量或型別沒有限制。因此,在這個最終示例中,讓我們結合所有路徑函式來製作一組非常著名的遊戲角色。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  roundedRect(ctx, 12, 12, 184, 168, 15);
  roundedRect(ctx, 19, 19, 170, 154, 9);
  roundedRect(ctx, 53, 53, 49, 33, 10);
  roundedRect(ctx, 53, 119, 49, 16, 6);
  roundedRect(ctx, 135, 53, 49, 33, 10);
  roundedRect(ctx, 135, 119, 25, 49, 10);

  ctx.beginPath();
  ctx.arc(37, 37, 13, Math.PI / 7, -Math.PI / 7, false);
  ctx.lineTo(31, 37);
  ctx.fill();

  for (let i = 0; i < 8; i++) {
    ctx.fillRect(51 + i * 16, 35, 4, 4);
  }

  for (let i = 0; i < 6; i++) {
    ctx.fillRect(115, 51 + i * 16, 4, 4);
  }

  for (let i = 0; i < 8; i++) {
    ctx.fillRect(51 + i * 16, 99, 4, 4);
  }

  ctx.beginPath();
  ctx.moveTo(83, 116);
  ctx.lineTo(83, 102);
  ctx.bezierCurveTo(83, 94, 89, 88, 97, 88);
  ctx.bezierCurveTo(105, 88, 111, 94, 111, 102);
  ctx.lineTo(111, 116);
  ctx.lineTo(106.333, 111.333);
  ctx.lineTo(101.666, 116);
  ctx.lineTo(97, 111.333);
  ctx.lineTo(92.333, 116);
  ctx.lineTo(87.666, 111.333);
  ctx.lineTo(83, 116);
  ctx.fill();

  ctx.fillStyle = "white";
  ctx.beginPath();
  ctx.moveTo(91, 96);
  ctx.bezierCurveTo(88, 96, 87, 99, 87, 101);
  ctx.bezierCurveTo(87, 103, 88, 106, 91, 106);
  ctx.bezierCurveTo(94, 106, 95, 103, 95, 101);
  ctx.bezierCurveTo(95, 99, 94, 96, 91, 96);
  ctx.moveTo(103, 96);
  ctx.bezierCurveTo(100, 96, 99, 99, 99, 101);
  ctx.bezierCurveTo(99, 103, 100, 106, 103, 106);
  ctx.bezierCurveTo(106, 106, 107, 103, 107, 101);
  ctx.bezierCurveTo(107, 99, 106, 96, 103, 96);
  ctx.fill();

  ctx.fillStyle = "black";
  ctx.beginPath();
  ctx.arc(101, 102, 2, 0, Math.PI * 2, true);
  ctx.fill();

  ctx.beginPath();
  ctx.arc(89, 102, 2, 0, Math.PI * 2, true);
  ctx.fill();
}

// A utility function to draw a rectangle with rounded corners.

function roundedRect(ctx, x, y, width, height, radius) {
  ctx.beginPath();
  ctx.moveTo(x, y + radius);
  ctx.arcTo(x, y + height, x + radius, y + height, radius);
  ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
  ctx.arcTo(x + width, y, x + width - radius, y, radius);
  ctx.arcTo(x, y, x, y + radius, radius);
  ctx.stroke();
}

生成的影像如下所示

我們不會詳細介紹這一點,因為它實際上出奇地簡單。最重要的事情是注意繪圖上下文上 fillStyle 屬性的使用,以及實用函式(在本例中為 roundedRect())的使用。對於您經常做的繪圖位使用實用函式非常有幫助,並且可以減少您需要的程式碼量及其複雜性。

我們將在本教程的後面更詳細地介紹 fillStyle。在這裡,我們所做的只是用它將路徑的填充顏色從預設的黑色更改為白色,然後再更改回來。

帶孔的形狀

要繪製帶孔的形狀,我們需要在繪製外部形狀時以不同的順時針方向繪製孔。我們或者順時針繪製外部形狀,逆時針繪製內部形狀;或者逆時針繪製外部形狀,順時針繪製內部形狀。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  ctx.beginPath();

  // Outer shape clockwise ⟳
  ctx.moveTo(0, 0);
  ctx.lineTo(150, 0);
  ctx.lineTo(75, 129.9);

  // Inner shape anticlockwise ↺
  ctx.moveTo(75, 20);
  ctx.lineTo(50, 60);
  ctx.lineTo(100, 60);

  ctx.fill();
}

在上面的例子中,外三角形順時針方向(移動到左上角,然後畫一條線到右上角,最後到底部),內三角形逆時針方向(移動到頂部,然後畫一條線到左下角,最後到右下角)。

Path2D 物件

正如我們在上一個示例中看到的那樣,可以有一系列路徑和繪圖命令來在 canvas 上繪製物件。為了簡化程式碼和提高效能,瀏覽器最新版本中提供的 Path2D 物件允許您快取或記錄這些繪圖命令。您可以快速回放您的路徑。讓我們看看如何構造一個 Path2D 物件

Path2D()

Path2D() 建構函式返回一個新例項化的 Path2D 物件,可選地帶另一個路徑作為引數(建立副本),或者可選地帶一個包含 SVG 路徑資料的字串。

js
new Path2D(); // empty path object
new Path2D(path); // copy from another Path2D object
new Path2D(d); // path from SVG path data

所有路徑方法,如 moveTorectarcquadraticCurveTo 等,我們上面瞭解到的,都可以在 Path2D 物件上使用。

Path2D API 還增加了一種使用 addPath 方法組合路徑的方法。這在您想從多個元件構建物件時很有用,例如。

Path2D.addPath(path [, transform])

將路徑新增到當前路徑,帶可選的變換矩陣。

Path2D 示例

在這個示例中,我們建立了一個矩形和一個圓形。兩者都儲存為 Path2D 物件,以便它們可以供以後使用。隨著新的 Path2D API 的出現,一些方法已更新,可以可選地接受 Path2D 物件以代替當前路徑。例如,這裡使用帶有路徑引數的 strokefill 將這兩個物件繪製到 canvas 上。

js
function draw() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  const rectangle = new Path2D();
  rectangle.rect(10, 10, 50, 50);

  const circle = new Path2D();
  circle.arc(100, 35, 25, 0, 2 * Math.PI);

  ctx.stroke(rectangle);
  ctx.fill(circle);
}

使用 SVG 路徑

新的 canvas Path2D API 的另一個強大功能是使用 SVG 路徑資料來初始化 canvas 上的路徑。這可能允許您在 SVG 和 canvas 中傳遞和重用路徑資料。

路徑將移動到點 (M10 10),然後水平向右移動 80 個點 (h 80),然後向下移動 80 個點 (v 80),然後向左移動 80 個點 (h -80),然後回到起點 (z)。您可以在 Path2D 建構函式頁面上檢視此示例。

js
const p = new Path2D("M10 10 h 80 v 80 h -80 Z");