使用图像

到目前为止,我们已经创建了自己的形状并对其应用了样式<canvas> 的一个更令人兴奋的功能是能够使用图像。这些图像可用于动态照片合成、图表背景、游戏中的精灵等。浏览器支持的任何格式的外部图像都可以使用,例如 PNG、GIF 或 JPEG。您甚至可以使用同一页面上其他 canvas 元素生成的图像作为源!

将图像导入 canvas 基本上是一个两步过程

  1. 获取对 HTMLImageElement 对象或另一个 canvas 元素作为源的引用。也可以通过提供 URL 来使用图像。
  2. 使用 drawImage() 函数在 canvas 上绘制图像。

让我们来看看如何做到这一点。

获取要绘制的图像

Canvas API 可以使用以下任何数据类型作为图像源

HTMLImageElement

这些是使用 Image() 构造函数创建的图像,以及任何 <img> 元素。

SVGImageElement

这些是使用 <image> 元素嵌入的图像。

HTMLVideoElement

使用 HTML <video> 元素作为图像源可以捕获视频的当前帧并将其用作图像。

HTMLCanvasElement

您可以使用另一个 <canvas> 元素作为图像源。

ImageBitmap

位图图像,最终会被裁剪。这类图像用于从较大的图像中提取一部分图像,即一个精灵

OffscreenCanvas

一种特殊的 <canvas>,它不会显示,并且在不显示的情况下进行准备。使用此类图像源允许在不向用户显示内容组合的情况下切换到它。

VideoFrame

表示视频单个帧的图像。

有几种方法可以获取用于 canvas 的图像。

使用同一页面上的图像

我们可以通过使用以下任何一种方法来获取 canvas 同一页面上图像的引用

  • document.images 集合
  • document.getElementsByTagName() 方法
  • 如果您知道要使用的特定图像的 ID,则可以使用 document.getElementById() 来检索该特定图像。

使用其他域的图像

使用 <img> 元素的 crossorigin 属性(由 HTMLImageElement.crossOrigin 属性反映),您可以请求允许从另一个域加载图像以用于 drawImage() 调用。如果托管域允许跨域访问图像,则可以在 canvas 中使用该图像而不会使其“污染”;否则,使用该图像将污染 canvas

使用其他 canvas 元素

与普通图像一样,我们通过使用 document.getElementsByTagName()document.getElementById() 方法来访问其他 canvas 元素。请确保在使用源 canvas 绘制了某些内容之后,再将其用于目标 canvas。

其中一个更实用的用途是使用第二个 canvas 元素作为较大 canvas 的缩略图视图。

从头创建图像

另一个选择是在我们的脚本中创建新的 HTMLImageElement 对象。为此,我们方便地使用了 Image() 构造函数。

js
const img = new Image(); // Create new img element
img.src = "myImage.png"; // Set source path

当此脚本执行时,图像开始加载,但如果您尝试在图像加载完成之前调用 drawImage(),它将什么也不做。旧的浏览器甚至可能抛出异常,因此您需要确保使用 load 事件,这样就不会在图像准备好之前将其绘制到 canvas 上。

js
const ctx = document.getElementById("canvas").getContext("2d");
const img = new Image();

img.addEventListener("load", () => {
  ctx.drawImage(img, 0, 0);
});

img.src = "myImage.png";

如果您使用的是一个外部图像,这可能是一个不错的方法,但一旦您想要使用许多图像或延迟加载资源,您可能需要在绘制到 canvas 之前等待所有文件都可用。下面处理多个图像的示例使用了一个异步函数和 Promise.all 来等待所有图像加载完毕,然后才调用 drawImage()

js
async function draw() {
  // Wait for all images to be loaded:
  await Promise.all(
    Array.from(document.images).map(
      (image) =>
        new Promise((resolve) => image.addEventListener("load", resolve)),
    ),
  );

  const ctx = document.getElementById("canvas").getContext("2d");
  // call drawImage() as usual
}
draw();

