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

Canvas缩放时内容分辨率异常问题求助(JavaScript)

Canvas缩放适配问题修复方案

问题背景

最近一周一直在解决Canvas适配问题:用VMIN、VMAX、VH、VW实现了禁止用户缩放Canvas内容的效果,但出现两个新问题:

  • 浏览器放大(比如500%缩放)时,Canvas内容模糊
  • 浏览器缩小时,内容细节冗余且运行卡顿
    试过修改CSS渲染方式,没用;尝试window.devicePixelRatio但要么用法错误,要么破坏了原本的禁止缩放功能。

效果对比

  • 缩放前(100%比例):内容清晰,运行流畅
  • 缩放后(500%比例):内容模糊,细节丢失

现有代码

gameCanvas CSS代码片段

#gameCanvas{
    position: absolute;
    border: 0;
    margin: 0;
    padding: 0;
    image-rendering: optimizeSpeed;
    z-index: -5;
}

Main.js脚本

import Utils from "./other/Utils.js"
import {Game} from "./Game.js";

//=== GLOBAL VARIABLES ===============================================================================

console.log("\n"+Utils.getTime(":")+"\n\n");

const container = document.getElementById("canvasSpace");
const canvas = document.getElementById("gameCanvas");
const context = canvas.getContext("2d");

const CANVAS_WIDTH = 100;
const CANVAS_HEIGHT = 100;

var VH = 1;
var VW = 1;
var VMIN = 1;
var VMAX = 1;

let secondsPassed = 0;
let oldTimeStamp = 0;
let timePassed = 0;

let debugPause = false;

var global = {
    
    getVH: function(perc){
        return VH*perc;
    },

    getVW: function(perc){
        return VW*perc;
    },

    getVMIN: function(perc){
        return VMIN*perc;
    },

    getVMAX: function(perc){
        return VMAX*perc;
    },
    
    getCanvasWidth: function(){
        return Utils.clamp(100, (getWindowWidth()/100)*CANVAS_WIDTH, 820);
    },
    
    getCanvasHeight: function(){
        return Utils.clamp(100, (getWindowHeight()/100)*CANVAS_HEIGHT, 1180);
    },

    getContainer: function(){
        return container;
    },

    doc: document,

    win: window,

    ctx: context,

    readJSONFile: function(filePath){
        const request = new XMLHttpRequest();
        request.open('GET', filePath, false);
        request.send();

        if (request.status === 200) 
        {
            return JSON.parse(request.responseText);
        } 
        else 
        {
            throw new Error(`Failed to load JSON file from ${filePath}`);
        }
    },

    nextScene: function(){
        game.nextScene();
    },

    getEaseTimeVar: function(){ //^ Used for easing [Matteo]
        var timeStamp = performance.now();
        secondsPassed = (timeStamp - oldTimeStamp) / 1000;
        oldTimeStamp = timeStamp;

        timePassed += secondsPassed;

        return timeStamp;
    },
    
};

responsivityUpdate();


//=== MAIN SETUP ===============================================================================

var game = new Game(global);

document.addEventListener('keydown', function(event) {
    if (event.code === 'Space') {
        debugPause = debugPause == true ? false : true;
    }
});

update();

//=== MAIN LOOP ===============================================================================

function update()
{
    
    if(!debugPause)
    {   
        responsivityUpdate();  

        //!_____
        game.update();
        
        //|_____
        draw();
        
    }
        
    requestAnimationFrame(update);
}

function draw() //^ draw() has the purpose to ONLY draw (and also call requestAnimationFrame() to mantain the loop) on the canvas
{
    context.clearRect(0, 0, canvas.width, canvas.height);

    context.fillStyle = "#F5E7C3";
    context.fillRect(0,0,global.getCanvasWidth(),global.getCanvasHeight());

    game.draw();

}


//=== FUNCTIONS ===============================================================================

function responsivityUpdate() //^ Ensures the canvas size and the respective viewports are reactive to the window size [Xavier]
{
    
    /* let ratio = window.devicePixelRatio;
    canvas.style.width = (global.getCanvasWidth()*ratio) + "px"; 
    canvas.style.height = (global.getCanvasHeight()*ratio) + "px";   */

    //? Align canvas size to our desired dimensions
    canvas.width = global.getCanvasWidth();
    canvas.height = global.getCanvasHeight();

    //? Calculating again the viewports values
    VW = (global.getCanvasWidth())/100;
    VH = (global.getCanvasHeight())/100;

    if(VH>VW)
    {
        VMAX = VH;
        VMIN = VW;
    }
    else{
        VMAX = VW;
        VMIN = VH;
    }

    //? Aligning the canvas to the center
    canvas.style.top = parseFloat(getCanvasOffsetX()) + "px"; 
    canvas.style.left = parseFloat(getCanvasOffsetY()) + "px"; 


    container.style.width = parseFloat(global.getCanvasWidth()) + "px";
    container.style.height = parseFloat(global.getCanvasHeight()) + "px";
    container.style.top = parseFloat(getCanvasOffsetX()) + "px";
    container.style.left = parseFloat(getCanvasOffsetY()) + "px";

}

