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
相关产品推荐
相关产品推荐

