位图表面与立即模式
<canvas> 对外是一个 DOM 元素,对内是一块 固定分辨率的位图。这块位图叫 backing store,尺寸由 IDL 属性 width / height 决定,单位是像素,默认 300 × 150。
CSS 的 width / height 只决定这块位图 怎么被拉伸显示,不改变像素数量。
1. 两套尺寸
const canvas = document.querySelector("canvas");
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error("expected canvas");
}
// 显示成 320 CSS px 宽,但 backing store 仍可能是 300
canvas.style.width = "320px";
console.log(canvas.width, canvas.clientWidth);
| 属性 | 含义 |
|---|---|
canvas.width / canvas.height | backing store 像素尺寸;写入会 重置整个表面(透明黑)并重置部分 context 状态 |
canvas.style.width / clientWidth | 布局尺寸,CSS 像素 |
ctx.getTransform() | 当前用户空间到 backing store 的矩阵,与 CSS 缩放无关 |
高 DPI 对齐见 DPR 与 backing store。这里先记住:模糊通常是「少量像素被 CSS 放大」,不是 imageSmoothingEnabled 能单独修好的。
2. 写入 width/height 的副作用
规范要求:设置 canvas.width 或 canvas.height 会:
- 清空 backing store。
- 把 context 的 drawing state 栈清空并恢复默认状态。
- 当前默认路径也会重置。
所以「为了 DPR 改尺寸」等于 整帧作废。resize 时必须从场景图重绘,不能指望像素还在。
function resizeBackingStore(
canvas: HTMLCanvasElement,
cssWidth: number,
cssHeight: number,
dpr: number,
): void {
const w = Math.max(1, Math.round(cssWidth * dpr));
const h = Math.max(1, Math.round(cssHeight * dpr));
if (canvas.width === w && canvas.height === h) {
return;
}
canvas.width = w;
canvas.height = h;
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
}
先改 backing store,再设 CSS。不要用 canvas.width = canvas.clientWidth 在已经 scale(dpr) 的情况下循环触发。非整 DPR 用 Math.round,与 05 一致。
3. 坐标默认单位
未做任何 scale 时,一个用户空间单位 = backing store 的一个像素。
CSS 把 canvas 拉大后,一个用户单位在屏幕上可能覆盖多个设备像素——这就是糊。
clearRect(0, 0, canvas.width, canvas.height) 清的是用户空间,若当前 CTM 不是单位阵,这样写会清错。生产代码在全清前:
function clearSurface(ctx: CanvasRenderingContext2D): void {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.restore();
}
4. 适用 / 不适用
适用:游戏视口、波形、标注层、导出 PNG。
不适用:段落文本排版、表单、需要浏览器查找/复制的内容。Canvas 里的字是像素,不是 DOM 文本。
权威资料
核对日期:2026-08-26