ImageData与像素管线
ImageData 是 CPU 上的 RGBA 缓冲(默认 8bit、非预乘、sRGB 或 display-p3 取决于 context)。它不是场景图,也不是 GPU 纹理句柄。一旦你把它放进热路径,整张 2D 表面的架构就被钉在软件光栅上。
1. 三条读写路径
| API | 方向 | 同步 | 典型用途 |
|---|---|---|---|
getImageData(x,y,w,h) | GPU/表面 → CPU | 是 | 拾色、碰撞遮罩、滤镜 |
putImageData(data,x,y) | CPU → 表面 | 是 | 写回处理结果;忽略 CTM |
createImageData / new ImageData | 分配 | 是 | 自己填像素再 put |
putImageData 不走 globalAlpha、shadow、filter、CTM。它按 backing store 像素对齐硬贴。这就是「滤镜预览对不齐绘制层」的根因:绘制层 scale(dpr),put 却按设备像素。
export function readUserRect(
ctx: CanvasRenderingContext2D,
dpr: number,
cssX: number,
cssY: number,
cssW: number,
cssH: number,
): ImageData {
const x = Math.round(cssX * dpr);
const y = Math.round(cssY * dpr);
const w = Math.max(1, Math.round(cssW * dpr));
const h = Math.max(1, Math.round(cssH * dpr));
return ctx.getImageData(x, y, w, h);
}
export function writeUserRect(
ctx: CanvasRenderingContext2D,
dpr: number,
data: ImageData,
cssX: number,
cssY: number,
): void {
ctx.putImageData(data, Math.round(cssX * dpr), Math.round(cssY * dpr));
}
dirty 版 putImageData(data, dx, dy, dirtyX, dirtyY, dirtyW, dirtyH) 只上传脏区,处理型表面值得用。
2. 像素布局
export function sampleRgba(
data: ImageData,
px: number,
py: number,
): readonly [number, number, number, number] {
if (px < 0 || py < 0 || px >= data.width || py >= data.height) {
throw new RangeError("pixel out of bounds");
}
const i = (py * data.width + px) * 4;
const buf = data.data;
return [buf[i]!, buf[i + 1]!, buf[i + 2]!, buf[i + 3]!];
}
- 行主序,每像素 4 byte,
R,G,B,A,A=0 时 RGB 仍可能非 0(非预乘)。自己做混合要按非预乘公式,或先预乘。 colorSpace为display-p3时,数值仍是 0–255 的该色域量化,不能当 sRGB 送进 CSS。- 不要假设
getImageData的结果能postMessage零拷贝。Worker 里处理应createImageBitmaptransfer,或共享OffscreenCanvas。
3. 架构:处理表面 ≠ 视图表面
export async function blurRegion(
source: ImageBitmap,
cssRect: { x: number; y: number; w: number; h: number },
dpr: number,
): Promise<ImageBitmap> {
const canvas = new OffscreenCanvas(
Math.max(1, Math.round(cssRect.w * dpr)),
Math.max(1, Math.round(cssRect.h * dpr)),
);
const ctx = canvas.getContext("2d", { willReadFrequently: true });
if (!ctx) throw new Error("2d unavailable");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.filter = "blur(6px)";
ctx.drawImage(
source,
cssRect.x,
cssRect.y,
cssRect.w,
cssRect.h,
0,
0,
cssRect.w,
cssRect.h,
);
return canvas.transferToImageBitmap();
}
视图 canvas 保持 willReadFrequently: false,只 drawImage 结果。不要在动画 canvas 上 getImageData 做一次模糊。
直方图、去底、洪水填充:必须 willReadFrequently: true 的离屏;输入用 ImageBitmap,输出也尽量是 bitmap,而不是把 ImageData 长期挂在 React/Vue state 里(那是巨大的可序列化炸弹)。
4. 适用 / 不适用
适用:导出前调色、印章去底、像素级碰撞遮罩(低频)、生成缩略图。
不适用:每帧拾取、每帧全画布滤镜、用像素反推「点到了哪个矩形」。拾取走几何,见 命中。
5. 失败形态
| 症状 | 原因 |
|---|---|
| put 上去错位/发糊 | 忽略 CTM 与 DPR,按 CSS 坐标 put |
| 第一次 get 卡 100ms | 视图表面未声明 readback |
| 透明边缘黑边 | 把非预乘当预乘混合 |
| Worker 里 ImageData 拷贝爆炸 | 没 transfer bitmap |
| SecurityError | 表面已被跨源图污染 |
权威资料
核对日期:2026-08-26