canvas高分屏模糊需三步:设canvas宽高为css尺寸×devicepixelratio,css设回显示尺寸,ctx.scale(dpr,dpr)并重置变换;绘图坐标取整防亚像素;监听resizeobserver和matchmedia动态适配。

Canvas 在高分屏上模糊,不是画得差,而是浏览器在拉伸一张“小图”去填“大框”——canvas.width 和 canvas.height 没跟上设备物理像素,ctx.scale() 也没同步校正,三者缺一不可。
怎么动态设置 canvas 物理尺寸并缩放绘图上下文
核心动作是让缓冲区像素数 = CSS 显示像素 × window.devicePixelRatio,再用 ctx.scale() 把坐标系“拉回来”,否则所有 fillRect(10, 10, 20, 20) 都会画在错位位置。
- 用
canvas.getBoundingClientRect()读当前显示宽高(别用offsetWidth,它受父级transform干扰) -
const dpr = window.devicePixelRatio || 1,然后canvas.width = rect.width * dpr、canvas.height = rect.height * dpr - 立刻用 CSS 压回显示尺寸:
canvas.style.width = rect.width + "px"、canvas.style.height = rect.height + "px" - 重设后必须调用
ctx.setTransform(1, 0, 0, 1, 0, 0)清除旧变换,再ctx.scale(dpr, dpr),防止多次缩放叠加
为什么整数坐标对清晰度至关重要
即使 dpr 和 scale 都对了,若画线时用了 x = 10.3 这类非整数坐标,浏览器仍会启动亚像素抗锯齿,把一个点颜色摊开到周围物理像素,线条就虚了。
- 网格线、边框、文字基线等关键位置,绘制前务必
Math.round(x)(已scale后的坐标) - 循环绘制时步长也要对齐,比如
for (let y = 0; y 比 <code>y += 0.8安全得多 -
textBaseline = "middle"配合y = Math.round(y),避免字体上下偏移半像素
响应式场景下如何持续适配尺寸变化
窗口缩放、横竖屏切换、系统缩放比例变更(如 Windows 125%)都会让 dpr 或显示尺寸突变,只初始化一次等于没做。
- 监听
resize和orientationchange,但别每帧都重设——加节流,比如setTimeout延迟 100ms 再执行重设逻辑 - 更可靠的是用
ResizeObserver监听 canvas 父容器尺寸变化,它比resize更精准,且不依赖窗口事件 - 用户切换显示器(如从笔记本插 HDMI 到 4K 屏)时,
window.devicePixelRatio可能突变,需监听window.matchMedia(`screen and (min-resolution: ${dpr}dppx)`).addEventListener('change', ...)
Canvas 和 DOM 元素叠加时定位错位怎么修
DOM 标签总往右下偏几像素?不是 CSS 定位写错了,是 Canvas 内部坐标系塌缩了:CSS 显示尺寸是 300×200,但 canvas.width 还是默认 300×150,上层 DOM 按 CSS 像素定位,底层 Canvas 却按更小的缓冲区渲染,视觉就“错开”了。
- 叠加 SVG 或 tooltip 时,Canvas 必须加
style="pointer-events: none"(纯展示)或手动计算坐标映射(需交互) - Canvas 容器要加
transform: translateZ(0)强制 GPU 层,否则可能被合成层遮挡,导致 DOM 元素不可见 - 验证是否真糊:用
ctx.getImageData(0, 0, 1, 1)读左上角 1×1 逻辑像素区域,若返回 RGBA 四字节中任意值 > 0 的像素超过 1 个,说明已被插值拉伸
最易被忽略的是:重设 canvas.width 会清空缓冲区,而 ctx.scale() 不重置绘图状态——比如你之前设置了 lineWidth = 2,缩放后实际线宽变成 2 * dpr 像素,但开发者常以为还是 2。所以每次重设后,最好显式重置关键状态:ctx.lineWidth = 1、ctx.font = "14px sans-serif",再乘回 dpr 调整字体大小。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











