使用 canvas 绘制图形

现在我们已经设置好了我们的 canvas 环境,我们可以深入了解如何在 canvas 上绘制。在本文结束时,您将学习如何绘制矩形、三角形、线条、弧线和曲线,从而熟悉一些基本形状。在 canvas 上绘制对象时,处理路径至关重要,我们将了解如何做到这一点。

网格

在开始绘制之前,我们需要讨论 canvas 网格或 **坐标空间**。我们上一页中的 HTML 骨架包含一个 150 像素宽、150 像素高的 canvas 元素。

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");
  if (canvas.getContext) {
    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 像素的矩形轮廓。

在接下来的页面中,我们将看到 clearRect() 的两种替代方法,我们还将了解如何更改渲染形状的颜色和笔触样式。

与我们将在下一节中看到的路径函数不同,所有三个矩形函数都会立即绘制到 canvas 上。

绘制路径

现在让我们看看路径。路径是由线段连接的点列表,这些线段可以是不同的形状,弯曲或不弯曲,具有不同的宽度和不同的颜色。路径甚至子路径都可以闭合。要使用路径创建形状,我们需要采取一些额外的步骤

  1. 首先,您创建路径。
  2. 然后,您使用 绘图命令 在路径中绘制。
  3. 创建路径后,您可以描边或填充路径以将其渲染。

以下是用于执行这些步骤的函数

beginPath()

创建一个新的路径。创建后,未来的绘图命令将被定向到路径中,并用于构建路径。

路径方法

设置对象不同路径的方法。

closePath()

向路径添加一条直线,到当前子路径的起点。

stroke()

通过描边其轮廓来绘制形状。

fill()

通过填充路径的内容区域来绘制实心形状。

创建路径的第一步是调用 beginPath()。在内部,路径存储为子路径(线条、弧线等)列表,这些子路径共同构成一个形状。每次调用此方法时,列表都会重置,我们可以开始绘制新形状。

注意:当当前路径为空时,例如在调用 beginPath() 之后或在新建的 canvas 上,第一个路径构造命令始终被视为 moveTo(),无论它实际上是什么。因此,您几乎总是希望在重置路径后专门设置起始位置。

第二步是调用实际指定要绘制的路径的方法。我们很快就会看到这些。

第三步(可选)是调用 closePath()。此方法尝试通过从当前点到起点的绘制直线来闭合形状。如果形状已经闭合或列表中只有一个点,则此函数不执行任何操作。

注意:当您调用 fill() 时,任何开放形状都会自动闭合,因此您不必调用 closePath()。当您调用 stroke() 时,情况**并非**如此。

绘制三角形

例如,绘制三角形的代码如下所示

js
function draw() {
  const canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    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");
  if (canvas.getContext) {
    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");
  if (canvas.getContext) {
    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 语句使上半部分为描边弧线,下半部分为填充弧线。

注意:此示例需要比本页其他示例稍大的画布:150 x 200 像素。

js
function draw() {
  const canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    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");
  if (canvas.getContext) {
    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");
  if (canvas.getContext) {
    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 的矩形。

在执行此方法之前,会自动使用参数 (x,y) 调用 moveTo() 方法。换句话说,当前笔位置会自动重置为默认坐标。

组合

到目前为止,本页上的每个示例每个形状都只使用了一种路径函数。但是,用于创建形状的路径数量或类型没有限制。因此,在这个最后的示例中,让我们组合所有路径函数来制作一组非常著名的游戏角色。

js
function draw() {
  const canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    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");
  if (canvas.getContext) {
    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 对象

正如我们在最后一个示例中看到的,可以有一系列路径和绘图命令将对象绘制到画布上。为了简化代码并提高性能,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 与路径参数一起使用以将两个对象绘制到画布上。

js
function draw() {
  const canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    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 路径

新的画布 Path2D API 的另一个强大功能是使用 SVG 路径数据 来初始化画布上的路径。这可能允许您传递路径数据并在 SVG 和画布中重复使用它们。

路径将移动到点 (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");