function getCanvasOffsetX() //^ Returns the width of the space between the window top left corner and the canvas top left corner
{
    return ((getWindowHeight()/100)*((100-CANVAS_HEIGHT)/2).toFixed(2));
}

function getCanvasOffsetY() //^ Returns the height of the space between the window top left corner and the canvas top left corner
{
    return ((getWindowWidth()/100)*((100-CANVAS_WIDTH)/2).toFixed(2));
}

function getWindowWidth() //^ Returns the window width removing any external element width (scrollbars,topbar,...) [Xavier]
{
    /*
    ? There are multiple possible ways to get it depending on the browser, that's why I added more options
    */
    return (window.innerWidth || window.visualViewport.width || document.documentElement.clientWidth || document.body.clientWidth);
}

function getWindowHeight() //^ Returns the window height removing any external element height (scrollbars,topbar,...) [Xavier]
{
    /*
    ? Returns the window height removing any external element height (scrollbars,topbar,...) 
    */
    return (window.innerHeight || window.visualViewport.height || document.documentElement.clientHeight || document.body.clientHeight);
}

解决方案

核心问题是Canvas的实际像素尺寸(width/height属性)和显示尺寸(style.width/style.height)没有结合设备像素比和浏览器缩放比做适配,同时当前逻辑在窗口缩放时频繁修改Canvas属性导致性能问题。

1. 修正responsivityUpdate函数,结合设备像素比

修改responsivityUpdate,将Canvas的实际像素尺寸乘以设备像素比,同时保持显示尺寸不变,这样能解决模糊问题:

function responsivityUpdate()
{
    const ratio = window.devicePixelRatio || 1;
    // 显示尺寸保持原有逻辑,确保禁止缩放的效果
    const displayWidth = global.getCanvasWidth();
    const displayHeight = global.getCanvasHeight();

    // 实际像素尺寸 = 显示尺寸 * 设备像素比,保证高清渲染
    canvas.width = displayWidth * ratio;
    canvas.height = displayHeight * ratio;

    // 设置显示尺寸,维持原有布局
    canvas.style.width = `${displayWidth}px`;
    canvas.style.height = `${displayHeight}px`;

    // 调整绘图上下文的缩放比例,确保绘制内容适配实际像素
    context.scale(ratio, ratio);

    // 原有视口计算逻辑不变
    VW = displayWidth / 100;
    VH = displayHeight / 100;

    VMAX = Math.max(VH, VW);
    VMIN = Math.min(VH, VW);

    // 居中逻辑不变
    canvas.style.top = `${parseFloat(getCanvasOffsetX())}px`; 
    canvas.style.left = `${parseFloat(getCanvasOffsetY())}px`; 

    container.style.width = `${displayWidth}px`;
    container.style.height = `${displayHeight}px`;
    container.style.top = `${parseFloat(getCanvasOffsetX())}px`;
    container.style.left = `${parseFloat(getCanvasOffsetY())}px`;
}

2. 优化性能,避免频繁重计算

当前responsivityUpdate在每一帧都执行,会导致不必要的重绘和计算。改为只在窗口大小变化时执行:

// 初始化时执行一次
responsivityUpdate();

// 监听窗口 resize 事件,仅在窗口大小变化时更新
window.addEventListener('resize', responsivityUpdate);

// 把update函数里的responsivityUpdate()移除
function update()
{
    if(!debugPause)
    {   
        game.update();
        draw();
    }
    requestAnimationFrame(update);
}

3. 优化CSS渲染属性

修改image-rendering属性,兼顾清晰度和性能:

#gameCanvas{
    position: absolute;
    border: 0;
    margin: 0;
    padding: 0;
    image-rendering: crisp-edges;
    image-rendering: pixelated; /* 针对像素风格内容,可选 */
    z-index: -5;
}

原理说明

  • 设备像素比适配:通过将Canvas实际像素尺寸乘以devicePixelRatio,让Canvas的像素密度匹配屏幕,解决放大时的模糊问题。
  • 分离显示与实际尺寸:保持显示尺寸由原有逻辑控制,确保禁止缩放的效果不被破坏。
  • 性能优化:仅在窗口大小变化时更新Canvas尺寸,减少每一帧的计算开销,解决缩小时的卡顿问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 18:35:01