通过 data: URL 嵌入图像

包含图像的另一种可能方法是通过 data: URL。Data URL 允许您将图像完全定义为 Base64 编码的字符字符串,直接放在您的代码中。

js
const img = new Image(); // Create new img element
img.src =
  "data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==";

Data URL 的一个优点是生成的图像可以立即获得,无需与服务器进行另一轮通信。另一个潜在的优点是,您还可以将所有的 CSSJavaScriptHTML 和图像封装在一个文件中,使其更易于移植到其他位置。

此方法的缺点是图像未被缓存,并且对于较大的图像,编码的 URL 可能会变得非常长。

使用视频帧

您还可以使用 <video> 元素呈现的视频的帧(即使视频不可见)。例如,如果您有一个 ID 为“myVideo”的 <video> 元素,您可以这样做。

js
function getMyVideo() {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  return document.getElementById("myVideo");
}

这会返回视频的 HTMLVideoElement 对象,正如前面所讨论的,该对象可以用作 canvas 的图像源。

绘制图像

一旦我们获得了源图像对象的引用,我们就可以使用 drawImage() 方法将其渲染到 canvas 上。正如我们稍后将看到的,drawImage() 方法是重载的,并且有几种变体。在其最基本的形式中,它看起来像这样:

drawImage(image, x, y)

在坐标 (x, y) 处绘制由 image 参数指定的图像。

注意: SVG 图像必须在根 <svg> 元素中指定宽度和高度。

示例:一个简单的折线图

在下面的示例中,我们将使用外部图像作为简单折线图的背景。使用背景可以使您的脚本显著变小,因为我们可以避免编写生成背景的代码。在此示例中,我们只使用一个图像,因此我使用图像对象的 load 事件处理程序来执行绘图语句。drawImage() 方法将背景放置在坐标 (0, 0) 处,这是 canvas 的左上角。

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");
  const img = new Image();
  img.onload = () => {
    ctx.drawImage(img, 0, 0);
    ctx.beginPath();
    ctx.moveTo(30, 96);
    ctx.lineTo(70, 66);
    ctx.lineTo(103, 76);
    ctx.lineTo(170, 15);
    ctx.stroke();
  };
  img.src = "backdrop.png";
}

draw();

生成的图表如下所示:

缩放

drawImage() 方法的第二个变体添加了两个新参数,允许我们在 canvas 上放置缩放后的图像。

drawImage(image, x, y, width, height)

这添加了 widthheight 参数,它们指定了将图像绘制到 canvas 上时要缩放到的尺寸。

示例:平铺图像

在此示例中,我们将使用一张图像作为壁纸,并将其在 canvas 上重复多次。这是通过循环并将缩放后的图像放置在不同位置来完成的。在下面的代码中,第一个 for 循环遍历行。第二个 for 循环遍历列。图像被缩放到其原始大小的三分之一,即 50x38 像素。

注意: 图像在放大或缩小过多时可能会变得模糊或颗粒状。如果您要在图像中包含需要保持清晰可读的文本,则不建议进行缩放。

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");
  const img = new Image();
  img.onload = () => {
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 3; j++) {
        ctx.drawImage(img, j * 50, i * 38, 50, 38);
      }
    }
  };
  img.src = "https://mdn.github.io/shared-assets/images/examples/rhino.jpg";
}

draw();

生成的 canvas 如下所示:

切片

drawImage() 方法的第三个也是最后一个变体,除了图像源之外,还有八个参数。它允许我们从源图像中剪切出一部分,然后将其缩放并绘制到我们的 canvas 上。

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

给定一个 image,此函数将源图像中由以 (sx, sy) 为左上角、宽度和高度分别为 sWidthsHeight 的矩形指定的区域,将其绘制到 canvas 上,放置在 canvas 的 (dx, dy) 位置,并将其缩放到由 dWidthdHeight 指定的大小,同时保持其纵横比

为了真正理解这一点,看看这张图可能会有帮助:

