使用 canvas 进行像素操作
到目前为止,我们还没有关注 Canvas 的实际像素。使用 ImageData 对象,您可以直接读取和写入一个数据数组来操作像素数据。我们还将研究如何控制图像平滑(抗锯齿),以及如何从 Canvas 保存图像。
ImageData 对象
ImageData 对象表示 Canvas 对象区域的底层像素数据。其 data 属性返回一个 Uint8ClampedArray(如果请求,则为 Float16Array),可以访问该数组来查看原始像素数据;每个像素由四个单字节值(按顺序为红、绿、蓝和 Alpha,即“RGBA”格式)表示。每种颜色分量都表示为一个介于 0 和 255 之间的整数。每个分量在数组中被分配一个连续的索引,其中左上角像素的红色分量位于数组索引 0 处。然后,像素从左到右,再向下,依次填充整个数组。
Uint8ClampedArray 包含 height × width × 4 字节的数据,索引值范围从 0 到 (height × width × 4) - 1。
例如,要读取图像中位于第 50 行、第 200 列像素的蓝色分量值,您需要执行以下操作:
const blueComponent = imageData.data[50 * (imageData.width * 4) + 200 * 4 + 2];
如果给定一组坐标(X 和 Y),您可能会执行类似以下操作:
const xCoord = 50;
const yCoord = 100;
const canvasWidth = 1024;
const getColorIndicesForCoord = (x, y, width) => {
const red = y * (width * 4) + x * 4;
return [red, red + 1, red + 2, red + 3];
};
const colorIndices = getColorIndicesForCoord(xCoord, yCoord, canvasWidth);
const [redIndex, greenIndex, blueIndex, alphaIndex] = colorIndices;
您还可以通过读取 Uint8ClampedArray.length 属性来访问像素数组的大小(以字节为单位)。
const numBytes = imageData.data.length;
创建 ImageData 对象
要创建一个新的、空的 ImageData 对象,您应该使用 createImageData() 方法。createImageData() 方法有两个版本:
const myImageData = ctx.createImageData(width, height);
这会创建一个具有指定尺寸的新 ImageData 对象。所有像素都预设为透明。
您还可以创建一个尺寸与 anotherImageData 指定的对象相同的 ImageData 对象。新对象的所有像素都预设为透明黑色。这不会复制图像数据!
const myImageData = ctx.createImageData(anotherImageData);
获取 Canvas 上下文的像素数据
要获取包含 Canvas 上下文像素数据副本的 ImageData 对象,您可以使用 getImageData() 方法。
const myImageData = ctx.getImageData(left, top, width, height);
此方法返回一个 ImageData 对象,该对象表示 Canvas 区域的像素数据,该区域的角点由以下点表示:(left, top)、(left+width, top)、(left, top+height)和(left+width, top+height)。坐标以 Canvas 坐标空间单位指定。
注意: 任何 Canvas 外部的像素将在生成的 ImageData 对象中返回为透明黑色。
本文档 使用 Canvas 操作视频 也演示了此方法。
创建颜色选择器
在此示例中,我们使用 getImageData() 方法来显示鼠标光标下的颜色。为此,我们需要鼠标的当前位置,然后查找 getImageData() 提供的像素数组中该位置的像素数据。最后,我们使用数组数据设置 <div> 的背景颜色和文本来显示颜色。单击图像将执行相同的操作,但使用选定的颜色。
<table>
<thead>
<tr>
<th>Source</th>
<th>Hovered color</th>
<th>Selected color</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<canvas id="canvas" width="300" height="227"></canvas>
</td>
<td class="color-cell" id="hovered-color"></td>
<td class="color-cell" id="selected-color"></td>
</tr>
</tbody>
</table>
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "/shared-assets/images/examples/rhino.jpg";
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
img.addEventListener("load", () => {
ctx.drawImage(img, 0, 0);
img.style.display = "none";
});
const hoveredColor = document.getElementById("hovered-color");
const selectedColor = document.getElementById("selected-color");
const pick = (event, destination) => {
const bounding = canvas.getBoundingClientRect();
const x = event.clientX - bounding.left;
const y = event.clientY - bounding.top;
const pixel = ctx.getImageData(x, y, 1, 1);
const data = pixel.data;
const rgbColor = `rgb(${data[0]} ${data[1]} ${data[2]} / ${data[3] / 255})`;
destination.style.background = rgbColor;
destination.textContent = rgbColor;
return rgbColor;
};
canvas.addEventListener("mousemove", (event) => pick(event, hoveredColor));
canvas.addEventListener("click", (event) => pick(event, selectedColor));
将光标悬停在图像上的任何位置,即可在“悬停颜色”列中看到结果。单击图像中的任何位置,即可在“选中颜色”列中看到结果。
将像素数据绘制到 Canvas 上下文
您可以使用 putImageData() 方法将像素数据绘制到 Canvas 上下文中。
ctx.putImageData(myImageData, dx, dy);
dx 和 dy 参数指示要在 Canvas 上下文中绘制您希望绘制的像素数据的左上角的设备坐标。
例如,要将 myImageData 表示的整个图像绘制到 Canvas 上下文的左上角,您可以执行以下操作:
ctx.putImageData(myImageData, 0, 0);
灰度化和反色
在此示例中,我们遍历所有像素以更改它们的值,然后使用 putImageData() 将修改后的像素数组放回 Canvas。invert 函数将每种颜色从最大值 255 中减去。grayscale 函数使用红色、绿色和蓝色的平均值。您也可以使用加权平均值,例如,由公式 x = 0.299r + 0.587g + 0.114b 给出。有关更多信息,请参阅 Wikipedia 上的 灰度。
<canvas id="canvas" width="300" height="227"></canvas>
<form>
<input type="radio" id="original" name="color" value="original" checked />
<label for="original">Original</label>
<input type="radio" id="grayscale" name="color" value="grayscale" />
<label for="grayscale">Grayscale</label>
<input type="radio" id="inverted" name="color" value="inverted" />
<label for="inverted">Inverted</label>
<input type="radio" id="sepia" name="color" value="sepia" />
<label for="sepia">Sepia</label>
</form>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "/shared-assets/images/examples/rhino.jpg";
img.onload = () => {
ctx.drawImage(img, 0, 0);
};
const original = () => {
ctx.drawImage(img, 0, 0);
};
const invert = () => {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // red
data[i + 1] = 255 - data[i + 1]; // green
data[i + 2] = 255 - data[i + 2]; // blue
}
ctx.putImageData(imageData, 0, 0);
};
const grayscale = () => {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // red
data[i + 1] = avg; // green
data[i + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
};
const sepia = () => {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
let r = data[i], // red
g = data[i + 1], // green
b = data[i + 2]; // blue
data[i] = Math.min(Math.round(0.393 * r + 0.769 * g + 0.189 * b), 255);
data[i + 1] = Math.min(Math.round(0.349 * r + 0.686 * g + 0.168 * b), 255);
data[i + 2] = Math.min(Math.round(0.272 * r + 0.534 * g + 0.131 * b), 255);
}
ctx.putImageData(imageData, 0, 0);
};
const inputs = document.querySelectorAll("[name=color]");
for (const input of inputs) {
input.addEventListener("change", (evt) => {
switch (evt.target.value) {
case "inverted":
return invert();
case "grayscale":
return grayscale();
case "sepia":
return sepia();
default:
return original();
}
});
}
单击不同的选项以查看实际效果。
缩放和抗锯齿
借助 drawImage() 方法、第二个 Canvas 和 imageSmoothingEnabled 属性,我们可以放大图片并查看细节。还绘制了一个没有 imageSmoothingEnabled 的第三个 Canvas,以便进行并排比较。
<table>
<thead>
<tr>
<th>Source</th>
<th>Smoothing enabled = true</th>
<th>Smoothing enabled = false</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<canvas id="canvas" width="300" height="227"></canvas>
</td>
<td>
<canvas id="smoothed" width="200" height="200"></canvas>
</td>
<td>
<canvas id="pixelated" width="200" height="200"></canvas>
</td>
</tr>
</tbody>
</table>
我们获取鼠标的位置,并裁剪图像,从左边和上方裁剪 5 像素,到右边和下方裁剪 5 像素。然后,我们将它复制到另一个 Canvas 上,并将图像缩放到我们想要的大小。在缩放 Canvas 中,我们将原始 Canvas 的 10x10 像素裁剪区域缩放到 200x200。
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "/shared-assets/images/examples/rhino.jpg";
img.onload = () => {
draw(img);
};
function draw(image) {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
const smoothCtx = document.getElementById("smoothed").getContext("2d");
smoothCtx.imageSmoothingEnabled = true;
const pixelatedCtx = document.getElementById("pixelated").getContext("2d");
pixelatedCtx.imageSmoothingEnabled = false;
const zoom = (ctx, x, y) => {
ctx.drawImage(
canvas,
Math.min(Math.max(0, x - 5), image.width - 10),
Math.min(Math.max(0, y - 5), image.height - 10),
10,
10,
0,
0,
200,
200,
);
};
canvas.addEventListener("mousemove", (event) => {
const x = event.layerX;
const y = event.layerY;
zoom(smoothCtx, x, y);
zoom(pixelatedCtx, x, y);
});
}
保存图像
HTMLCanvasElement 提供了一个 toDataURL() 方法,这在保存图像时非常有用。它返回一个 data URL,其中包含由 type 参数(默认为 PNG)指定的格式的图像表示。返回的图像分辨率为 96 dpi。
注意: 请注意,如果 Canvas 包含任何未通过 CORS 获得但源自另一个 源 的像素,则 Canvas 将被污染,其内容将无法再被读取和保存。请参阅 安全和被污染的 Canvas。
canvas.toDataURL('image/png')-
默认设置。创建 PNG 图像。
canvas.toDataURL('image/jpeg', quality)-
创建 JPG 图像。可选地,您可以提供一个介于 0 到 1 之间的质量值,其中 1 是最佳质量,0 几乎无法识别但文件大小很小。
一旦您从 Canvas 生成了 data URL,您就可以将其用作任何 的源,或者将其放入带有 download 属性 的超链接中,以便将其保存到磁盘。
您还可以从 Canvas 创建一个 Blob。
canvas.toBlob(callback, type, encoderOptions)-
创建表示 Canvas 中图像的
Blob对象。