रिस्पॉन्सिव Game Canvas
जो canvas ठीक से स्केल नहीं करता वह अलग-अलग स्क्रीन साइज़ पर धुंधला दिखता है या टूट जाता है। मैंने इस समस्या को गिनती से ज़्यादा बार डीबग किया है, तो चलिए मैं आपकी परेशानी बचा देता हूं।
अलग-अलग स्केलिंग modes आज़माएं:
मुख्य समस्या
Canvas के दो साइज़ होते हैं जिन्हें आसानी से गड़बड़ा दिया जाता है। CSS साइज़ तय करता है कि यह स्क्रीन पर कितना बड़ा दिखेगा। आंतरिक resolution तय करता है कि आप असल में कितने पिक्सेल draw कर रहे हैं। अगर ये मेल नहीं खाते, तो आपको धुंधलापन या विकृति मिलती है।
Retina display पर, आपका CSS कह सकता है 800x600, लेकिन स्क्रीन में असल में 1600x1200 फिज़िकल पिक्सेल होते हैं। अगर आपके canvas का आंतरिक resolution सिर्फ 800x600 है, तो browser इसे बड़ा करके स्केल कर देता है और सब कुछ धुंधला दिखने लगता है।
बेसिक रिस्पॉन्सिव सेटअप
यहां बताया गया है कि canvas को window भरने वाला और तेज़ दिखने वाला कैसे बनाएं:
const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')
function resize() {
const dpr = window.devicePixelRatio || 1
const width = window.innerWidth
const height = window.innerHeight
// Set internal resolution to match physical pixels
canvas.width = width * dpr
canvas.height = height * dpr
// Set CSS size to match logical pixels
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
// Scale context so you can draw in logical pixels
ctx.scale(dpr, dpr)
}
window.addEventListener('resize', resize)
resize()ctx.scale(dpr, dpr) वाली लाइन ही असली तरकीब है। इसके बाद, आप position (100, 100) पर draw कर सकते हैं और यह DPR की परवाह किए बिना सही जगह दिखाई देगा। Canvas आंतरिक रूप से 2x display पर 200x200 पर render करता है, लेकिन आपके code को यह जानने की ज़रूरत नहीं।
एक बात ध्यान रखें: ctx.scale संचयी (cumulative) है। अगर आप resize को कई बार कॉल करते हैं, तो स्केलिंग बढ़ती रहेगी। या तो स्केल करने से पहले transform को ctx.setTransform(1, 0, 0, 1, 0, 0) से रीसेट करें, या सिर्फ एक बार स्केल करें।
Letterboxing के साथ तय Aspect Ratio
कई games को एक खास aspect ratio चाहिए होता है। आप नहीं चाहेंगे कि आपका 16:9 game एक 21:9 ultrawide monitor को भरने के लिए खिंच जाए। Letterboxing काली पट्टियां जोड़कर ratio बनाए रखता है।
const GAME_WIDTH = 1920
const GAME_HEIGHT = 1080
const ASPECT = GAME_WIDTH / GAME_HEIGHT
function resizeWithLetterbox() {
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
const windowAspect = windowWidth / windowHeight
let width, height
if (windowAspect > ASPECT) {
// Window is wider than game, add bars on sides
height = windowHeight
width = height * ASPECT
} else {
// Window is taller than game, add bars on top/bottom
width = windowWidth
height = width / ASPECT
}
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
canvas.style.position = 'absolute'
canvas.style.left = (windowWidth - width) / 2 + 'px'
canvas.style.top = (windowHeight - height) / 2 + 'px'
// Internal resolution stays fixed
canvas.width = GAME_WIDTH
canvas.height = GAME_HEIGHT
}Letterbox पट्टियां दिखाने के लिए, अपने body का background काला सेट करें:
body {
margin: 0;
background: #000;
overflow: hidden;
}Pixel Art के लिए Integer Scaling
Pixel art integer स्केल (1x, 2x, 3x) पर साफ दिखता है और 2.7x जैसे आंशिक स्केल पर धुंधला। image-rendering: pixelated के साथ भी, 2.5x स्केल का मतलब है कि कुछ पिक्सेल 2 स्क्रीन पिक्सेल चौड़े होते हैं और कुछ 3।
const GAME_WIDTH = 320
const GAME_HEIGHT = 180
function resizePixelPerfect() {
const maxWidth = window.innerWidth
const maxHeight = window.innerHeight
// Find largest integer scale that fits
const scaleX = Math.floor(maxWidth / GAME_WIDTH)
const scaleY = Math.floor(maxHeight / GAME_HEIGHT)
const scale = Math.max(1, Math.min(scaleX, scaleY))
const width = GAME_WIDTH * scale
const height = GAME_HEIGHT * scale
canvas.width = GAME_WIDTH
canvas.height = GAME_HEIGHT
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
canvas.style.imageRendering = 'pixelated'
}इसका नुकसान यह है कि आपका game स्क्रीन को बिल्कुल पूरा नहीं भरेगा। 320x180 game के साथ 1920x1080 display पर, आपको 5x स्केलिंग (1600x900) मिलती है और किनारों के चारों ओर काली पट्टियां। कुछ players इसे पसंद करते हैं; दूसरे चाहते हैं कि game उनकी स्क्रीन भर दे भले ही वह थोड़ा धुंधला हो।
WebGL Canvas Scaling
WebGL के लिए, आपको canvas साइज़ के साथ-साथ viewport को भी अपडेट करना होगा:
function resizeGL(gl, canvas) {
const dpr = window.devicePixelRatio || 1
const width = canvas.clientWidth * dpr
const height = canvas.clientHeight * dpr
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
gl.viewport(0, 0, width, height)
return true // Resized, you may need to update projection matrices
}
return false
}इसे अपने render loop की शुरुआत में कॉल करें, सिर्फ resize events पर नहीं। कुछ browsers DPR बदल देते हैं जब आप window को monitors के बीच खींचते हैं, और आप उसे पकड़ना चाहेंगे।
Resize Events को Debounce करना
जब user window के किनारे को खींचता है तो resize events तेज़ी से fire होते हैं। अगर आपका resize function महंगा है, तो आपको jank मिलेगा।
let resizeTimeout
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(resize, 100)
})100ms की देरी का मतलब है कि resize सिर्फ तब होता है जब user खींचना बंद कर देता है। खींचने के दौरान सहज तरीके से संभालने के लिए, इसके बजाय ResizeObserver इस्तेमाल करें:
const observer = new ResizeObserver(entries => {
resize()
})
observer.observe(document.body)ResizeObserver कम बार fire होता है और browser द्वारा बेहतर ऑप्टिमाइज़ किया जाता है।
Fullscreen
Fullscreen API आपको पूरी स्क्रीन ले लेने देता है:
async function toggleFullscreen() {
if (!document.fullscreenElement) {
await canvas.requestFullscreen()
} else {
await document.exitFullscreen()
}
}
document.addEventListener('fullscreenchange', resize)आपको requestFullscreen को click जैसे किसी user gesture के जवाब में कॉल करना होगा। नहीं तो browsers इसे ब्लॉक कर देते हैं। और fullscreen में जाते/बाहर निकलते समय resize करना याद रखें क्योंकि उपलब्ध जगह बदल जाती है।
एक platform चेतावनी: 2026 के मध्य तक भी Fullscreen API iPhone Safari पर element fullscreen को कवर नहीं करता। requestFullscreen desktop browsers और iPadOS पर काम करता है, लेकिन iPhone पर यह canvas जैसे मनमाने elements के लिए समर्थित नहीं है (सिर्फ नेटिव video fullscreen काम करता है)। if (canvas.requestFullscreen) से feature-detect करें और शालीनता से fall back करें, उदाहरण के लिए canvas को दिखने वाले viewport को भरने के लिए साइज़ करके या users को game को PWA के रूप में अपनी home screen पर जोड़ने का संकेत देकर।
सब कुछ एक साथ जोड़ना
यहां एक class है जो सभी आम मामलों को संभालती है:
class ResponsiveCanvas {
constructor(canvas, options = {}) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.gameWidth = options.width || 1280
this.gameHeight = options.height || 720
this.pixelArt = options.pixelArt || false
this.letterbox = options.letterbox !== false
this.bind()
this.resize()
}
bind() {
window.addEventListener('resize', () => this.resize())
document.addEventListener('fullscreenchange', () => this.resize())
}
resize() {
const aspect = this.gameWidth / this.gameHeight
let width = window.innerWidth
let height = window.innerHeight
if (this.letterbox) {
if (width / height > aspect) {
width = height * aspect
} else {
height = width / aspect
}
}
if (this.pixelArt) {
const scale = Math.max(1, Math.floor(Math.min(
window.innerWidth / this.gameWidth,
window.innerHeight / this.gameHeight
)))
width = this.gameWidth * scale
height = this.gameHeight * scale
}
this.canvas.width = this.gameWidth
this.canvas.height = this.gameHeight
this.canvas.style.width = width + 'px'
this.canvas.style.height = height + 'px'
this.canvas.style.position = 'absolute'
this.canvas.style.left = (window.innerWidth - width) / 2 + 'px'
this.canvas.style.top = (window.innerHeight - height) / 2 + 'px'
if (this.pixelArt) {
this.canvas.style.imageRendering = 'pixelated'
}
}
get width() { return this.gameWidth }
get height() { return this.gameHeight }
}इसे ऐसे इस्तेमाल करें:
const responsive = new ResponsiveCanvas(document.getElementById('game'), {
width: 320,
height: 180,
pixelArt: true,
letterbox: true
})
// In your game code, use responsive.width and responsive.heightआम गलतियां
कुछ चीज़ें जो लोगों को फंसा देती हैं:
Resize पर canvas साफ हो जाता है। जब आप canvas.width या canvas.height सेट करते हैं, तो canvas साफ हो जाता है। अगर आप हर frame redraw नहीं कर रहे, तो आपको resize के बाद redraw करना होगा।
DPR बदल सकता है। अगर user browser window को Retina display से non-Retina display पर खींचता है, तो DPR बदल जाता है। इसे अपने render loop में जांचें, सिर्फ resize पर नहीं।
iOS Safari के viewport झमेले। जब address bar दिखता/छिपता है तो viewport height बदल जाती है। mobile पर ज़्यादा सटीक साइज़िंग के लिए window.visualViewport इस्तेमाल करें।
Context state रीसेट हो जाता है। Canvas dimensions सेट करने से context state रीसेट हो जाता है, जिसमें transforms, fill styles, और imageSmoothingEnabled शामिल हैं। resize के बाद इन्हें फिर से लागू करें।
और संसाधन
Canvas 2D game loop update/render loops की बुनियादी बातें बताता है।
Pixel art rendering साफ pixel graphics पर और गहराई से जाता है।
Mobile-friendly web games touch input और viewport समस्याओं को कवर करता है।
WebGL fundamentals WebGL contexts के लिए viewport और canvas सेटअप को कवर करता है।
तेज़ी से लोड होने वाला web game शिप करें performance के लिए canvas साइज़ ऑप्टिमाइज़ करना कवर करता है।