如何正确对HTML Canvas进行下采样以解决高DPI屏幕上的图像模糊问题?
解决高DPI屏幕下Canvas动画模糊问题
我看了你的代码,问题主要出在预渲染Canvas绘制到主Canvas时的坐标计算错误,以及一些上下文状态管理的小问题。下面是具体的修复方案和解释:
核心问题分析
- 绘制偏移计算错误:你在主Canvas上调用
scale(devicePixelRatio, devicePixelRatio)后,主Canvas的坐标系已经转换为CSS尺寸的坐标系(每个单位对应devicePixelRatio个实际像素)。但在Circle.draw方法中,你用了预渲染Canvas的实际像素宽度(this.m_canvas.width,已经乘过devicePixelRatio)来计算偏移,这就导致绘制的图像被放大了,看起来模糊且位置不对。 - 上下文状态混乱:预渲染Canvas的上下文调用了
tmp.restore()但没有对应的tmp.save(),这会破坏上下文的默认状态,影响后续绘制。 - 多余的路径操作:
drawImage不需要包裹在beginPath/closePath里,这些多余的代码可能会导致意外的绘制问题。
修复后的完整代码
var canvas = document.querySelector('canvas'); var c = canvas.getContext('2d'); var circles = []; // 模拟Retina屏幕 = 2, 普通屏幕 = 1 let devicePixelRatio = 2; function mainCanvasPixelRatio() { let rect = canvas.getBoundingClientRect(); // 设置主Canvas实际像素尺寸 canvas.width = rect.width * devicePixelRatio; canvas.height = rect.height * devicePixelRatio; // 缩放上下文,适配高DPI c.scale(devicePixelRatio, devicePixelRatio); // 用CSS设置Canvas显示尺寸 canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; } // 生成预渲染Canvas function spawn() { for (let i = 0; i < 2; i++) { let radius = parseInt(i * 30); // 主Canvas的CSS尺寸中心位置(因为主Canvas已经做了scale,所以用CSS尺寸计算) let x = Math.round(canvas.getBoundingClientRect().width / 2); let y = Math.round(canvas.getBoundingClientRect().height / 2); let PreRenderCanvas = document.createElement('canvas'); const tmp = PreRenderCanvas.getContext("2d"); // 预渲染Canvas的CSS尺寸 let preRenderCSSWidth = ((radius * 2) * 1.5) + 1; let preRenderCSSHeight = ((radius * 2) * 1.5) + 1; // 设置预渲染Canvas实际像素尺寸 PreRenderCanvas.width = preRenderCSSWidth * devicePixelRatio; PreRenderCanvas.height = preRenderCSSHeight * devicePixelRatio; // 设置显示尺寸 PreRenderCanvas.style.width = preRenderCSSWidth + 'px'; PreRenderCanvas.style.height = preRenderCSSHeight + 'px'; // 保存上下文状态,避免scale影响后续操作 tmp.save(); tmp.scale(devicePixelRatio, devicePixelRatio); // 预渲染Canvas的CSS尺寸中心 let m_canvasCenterX = preRenderCSSWidth * .5; let m_canvasCenterY = preRenderCSSHeight * .5; // 绘制红色圆圈 tmp.strokeStyle = "red"; tmp.beginPath(); tmp.arc(m_canvasCenterX, m_canvasCenterY, preRenderCSSWidth / 3, 0, 2 * Math.PI); tmp.lineWidth = 2; tmp.stroke(); tmp.closePath(); // 加载并绘制图像 const image = new Image(); let paddingX = preRenderCSSWidth / 5; let paddingY = preRenderCSSHeight / 5; image.src = "https://play-lh.googleusercontent.com/IeNJWoKYx1waOhfWF6TiuSiWBLfqLb18lmZYXSgsH1fvb8v1IYiZr5aYWe0Gxu-pVZX3"; image.onload = function () { // drawImage不需要beginPath/closePath tmp.drawImage( image, paddingX, paddingY, preRenderCSSWidth - (paddingX * 2), preRenderCSSHeight - (paddingY * 2) ); } // 恢复上下文状态 tmp.restore(); // 将预渲染Canvas的CSS尺寸传入Circle,方便后续计算偏移 let circle = new Circle(x, y, c, PreRenderCanvas, preRenderCSSWidth, preRenderCSSHeight); circles.push(circle); } } // Circle构造函数,新增CSS尺寸参数 function Circle(x, y, c, m_canvas, cssWidth, cssHeight) { this.x = x; this.y = y; this.c = c; this.m_canvas = m_canvas; this.cssWidth = cssWidth; this.cssHeight = cssHeight; } Circle.prototype = { draw: function () { // 使用CSS尺寸计算偏移,适配主Canvas的缩放坐标系 this.c.drawImage( this.m_canvas, this.x - (this.cssWidth / 2), this.y - (this.cssHeight / 2), this.cssWidth, this.cssHeight ); } }; function animate() { // 用主Canvas的CSS尺寸清理画布,因为上下文已经做了scale const rect = canvas.getBoundingClientRect(); c.clearRect(0, 0, rect.width, rect.height); circles.slice().reverse().forEach(function (circle) { circle.draw(); }); requestAnimationFrame(animate); } mainCanvasPixelRatio(); spawn(); animate();
#mainCanvas { background: blue; /* 给Canvas设置默认尺寸,方便测试 */ width: 400px; height: 400px; }
<canvas id="mainCanvas"></canvas> <br>
关键修复点解释
- 传递预渲染Canvas的CSS尺寸:在
spawn函数中,我们把预渲染Canvas的CSS尺寸(preRenderCSSWidth/preRenderCSSHeight)传入Circle构造函数,这样在绘制时可以直接用这个尺寸计算偏移,避免依赖style.width的字符串解析。 - 修正绘制偏移计算:在
Circle.draw方法中,用CSS尺寸的一半来计算绘制位置的偏移,同时明确指定绘制的宽度和高度为CSS尺寸,确保图像在主Canvas上正确显示。 - 上下文状态管理:在预渲染Canvas的上下文调用
scale之前,先调用tmp.save(),之后用tmp.restore()恢复状态,避免缩放影响后续的绘制操作。 - 移除多余路径操作:删掉
drawImage前后的beginPath/closePath,因为drawImage不需要路径包裹。 - 优化主Canvas清理:直接用
getBoundingClientRect()获取的CSS尺寸来清理画布,代码更清晰。
这样修改后,你的Canvas动画在高DPI屏幕上就会清晰显示,同时保持正确的尺寸和位置了。
内容的提问来源于stack exchange,提问作者Reginald1234
相关产品推荐
相关产品推荐

