先记住这个答案
先把 canvas 的 width/height 设为 CSS 显示尺寸乘以 devicePixelRatio,再调用 ctx.scale(dpr, dpr) 让后续绘制使用 CSS 像素坐标,最后把 style.width/height 设回原 CSS 尺寸。若期间调整过变换,需先 ctx.setTransform(1,0,0,1,0,0) 复位,避免缩放叠加。这样 backing store 与显示像素一一对应,文字和矢量线条不再模糊。
- canvas 尺寸属性与 CSS 尺寸分离
- 必须按 dpr 放大 backing store
- ctx.scale 后用 CSS 像素绘制
- 变换需 setTransform 复位
模糊的根因:像素倒挂
Canvas 的 width 和 height 属性定义的是位图缓冲区的像素数,即 backing store;而 CSS 的 style.width 和 style.height 决定元素占据的屏幕区域。当两者数值相同时,在 DPR=2 的屏幕上,一块 200×100 的位图被拉伸到 400×200 的 CSS 面积,每个位图像素对应用 2×2 个物理像素,浏览器只能做插值补点,导致边缘和文字发虚。
正确的做法是让 backing store 的像素数等于 CSS 尺寸乘以 devicePixelRatio。绘制时如果不调整变换,后续所有坐标都必须换算成物理像素,很繁琐。所以常见方案是调用 ctx.scale(dpr, dpr) 将默认用户单位放大,之后继续使用 CSS 像素坐标,浏览器自动映射到高密度位图。
1200×800 画板适配 2x 屏
设计一个图表库的 resize 流程,画布逻辑尺寸固定 600×400,目标设备 DPR=2。代码先读取 window.devicePixelRatio(设为 2),然后设置 canvas.width = 600 * 2,canvas.height = 400 * 2,再执行 ctx.setTransform(2,0,0,2,0,0),最后把 canvas.style.width 和 canvas.style.height 设为 '600px' 和 '400px'。注意不能省略 setTransform,因为可能存在上一次残余变换。
这样处理后的后果:之后所有 fillRect、arc、drawImage 都接受 CSS 像素坐标,内部自动映射到物理像素,线条宽度无需乘 DPR。若改成 ctx.scale(dpr, dpr),当后续代码已有变换时容易累积,建议统一用 setTransform 一次性覆盖,避免状态混乱。
失效场景与补偿手段
一种常见误区是只增大 canvas.width 而不改 style.width,这样画布位图变大了但显示区域不变,可能导致内容被截断或显示缩小。必须同时修改 CSS 属性保持布局不变。处理动画时若每帧调用 clearRect,注意坐标系统已缩放,清空区域要覆盖 0,0 到实际逻辑宽高,可调用 ctx.clearRect(0,0,width,height) 其中 width/height 是逻辑值。
另一个边界是浏览器窗口缩放或系统缩放变化导致 devicePixelRatio 变化。需要监听 resize 事件重新读取 DPR 并执行整个重置。重置时先 ctx.setTransform(1,0,0,1,0,0) 复位,再更新尺寸,防止残留变换。代价是重设尺寸会清空画布,必须重绘所有内容,因此高频 resize 时应做防抖或使用 requestAnimationFrame 合并。
容易答错的地方
- 只调 canvas.width 不调 CSS
- 有人误以为放大 width 即可,但 CSS 尺寸不变,画布仍按原大小绘制,只是内部坐标系变大,实际显示会缩小,不会变清晰。必须同步设置 style 尺寸。
- 用 scale 而非 setTransform
- 直接 ctx.scale(dpr,dpr) 在已有变换时会叠加,若之前有旋转或缩放,整体矩阵错误。正确做法是先用 setTransform(1,0,0,1,0,0) 复位再 setTransform(dpr,0,0,dpr,0,0),保证基底干净。
面试官还会怎么问?
若 canvas 需要显示在多个不同 DPR 的窗口,如何监听变化?
通过 ResizeObserver 观察元素尺寸,结合 window.devicePixelRatio 监听屏幕缩放事件(matchMedia('resolution'))。变化时重建 canvas 尺寸并重绘。注意性能,使用 rAF 合并。
ctx.scale 后如何避免 clearRect 清不净?
clearRect 使用当前变换,若已放大到 dpr,则传 0,0,逻辑宽,逻辑高 即可。如果变换有偏移,需考虑平移;最简单方法是 setTransform(dpr,0,0,dpr,0,0) 后 clearRect。
用 CSS 的 transform: scale(2) 替代可避免模糊吗?
不能,CSS 缩放仅影响显示尺寸,canvas 的 backing store 未变化,仍会插值模糊。只有提升 canvas.width 才增加物理像素密度。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。