像素操作
Canvas 提供了直接访问和操作像素数据的 API。通过 getImageData()、putImageData() 和 createImageData(),你可以读取画布上的每个像素,进行修改后再写回,实现图像滤镜、颜色调整、碰撞检测等底层操作。本节将详细介绍 Canvas 的像素操作机制和实际应用。
前置知识
阅读本节前,建议先了解:合成与裁剪
基础概念
Canvas 的像素数据以 ImageData 对象形式存在,包含一个 Uint8ClampedArray 类型的 data 属性。每个像素由 4 个字节表示:R(红)、G(绿)、B(蓝)、A(透明度),取值范围 0~255。
ImageData 数据结构
像素排列(从左到右,从上到下):
[R0, G0, B0, A0, R1, G1, B1, A1, R2, G2, B2, A2, ...]
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
像素 0 像素 1获取指定像素
javascript
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// 获取像素 (x, y) 的颜色值
const index = (y * width + x) * 4;
const r = data[index]; // 红色分量
const g = data[index + 1]; // 绿色分量
const b = data[index + 2]; // 蓝色分量
const a = data[index + 3]; // 透明度分量
// 设置像素 (x, y) 的颜色
data[index] = 255; // R
data[index + 1] = 0; // G
data[index + 2] = 0; // B
data[index + 3] = 255; // AImageData API
getImageData()
getImageData(sx, sy, sw, sh) 从画布中读取指定区域的像素数据。
javascript
/**
* getImageData(sx, sy, sw, sh)
* @param {number} sx - 起始 X
* @param {number} sy - 起始 Y
* @param {number} sw - 宽度
* @param {number} sh - 高度
* @returns {ImageData}
*/
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);putImageData()
putImageData(imageData, dx, dy) 将像素数据绘制到画布上。
javascript
/**
* putImageData(imageData, dx, dy [, dirtyX, dirtyY, dirtyWidth, dirtyHeight])
* @param {ImageData} imageData - 要绘制的像素数据
* @param {number} dx - 目标 X
* @param {number} dy - 目标 Y
*/
ctx.putImageData(imageData, 0, 0);
// 部分更新(只更新脏矩形区域)
ctx.putImageData(imageData, 0, 0, 50, 50, 100, 100);createImageData()
createImageData(width, height) 创建一个空的 ImageData 对象。
javascript
// 方式 1:直接创建
const imageData = ctx.createImageData(200, 100);
// 方式 2:使用另一个 ImageData 的尺寸
const imageData2 = ctx.createImageData(existingImageData);
// 所有像素初始化为透明黑色 (0, 0, 0, 0)
const data = imageData.data;
console.log(data[0]); // 0 (R)
console.log(data[3]); // 0 (A)ImageData 属性
| 属性 | 类型 | 说明 |
|---|---|---|
width | number | 图像宽度 |
height | number | 图像高度 |
data | Uint8ClampedArray | 像素数据数组 |
colorSpace | string | 颜色空间(srgb 默认) |
像素操作实战
示例 1:灰度滤镜
javascript
/**
* 将 ImageData 转换为灰度
*/
function toGrayscale(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// 加权平均法(人眼对绿色更敏感)
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
data[i] = gray; // R
data[i + 1] = gray; // G
data[i + 2] = gray; // B
// A 保持不变
}
return imageData;
}
// 使用
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
toGrayscale(imageData);
ctx.putImageData(imageData, 0, 0);示例 2:亮度与对比度调整
javascript
/**
* 调整亮度和对比度
* @param {ImageData} imageData
* @param {number} brightness - 亮度偏移(-255 ~ 255)
* @param {number} contrast - 对比度因子(0 ~ 2,1 为原始)
*/
function adjustBrightnessContrast(imageData, brightness, contrast) {
const data = imageData.data;
const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
for (let i = 0; i < data.length; i += 4) {
for (let c = 0; c < 3; c++) {
// 先调整对比度,再调整亮度
let value = factor * (data[i + c] - 128) + 128 + brightness;
data[i + c] = Math.max(0, Math.min(255, value)); // 钳位到 0~255
}
}
return imageData;
}示例 3:模糊滤镜(盒模糊)
javascript
/**
* 盒模糊(Box Blur)
* @param {ImageData} imageData
* @param {number} radius - 模糊半径
*/
function boxBlur(imageData, radius) {
const { width, height, data } = imageData;
const copy = new Uint8ClampedArray(data); // 复制原始数据
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0, count = 0;
// 遍历周围像素
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const nx = Math.min(width - 1, Math.max(0, x + dx));
const ny = Math.min(height - 1, Math.max(0, y + dy));
const idx = (ny * width + nx) * 4;
r += copy[idx];
g += copy[idx + 1];
b += copy[idx + 2];
count++;
}
}
const idx = (y * width + x) * 4;
data[idx] = r / count;
data[idx + 1] = g / count;
data[idx + 2] = b / count;
}
}
return imageData;
}示例 4:像素级碰撞检测
javascript
/**
* 检测两个矩形区域是否有非透明像素重叠
*/
function checkPixelCollision(ctx, rect1, rect2) {
// 计算重叠区域
const overlapX = Math.max(rect1.x, rect2.x);
const overlapY = Math.max(rect1.y, rect2.y);
const overlapW = Math.min(rect1.x + rect1.w, rect2.x + rect2.w) - overlapX;
const overlapH = Math.min(rect1.y + rect1.h, rect2.y + rect2.h) - overlapY;
if (overlapW <= 0 || overlapH <= 0) return false;
const imageData = ctx.getImageData(overlapX, overlapY, overlapW, overlapH);
const data = imageData.data;
// 检查是否有任何非透明像素
for (let i = 3; i < data.length; i += 4) {
if (data[i] > 0) return true;
}
return false;
}完整示例:图像处理工具
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>像素操作</title>
<style>
body { font-family: sans-serif; padding: 20px; }
canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
.controls { margin: 10px 0; }
button { padding: 5px 12px; margin: 3px; cursor: pointer; }
</style>
</head>
<body>
<h1>Canvas 像素操作</h1>
<canvas id="pixelCanvas" width="400" height="300"></canvas>
<div class="controls">
<button onclick="applyFilter('grayscale')">灰度</button>
<button onclick="applyFilter('invert')">反色</button>
<button onclick="applyFilter('sepia')">怀旧</button>
<button onclick="applyFilter('brightness')">增亮</button>
<button onclick="applyFilter('blur')">模糊</button>
<button onclick="applyFilter('noise')">噪点</button>
<button onclick="resetCanvas()">重置</button>
</div>
<script>
const canvas = document.getElementById('pixelCanvas');
const ctx = canvas.getContext('2d');
let originalData = null;
// 绘制初始图案
function drawInitialPattern() {
// 背景渐变
const bgGrad = ctx.createLinearGradient(0, 0, 400, 300);
bgGrad.addColorStop(0, '#4285f4');
bgGrad.addColorStop(1, '#34a853');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, 400, 300);
// 绘制圆形
const radGrad = ctx.createRadialGradient(200, 150, 20, 200, 150, 100);
radGrad.addColorStop(0, '#fbbc05');
radGrad.addColorStop(1, '#ea4335');
ctx.fillStyle = radGrad;
ctx.beginPath();
ctx.arc(200, 150, 80, 0, Math.PI * 2);
ctx.fill();
// 绘制矩形
ctx.fillStyle = '#9c27b0';
ctx.fillRect(50, 50, 80, 80);
ctx.fillStyle = '#00bcd4';
ctx.fillRect(270, 50, 80, 80);
// 保存原始数据
originalData = ctx.getImageData(0, 0, 400, 300);
}
drawInitialPattern();
// 滤镜函数
const filters = {
grayscale(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
data[i] = data[i + 1] = data[i + 2] = avg;
}
},
invert(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
},
sepia(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2];
data[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
}
},
brightness(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, data[i] + 40);
data[i + 1] = Math.min(255, data[i + 1] + 40);
data[i + 2] = Math.min(255, data[i + 2] + 40);
}
},
blur(imageData) {
const { width, height, data } = imageData;
const copy = new Uint8ClampedArray(data);
const radius = 2;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0, count = 0;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const nx = Math.max(0, Math.min(width - 1, x + dx));
const ny = Math.max(0, Math.min(height - 1, y + dy));
const idx = (ny * width + nx) * 4;
r += copy[idx]; g += copy[idx + 1]; b += copy[idx + 2];
count++;
}
}
const idx = (y * width + x) * 4;
data[idx] = r / count;
data[idx + 1] = g / count;
data[idx + 2] = b / count;
}
}
},
noise(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const noise = (Math.random() - 0.5) * 60;
data[i] = Math.max(0, Math.min(255, data[i] + noise));
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
}
}
};
function applyFilter(name) {
if (!originalData) return;
// 复制原始数据
const imageData = new ImageData(
new Uint8ClampedArray(originalData.data),
originalData.width,
originalData.height
);
filters[name](imageData);
ctx.putImageData(imageData, 0, 0);
}
function resetCanvas() {
if (originalData) {
ctx.putImageData(originalData, 0, 0);
}
}
</script>
</body>
</html>注意事项
1. 跨域安全限制
javascript
// 跨域图像无法使用 getImageData
const img = new Image();
img.src = 'https://example.com/photo.jpg';
img.onload = () => {
ctx.drawImage(img, 0, 0);
try {
ctx.getImageData(0, 0, 100, 100); // 报错!
} catch (e) {
console.error('安全错误:跨域图像无法读取像素');
}
};
// 解决方案:设置 crossOrigin
img.crossOrigin = 'anonymous';2. Uint8ClampedArray 的自动钳位
javascript
const data = imageData.data;
data[0] = 300; // 自动钳位为 255
data[0] = -10; // 自动钳位为 0
console.log(data[0]); // 255 或 03. 大图像的性能
getImageData() 和 putImageData() 是昂贵的操作。对于大画布:
javascript
// 不好的做法:每帧读取全部像素
function animate() {
const data = ctx.getImageData(0, 0, 1920, 1080); // 很慢!
requestAnimationFrame(animate);
}
// 好的做法:只读取需要修改的区域
const smallData = ctx.getImageData(100, 100, 200, 200); // 只读取局部最佳实践
1. 像素操作工具库
javascript
const PixelUtils = {
/**
* 创建指定颜色的 ImageData
*/
create(width, height, r, g, b, a) {
const imageData = new ImageData(width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = a;
}
return imageData;
},
/**
* 获取指定坐标的像素
*/
getPixel(imageData, x, y) {
const { width, data } = imageData;
const index = (y * width + x) * 4;
return {
r: data[index],
g: data[index + 1],
b: data[index + 2],
a: data[index + 3]
};
},
/**
* 设置指定坐标的像素
*/
setPixel(imageData, x, y, r, g, b, a) {
const { width, data } = imageData;
const index = (y * width + x) * 4;
data[index] = r;
data[index + 1] = g;
data[index + 2] = b;
data[index + 3] = a;
},
/**
* 遍历所有像素
*/
forEach(imageData, callback) {
const { width, height, data } = imageData;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
callback({ r: data[index], g: data[index + 1], b: data[index + 2], a: data[index + 3] }, x, y);
}
}
}
};2. ImageData 性能优化技巧
| 技巧 | 说明 |
|---|---|
| 局部读取 | 只读取需要处理的区域 |
| 复制后修改 | 使用 new Uint8ClampedArray(data) 创建副本 |
| 避免频繁 putImageData | 批量修改后一次性写回 |
| 使用 TypedArray 操作 | 利用 set()、subarray() 等高效方法 |
| Web Worker | 将计算密集的像素操作放到 Worker 中 |
3. 使用 Web Worker 处理像素
javascript
// 主线程
const worker = new Worker('pixel-worker.js');
worker.postMessage({
imageData: ctx.getImageData(0, 0, 400, 300),
filter: 'grayscale'
});
worker.onmessage = (e) => {
ctx.putImageData(e.data.imageData, 0, 0);
};
// pixel-worker.js
self.onmessage = (e) => {
const { imageData, filter } = e.data;
const data = imageData.data;
if (filter === 'grayscale') {
for (let i = 0; i < data.length; i += 4) {
const avg = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
data[i] = data[i + 1] = data[i + 2] = avg;
}
}
self.postMessage({ imageData });
};下一节
继续学习:动画基础