使用图像

到目前为止,我们已经创建了自己的 形状应用了样式<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 相同页面上的图像的引用

使用来自其他域的图像

使用 crossorigin 属性 <img> 元素(反映在 HTMLImageElement.crossOrigin 属性中),你可以请求权限从其他域加载图像以用于你的 drawImage() 调用。如果托管域允许跨域访问图像,则图像可以在你的 Canvas 中使用,而不会污染它;否则,使用该图像将 污染 Canvas

使用其他 Canvas 元素

与普通图像一样,我们使用 document.getElementsByTagName()document.getElementById() 方法访问其他 Canvas 元素。请确保你在将其他 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。数据 URL 允许你在代码中直接以 Base64 编码的字符字符串形式完全定义一个图像。

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

数据 URL 的一个优点是生成的图像立即可用,无需再次向服务器请求。另一个潜在的优点是,它也可以在一个文件中封装所有的 CSSJavaScriptHTML 和图像,使其更容易移植到其他位置。

此方法的一些缺点是你的图像不会被缓存,而且对于较大的图像,编码后的 URL 会变得很长。

使用视频中的帧

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

js
function getMyVideo() {
  const canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    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 style="display: none;">
  <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 对象来加载它们,而是将它们作为 <img> 标签包含在我们的 HTML 源代码中,并在绘制到画布时从这些标签中检索图像。通过将 CSS 属性 display 设置为这些图像的 none,图像在页面上是隐藏的。

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

在本节的最后一个例子中,我们将构建一个小艺术画廊。画廊包含一个包含多个图像的表格。当页面加载时,将为每个图像插入一个 <canvas> 元素,并在其周围绘制一个框架。

在本例中,每个图像都有固定的宽度和高度,与在其周围绘制的框架一样。您可以增强脚本,使其使用图像的宽度和高度来使框架完美地围绕图像。

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

html
<table>
  <tr>
    <td><img src="gallery_1.jpg" /></td>
    <td><img src="gallery_2.jpg" /></td>
    <td><img src="gallery_3.jpg" /></td>
    <td><img src="gallery_4.jpg" /></td>
  </tr>
  <tr>
    <td><img src="gallery_5.jpg" /></td>
    <td><img src="gallery_6.jpg" /></td>
    <td><img src="gallery_7.jpg" /></td>
    <td><img src="gallery_8.jpg" /></td>
  </tr>
</table>
<img id="frame" src="canvas_picture_frame.png" width="132" height="150" />

这里有一些 CSS 来使外观更漂亮

css
body {
  background: 0 -100px repeat-x url(bg_gallery.png) #4f191a;
  margin: 10px;
}

img {
  display: none;
}

table {
  margin: 0 auto;
}

td {
  padding: 15px;
}

将所有内容整合在一起的是绘制带框架图像的 JavaScript 代码

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)),
    ),
  );

  // Loop through all images.
  for (const image of document.images) {
    // Don't add a canvas for the frame image
    if (image.getAttribute("id") !== "frame") {
      // Create canvas element
      const canvas = document.createElement("canvas");
      canvas.setAttribute("width", 132);
      canvas.setAttribute("height", 150);

      // Insert before the image
      image.parentNode.insertBefore(canvas, image);

      ctx = canvas.getContext("2d");

      // Draw image to canvas
      ctx.drawImage(image, 15, 20);

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

draw();

控制图像缩放行为

如前所述,缩放图像可能会导致模糊或块状伪影,这是由于缩放过程造成的。您可以使用绘图上下文的 imageSmoothingEnabled 属性来控制在上下文内缩放图像时使用图像平滑算法。默认情况下,此值为 true,这意味着在缩放时会平滑图像。