使用 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");