跳到主要内容

ImageData与像素管线

ImageDataCPU 上的 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 不走 globalAlphashadowfilter、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(非预乘)。自己做混合要按非预乘公式,或先预乘。
  • colorSpacedisplay-p3 时,数值仍是 0–255 的该色域量化,不能当 sRGB 送进 CSS。
  • 不要假设 getImageData 的结果能 postMessage 零拷贝。Worker 里处理应 createImageBitmap transfer,或共享 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