You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确对HTML Canvas进行下采样以解决高DPI屏幕上的图像模糊问题?

解决高DPI屏幕下Canvas动画模糊问题

我看了你的代码,问题主要出在预渲染Canvas绘制到主Canvas时的坐标计算错误,以及一些上下文状态管理的小问题。下面是具体的修复方案和解释:

核心问题分析

  1. 绘制偏移计算错误:你在主Canvas上调用scale(devicePixelRatio, devicePixelRatio)后,主Canvas的坐标系已经转换为CSS尺寸的坐标系(每个单位对应devicePixelRatio个实际像素)。但在Circle.draw方法中,你用了预渲染Canvas的实际像素宽度(this.m_canvas.width,已经乘过devicePixelRatio)来计算偏移,这就导致绘制的图像被放大了,看起来模糊且位置不对。
  2. 上下文状态混乱:预渲染Canvas的上下文调用了tmp.restore()但没有对应的tmp.save(),这会破坏上下文的默认状态,影响后续绘制。
  3. 多余的路径操作: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>

关键修复点解释

  1. 传递预渲染Canvas的CSS尺寸:在spawn函数中,我们把预渲染Canvas的CSS尺寸(preRenderCSSWidth/preRenderCSSHeight)传入Circle构造函数,这样在绘制时可以直接用这个尺寸计算偏移,避免依赖style.width的字符串解析。
  2. 修正绘制偏移计算:在Circle.draw方法中,用CSS尺寸的一半来计算绘制位置的偏移,同时明确指定绘制的宽度和高度为CSS尺寸,确保图像在主Canvas上正确显示。
  3. 上下文状态管理:在预渲染Canvas的上下文调用scale之前,先调用tmp.save(),之后用tmp.restore()恢复状态,避免缩放影响后续的绘制操作。
  4. 移除多余路径操作:删掉drawImage前后的beginPath/closePath,因为drawImage不需要路径包裹。
  5. 优化主Canvas清理:直接用getBoundingClientRect()获取的CSS尺寸来清理画布,代码更清晰。

这样修改后,你的Canvas动画在高DPI屏幕上就会清晰显示,同时保持正确的尺寸和位置了。

内容的提问来源于stack exchange,提问作者Reginald1234

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 03:47:31