The rectangular source image top left coordinates are sx and sy with a width and height of sWidth and sHeight respectively. The source image is translated to the destination canvas where the top-left corner coordinates are dx and dy, with a width and height of dWidth and dHeight respectively.

前四个参数定义了源图像上切片的**位置和大小**。后四个参数定义了在目标 canvas 上绘制图像的**矩形**。

当您想要进行合成时,切片可能是一个有用的工具。您可以将所有元素放在单个图像文件中,并使用此方法合成完整的绘图。例如,如果您想制作图表,您可以有一个包含所有必需文本的 PNG 图像文件,并且根据您的数据,可以相当容易地更改图表的比例。另一个优点是您不必单独加载每个图像,这可以提高加载性能。

示例:为图像添加边框

在此示例中,我们将使用与上一个示例中相同的犀牛,但我们将切出它的头部并将其合成到相框中。相框图像是 24 位 PNG,包含阴影。因为 24 位 PNG 图像包含完整的 8 位 alpha 通道,与 GIF 和 8 位 PNG 图像不同,它可以放在任何背景上,而无需担心背景色。

html
<canvas id="canvas" width="150" height="150"></canvas>
<div class="hidden">
  <img
    id="source"
    src="https://mdn.github.io/shared-assets/images/examples/rhino.jpg"
    width="300"
    height="227" />
  <img id="frame" src="canvas_picture_frame.png" width="132" height="150" />
</div>
js
async function draw() {
  // Wait for all images to be loaded.
  await Promise.all(
    Array.from(document.images).map(
      (image) =>
        new Promise((resolve) => image.addEventListener("load", resolve)),
    ),
  );

  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");

  // Draw slice
  ctx.drawImage(
    document.getElementById("source"),
    33,
    71,
    104,
    124,
    21,
    20,
    87,
    104,
  );

  // Draw frame
  ctx.drawImage(document.getElementById("frame"), 0, 0);
}

draw();

这次我们采取了不同的方法来加载图像。我们没有通过创建新的 HTMLImageElement 对象来加载它们,而是将它们包含在 HTML 源的 <img> 标签中,并在绘制到 canvas 时从这些标签中检索图像。通过将这些图像的 CSS 属性 display 设置为 none,使它们在页面上隐藏。

每个 <img> 都被赋予了一个 ID 属性,因此我们有一个用于 source,一个用于 frame,这使得使用 document.getElementById() 很容易选择它们。我们使用 Promise.all 来等待所有图像加载完毕,然后才调用 drawImage()drawImage() 从第一个图像中切出犀牛,并将其缩放到 canvas 上。最后,我们使用第二个 drawImage() 调用绘制相框。

在本章的最后一个示例中,我们将构建一个小型艺术画廊。画廊由一个包含几张图像的表格组成。页面加载时,会为每个图像插入一个 <canvas> 元素,并在其周围绘制一个边框。

在这种情况下,每个图像都有固定的宽度和高度,围绕它们绘制的边框也是如此。您可以增强脚本,使其使用图像的宽度和高度来使边框完美地适合它们。

在下面的代码中,我们使用 Promise.all 来等待所有图像加载完毕,然后才将任何图像绘制到 canvas 上。我们循环遍历 document.images 容器,并为每个图像添加新的 canvas 元素。另一件需要注意的事情是 Node.insertBefore 方法的使用。insertBefore() 是父节点(表格单元格)的一个方法,它在我们希望插入新节点(canvas 元素)的元素(图像)之前进行插入。

html

以下是一些 CSS,可以让效果更美观:

css

将所有内容连接起来的是绘制带边框图像的 JavaScript:

js

控制图像缩放行为

如前所述,由于缩放过程,缩放图像可能会导致出现模糊或块状伪影。您可以使用绘图上下文的 imageSmoothingEnabled 属性来控制缩放图像时图像平滑算法的使用。默认情况下,此值为 true,表示图像在缩放时会被平滑处理。