canvas 渲染模糊的根源是未按 devicepixelratio 缩放 canvas.width/height,导致像素点不足而插值拉伸;需用 math.floor(csssize * dpr) 设置 canvas 内部尺寸,style 保持 css 像素,再调 ctx.scale(dpr, dpr) 对齐坐标系,并适配图片、文字资源及监听 dpr 动态变化。

canvas.width/height 必须按 dpr 缩放,否则绘图缓冲区永远不够用
Canvas 渲染模糊的根源不是“画得不好”,而是画布内部只分配了 400×300 个像素点,却硬要塞进 800×600 物理像素的空间里——浏览器只能插值拉伸,结果就是发虚、锯齿、线条变粗。window.devicePixelRatio 就是告诉你“该塞多少物理像素进来”。
实操要点:
- 先读取
window.devicePixelRatio || 1,别假设它是整数(Windows 缩放 125% 时可能是1.25) -
canvas.width和canvas.height必须设为Math.floor(cssWidth * dpr)和Math.floor(cssHeight * dpr),用Math.floor避免小数导致 WebGL 上下文丢失 -
canvas.style.width和canvas.style.height保持原始 CSS 像素值(如"400px"),不能跟着 dpr 放大 - 如果 canvas 是响应式(比如父容器宽度变化),每次 resize 后都要重设这四个值,不能只设一次
ctx.scale(dpr, dpr) 是坐标系对齐的关键,漏掉它就等于重写所有绘图逻辑
光放大 canvas 尺寸还不够。此时 ctx.fillRect(0, 0, 10, 10) 画出来的其实是 10×10 物理像素的方块,在 DPR=2 下只占逻辑尺寸的一半——所有坐标、尺寸都偏小了。
必须在设置完 canvas 尺寸后立即执行:
ctx.scale(dpr, dpr);
这样之后所有绘图操作仍可按 CSS 像素写,比如 ctx.fillRect(0, 0, 10, 10) 就真画出 10×10 CSS 像素的区域。注意:这个缩放会累积,每次重置 ctx 前建议用 ctx.setTransform(1, 0, 0, 1, 0, 0) 彻底清空状态。
图像和文字资源不匹配 dpr,清晰度前功尽弃
Canvas 内部再清晰,贴一张 100×100 的普通图片,或用 ctx.font = "14px Arial" 渲染文字,照样糊。因为它们没参与物理像素密度适配。
常见处理方式:
- 加载图片时优先请求
@2x版本:img.src = "icon.png".replace(/\.png$/, `@${Math.round(dpr)}x.png`) - 用
drawImage贴图时,显式传入目标宽高(避免浏览器自动缩放):ctx.drawImage(img, 0, 0, img.width, img.height, x, y, width / dpr, height / dpr) - 文字字号建议乘以 dpr:
ctx.font = `${14 * dpr}px Arial`,同时确保ctx.scale(dpr, dpr)已启用,否则字体位置会错乱
dpr 可能动态变化,监听 resize 不够,得对比值再重绘
用户切显示器、进全屏、调系统缩放,window.devicePixelRatio 都可能突变。仅靠 window.addEventListener('resize', ...) 不可靠——resize 不一定触发 dpr 变化,dpr 变化也不一定触发 resize。
稳妥做法是缓存上一次的 dpr 值,每次 resize 或 focus 时检查:
let lastDPR = window.devicePixelRatio || 1;
window.addEventListener('resize', () => {
const currentDPR = window.devicePixelRatio || 1;
if (currentDPR !== lastDPR) {
lastDPR = currentDPR;
resetCanvasSize(); // 重设 width/height/style
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(currentDPR, currentDPR);
redraw(); // 重绘全部内容,不能只 drawImage 旧缓存
}
});
最易被忽略的是:重绘必须基于新 dpr 下的坐标逻辑,而不是复用旧 canvas.toDataURL() 或离屏 canvas —— 那些数据本身已绑定旧 dpr 分辨率。











