使用图像
到目前为止,我们已经创建了自己的 形状 并 应用了样式。 <canvas>
的一个更令人兴奋的功能是能够使用图像。这些图像可以用于动态照片合成或作为图形的背景,用于游戏中的精灵等等。外部图像可以使用浏览器支持的任何格式,例如 PNG、GIF 或 JPEG。你甚至可以使用同一页面上其他 Canvas 元素生成的图像作为来源!
将图像导入 Canvas 本质上是一个两步过程
- 获取对
HTMLImageElement
对象或其他 Canvas 元素作为源的引用。也可以通过提供 URL 来使用图像。 - 使用
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()
来检索该特定图像
使用来自其他域的图像
使用 crossorigin
属性 <img>
元素(反映在 HTMLImageElement.crossOrigin
属性中),你可以请求权限从其他域加载图像以用于你的 drawImage()
调用。如果托管域允许跨域访问图像,则图像可以在你的 Canvas 中使用,而不会污染它;否则,使用该图像将 污染 Canvas。
使用其他 Canvas 元素
与普通图像一样,我们使用 document.getElementsByTagName()
或 document.getElementById()
方法访问其他 Canvas 元素。请确保你在将其他 Canvas 元素用作目标 Canvas 的图像源之前已经绘制了一些内容到源 Canvas 上。
其中一个比较实用的用法是使用第二个 Canvas 元素作为其他较大 Canvas 的缩略图视图。
从头开始创建图像
另一个选择是在我们的脚本中创建新的 HTMLImageElement
对象。为此,我们有一个方便的 Image()
构造函数
const img = new Image(); // Create new img element
img.src = "myImage.png"; // Set source path
当此脚本执行时,图像开始加载,但如果你在图像加载完成之前尝试调用 drawImage()
,它将不会执行任何操作。旧版浏览器甚至可能会抛出异常,因此你需要确保使用 load 事件,这样你才能在图像准备就绪之前将其绘制到 Canvas 上
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()
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 编码的字符字符串形式完全定义一个图像。
const img = new Image(); // Create new img element
img.src =
"data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==";
数据 URL 的一个优点是生成的图像立即可用,无需再次向服务器请求。另一个潜在的优点是,它也可以在一个文件中封装所有的 CSS、JavaScript、HTML 和图像,使其更容易移植到其他位置。
此方法的一些缺点是你的图像不会被缓存,而且对于较大的图像,编码后的 URL 会变得很长。
使用视频中的帧
你也可以使用由 <video>
元素呈现的视频中的帧(即使该视频不可见)。例如,如果你有一个 ID 为“myVideo”的 <video>
元素,你可以这样做
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 的左上角。
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)
-
这添加了
width
和height
参数,它们指示在将图像绘制到 Canvas 上时要将其缩放到的尺寸。
示例:平铺图像
在这个示例中,我们将使用一个图像作为壁纸,并在 Canvas 上重复几次。这是通过循环并在不同位置放置缩放后的图像来完成的。在下面的代码中,第一个 for
循环遍历行。第二个 for
循环遍历列。图像被缩放到其原始尺寸的三分之一,即 50x38 像素。
注意:缩放图像时,如果放大可能会变得模糊,如果缩小太多可能会变得颗粒状。如果你要在图像中保留一些需要保持清晰可见的文本,最好不要进行缩放。
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
),宽度和高度为sWidth
和sHeight
,并将其绘制到 Canvas 上,将其放置在 Canvas 上的 (dx
,dy
) 处,并将其缩放为dWidth
和dHeight
指定的大小,同时保持其 纵横比。
为了真正理解这在做什么,看看这幅图像可能会有所帮助
前四个参数定义源图像上切片的定位和尺寸。后四个参数定义在目标 Canvas 上绘制图像的矩形。
当您想要制作合成图像时,切片可以是一个有用的工具。您可以将所有元素都放在一个图像文件中,并使用此方法合成完整的绘图。例如,如果您想制作图表,您可以将所有必要的文本放在一个 PNG 图像文件中,并且根据您的数据,您可以相当容易地改变图表的大小。另一个优点是,您无需单独加载每个图像,这可以提高加载性能。
示例:将图像框起来
在本例中,我们将使用与上一例相同的犀牛,但我们将切出它的头部并将其合成到一个相框中。相框图像是一个 24 位 PNG,它包含一个阴影。由于 24 位 PNG 图像包含完整的 8 位 alpha 通道,与 GIF 和 8 位 PNG 图像不同,它可以放置在任何背景上,而不必担心遮罩颜色。
<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>
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()
是父节点(表格单元格)的元素(图像)的方法,我们希望在其之前插入新的节点(画布元素)。
<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 来使外观更漂亮
body {
background: 0 -100px repeat-x url(bg_gallery.png) #4f191a;
margin: 10px;
}
img {
display: none;
}
table {
margin: 0 auto;
}
td {
padding: 15px;
}
将所有内容整合在一起的是绘制带框架图像的 JavaScript 代码
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
,这意味着在缩放时会平滑图像。