Canvas 像素级操作 超详细讲解
Canvas 像素级操作是直接读写画布上每一个像素的颜色数据,能实现滤镜、图像合成、像素检测、粒子效果、截图处理等高级功能,是 Canvas 进阶核心能力。
我会从基础原理 → 核心API → 实战代码 → 常见应用一步步讲透,新手也能完全看懂。
一、先搞懂:Canvas 像素是什么?
Canvas 画布上的每一个点,都是一个像素(Pixel)。 每个像素由 4 个通道 组成,按固定顺序存储:
- R(Red):红色 0~255
- G(Green):绿色 0~255
- B(Blue):蓝色 0~255
- A(Alpha):透明度 0~255(0=完全透明,255=完全不透明)
关键数据结构:ImageData
像素级操作的核心对象就是 ImageData,它包含:
data:Uint8ClampedArray 类型数组(无符号 8 位整数,值被钳在 0-255),存储所有像素的 RGBA 值width:画布宽度height:画布高度
像素索引计算公式(必背!)
假设画布坐标 (x, y),对应像素在 data 数组中的起始索引:
const index = (y * width + x) * 4
y * width:找到当前行第一个像素的位置+ x:找到当前行第 x 列的像素* 4:每个像素占 4 个位置(RGBA)
对应取值:
const r = data[index] // 红
const g = data[index + 1] // 绿
const b = data[index + 2] // 蓝
const a = data[index + 3] // 透明度
二、像素级操作 3 个核心 API
这三个 API 是像素操作的全部基础,必须记住:
1. 获取像素数据:getImageData(x, y, w, h)
- x:要提取的图像数据区域左上角的 x 坐标。
- y:要提取的图像数据区域左上角的 y 坐标。
- w (width):要提取的矩形区域的 宽度。
- h (height):要提取的矩形区域的 高度。
这4个参数都是必选的
从画布指定区域读取像素,返回 ImageData 对象。
// 获取整个画布的像素
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const data = imageData.data // 像素数组
2. 创建空白像素:createImageData(w, h)
创建一个全透明的空白像素数据(所有 RGBA 都是 0)。
// 创建和画布一样大的空白像素
const blankData = ctx.createImageData(canvas.width, canvas.height)
3. 绘制像素数据:putImageData(imageData, x, y)
// 精简版(3 参数,最常用)
ctx.putImageData(imageData, dx, dy)
// 完整版(7 参数,功能更强)
ctx.putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight)
参数详细解释
- imageData:像素数据对象(必须传)
- dx:画布上的目标 x 坐标(ImageData 画到画布的哪个位置)
- dy:画布上的目标 y 坐标
- dirtyX:可选,从 ImageData 里裁剪的起始 x(默认 0)
- dirtyY:可选,从 ImageData 里裁剪的起始 y(默认 0)
- dirtyWidth:可选,裁剪宽度(默认整个宽度)
- dirtyHeight:可选,裁剪高度(默认整个高度)
后 4 个参数 = 只画 ImageData 的一小块,而不是整张图
举个直观例子:
// 1. 获取整张画布像素
const imgData = ctx.getImageData(0,0,w,h)
// 只把 ImageData 里 (10,10) 位置、宽50高50的区域
// 画到画布 (0,0) 位置
ctx.putImageData(imageData, 0, 0, 10, 10, 50, 50)
//绘制整个 imageData 到画布的 (0, 0) 位置
ctx.putImageData(imgData, 0, 0);
dx, dy= 画到画布的哪个位置dirtyX/Y/W/H= 从 ImageData 里切哪一块
putImageData官方完整参数:7 个- 常用简化版:3 个(
imageData, dx, dy) - 后 4 个作用:裁剪 ImageData 局部区域绘制
- 核心价值:性能优化 + 局部像素更新
三、完整基础流程(万能模板)
所有像素操作都遵循这个固定步骤:
- 获取画布像素
- 遍历每一个像素
- 修改 RGBA 值
- 把像素重新绘制到画布
// 1. 获取画布和上下文
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
// 2. 获取整张画布的像素数据
const w = canvas.width
const h = canvas.height
const imageData = ctx.getImageData(0, 0, w, h)
const data = imageData.data
// 3. 遍历所有像素(核心!)
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
// 计算当前像素索引
const idx = (y * w + x) * 4
// 获取原颜色
const r = data[idx]
const g = data[idx+1]
const b = data[idx+2]
const a = data[idx+3]
// ============== 在这里修改像素 ==============
// 示例:把所有像素变成红色
data[idx] = 255 // R
data[idx+1] = 0 // G
data[idx+2] = 0 // B
data[idx+3] = 255 // A
}
}
// 4. 把修改后的像素画回画布
ctx.putImageData(imageData, 0, 0)
四、实战:5 个最常用像素效果(直接复制可用)
1. 灰度图(黑白效果)
原理:用亮度公式计算灰度值 gray = R*0.299 + G*0.587 + B*0.114
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = (y * w + x) * 4
const r = data[idx]
const g = data[idx+1]
const b = data[idx+2]
// 计算灰度
const gray = r * 0.299 + g * 0.587 + b * 0.114
// RGB 都设为灰度值
data[idx] = data[idx+1] = data[idx+2] = gray
}
}
gray 通常是根据原像素的亮度计算出来的,比如用加权平均:
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
这样人眼感知最自然,因为人眼对绿色最敏感,红色次之,蓝色最弱。
把 RGB 三个通道都设为同一个值 gray,这个像素就会变成灰色。原因是:
当 R=G=B 时,颜色没有色相,呈现为灰度色。
2. 反色(底片效果)
原理:每个颜色用 255 - 原值
const idx = (y * w + x) * 4
data[idx] = 255 - data[idx] // R
data[idx+1] = 255 - data[idx+1] // G
data[idx+2] = 255 - data[idx+2] // B
3. 黑白二值化(只有纯黑/纯白)
原理:设定阈值,大于阈值变白,小于变黑
const idx = (y * w + x) * 4
const gray = data[idx]*0.299 + data[idx+1]*0.587 + data[idx+2]*0.114
const threshold = 128 // 阈值
const val = gray > threshold ? 255 : 0
data[idx] = data[idx+1] = data[idx+2] = val
4. 透明度修改
const idx = (y * w + x) * 4
data[idx + 3] = 125 // 0-255,125=半透明
5. 像素化(马赛克效果)
原理:按块取色,让多个像素用同一个颜色
const size = 10 // 马赛克块大小
for (let y = 0; y < h; y += size) {
for (let x = 0; x < w; x += size) {
const idx = (y * w + x) * 4
const r = data[idx]
const g = data[idx+1]
const b = data[idx+2]
// 给整个块填充相同颜色
for (let dy = 0; dy < size; dy++) {
for (let dx = 0; dx < size; dx++) {
const i = ((y+dy) * w + (x+dx)) * 4
data[i] = r
data[i+1] = g
data[i+2] = b
}
}
}
}
性能优化技巧
问题 1:dx/dy 能不能从 1 开始?跳过 (0,0) 因为已经是要填充的颜色
理论上可以,但实际不建议:
- 只节省 1 次像素写入(3 个字节赋值)
- 却增加了每次循环的条件判断
dy === 0 ? 1 : 0 - 条件判断的开销比省下的那次写入还大
// ❌ 不推荐 - 代码变复杂,收益几乎为零
for (let dy = 0; dy < size; dy++) {
for (let dx = (dy === 0 ? 1 : 0); dx < size; dx++) {
const i = ((y+dy) * w + (x+dx)) * 4
data[i] = r; data[i+1] = g; data[i+2] = b
}
}
结论:保持从 0 开始,代码简单,性能更好。
问题 2:先遍历 x 还是先遍历 y?有区别吗?
有区别,而且很大 —— 涉及内存访问模式。
Canvas 的 ImageData.data 是行优先 (row-major) 存储:
像素 0,0 | 像素 1,0 | 像素 2,0 | ... | 像素 0,1 | 像素 1,1 | ...
相邻的 x 坐标在内存中是连续的,相邻的 y 坐标相隔很远(相隔 w * 4 字节)。
// ✅ 行优先 - 缓存友好(推荐)
for (let dy = 0; dy < size; dy++) { // 外层 y
for (let dx = 0; dx < size; dx++) { // 内层 x
const i = ((y+dy) * w + (x+dx)) * 4 // 顺序访问内存
}
}
// ❌ 列优先 - 缓存不友好(慢 2-5 倍)
for (let dx = 0; dx < size; dx++) { // 外层 x
for (let dy = 0; dy < size; dy++) { // 内层 y
const i = ((y+dy) * w + (x+dx)) * 4 // 每次跳 w*4 字节
}
}
原因:
- CPU 缓存一次加载一整行(比如 64 字节)
- 顺序访问时,后续数据已经在缓存里了
- 跳跃访问时,每次都错过缓存(cache miss),要去内存读
结论:外层循环遍历 y,内层循环遍历 x(当前代码就是正确的)。
五、高级实用技巧
1. 像素拾取(获取鼠标位置的颜色)
非常常用:点击画布获取点击点的 RGB 值
canvas.addEventListener('click', (e) => {
const x = e.offsetX
const y = e.offsetY
const imageData = ctx.getImageData(x, y, 1, 1)
const [r, g, b, a] = imageData.data
console.log('颜色:', `rgba(${r},${g},${b},${a/255})`)
})
2. 批量操作优化
遍历像素是性能敏感操作,优化技巧:
- 缓存
width/height,不要循环内读取 - 使用
let代替var - 大画布用分块处理
3. 注意:跨域图片限制
如果绘制了跨域图片,调用 getImageData() 会报错:
Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
解决方案:
- 图片服务器配置 CORS 头
- 图片标签添加
crossOrigin="anonymous"
const img = new Image()
img.crossOrigin = 'anonymous' // 关键
img.src = '图片地址'
六、像素级操作能做什么?
掌握后可以实现这些高级功能:
- 图像滤镜(复古、模糊、锐化、美颜)
- 截图/画布导出、图片水印
- 颜色拾取器、取色器
- 像素艺术、画板工具
- 图像识别预处理(人脸识别、物体检测)
- 粒子特效、像素动画
总结
- 核心公式:像素索引
index = (y * width + x) * 4 - 核心API:
getImageData→ 遍历修改 →putImageData - 每个像素:固定 RGBA 4 个通道,值 0~255
- 万能模板:获取数据 → 双层循环遍历 → 修改通道值 → 回绘画布
只要掌握索引计算和像素遍历逻辑,你就能完全操控 Canvas 的每一个像素!
源码案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
margin: 0;
padding: 0;
}
canvas {
display: block;
}
/*
<canvas> 默认是 inline 元素,inline 元素的基线对齐(baseline alignment)会在底部预留一些空间,导致页面高度超出视口。
*/
</style>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
const image = new Image();
image.src = './imgs/月之暗面.png'
// image.crossOrigin = 'anonymous';
let w = canvas.width
let h = canvas.height
image.onload = function(){
ctx.drawImage(image,0,0,w,h);
// 1.获取像素数据:getImageData(x, y, w, h)
const imageData = ctx.getImageData(0,0,w,h);
console.log(w,h, imageData, 'imageData-----------------');
// 1920 * 953 * 4 = 7,319,040
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const index = (x + y * w) * 4;
const red = imageData.data[index];
const green = imageData.data[index + 1];
const blue = imageData.data[index + 2];
const alpha = imageData.data[index + 3];
// console.log(red,green,blue,alpha, 'red,green,blue,alpha') //像素太多打印浏览器会卡死
// 案例1. 灰度图(黑白效果)
// 计算灰度
const gray = red * 0.299 + green * 0.587 + blue * 0.114
const data = imageData.data
// RGB 都设为灰度值
// data[index] = data[index+1] = data[index+2] = gray
}
}
// 4.像素化(马赛克效果)
let size = 20
for (let y = 0; y < h; y += size) {
for (let x = 0; x < w; x += size) {
const index = (x + y * w) * 4;
const red = imageData.data[index];
const green = imageData.data[index + 1];
const blue = imageData.data[index + 2];
const alpha = imageData.data[index + 3];
for (let dy = 0; dy < size; dy++) {
for (let dx = 0; dx < size; dx++) {
const index2 = (x + dx + (y + dy) * w) * 4;
imageData.data[index2] = red;
imageData.data[index2 + 1] = green;
imageData.data[index2 + 2] = blue;
}
}
}
}
// 2.把修改后的像素画回画布:putImageData(imageData, x, y)
ctx.putImageData(imageData, 0, 0)
}
</script>
</body>
</html>