Skip to content

रिस्पॉन्सिव Game Canvas

जो canvas ठीक से स्केल नहीं करता वह अलग-अलग स्क्रीन साइज़ पर धुंधला दिखता है या टूट जाता है। मैंने इस समस्या को गिनती से ज़्यादा बार डीबग किया है, तो चलिए मैं आपकी परेशानी बचा देता हूं।

अलग-अलग स्केलिंग modes आज़माएं:

मुख्य समस्या

Canvas के दो साइज़ होते हैं जिन्हें आसानी से गड़बड़ा दिया जाता है। CSS साइज़ तय करता है कि यह स्क्रीन पर कितना बड़ा दिखेगा। आंतरिक resolution तय करता है कि आप असल में कितने पिक्सेल draw कर रहे हैं। अगर ये मेल नहीं खाते, तो आपको धुंधलापन या विकृति मिलती है।

Retina display पर, आपका CSS कह सकता है 800x600, लेकिन स्क्रीन में असल में 1600x1200 फिज़िकल पिक्सेल होते हैं। अगर आपके canvas का आंतरिक resolution सिर्फ 800x600 है, तो browser इसे बड़ा करके स्केल कर देता है और सब कुछ धुंधला दिखने लगता है।

बेसिक रिस्पॉन्सिव सेटअप

यहां बताया गया है कि canvas को window भरने वाला और तेज़ दिखने वाला कैसे बनाएं:

js
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 बनाए रखता है।

js
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 काला सेट करें:

css
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।

js
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 को भी अपडेट करना होगा:

js
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 मिलेगा।

js
let resizeTimeout
window.addEventListener('resize', () => {
  clearTimeout(resizeTimeout)
  resizeTimeout = setTimeout(resize, 100)
})

100ms की देरी का मतलब है कि resize सिर्फ तब होता है जब user खींचना बंद कर देता है। खींचने के दौरान सहज तरीके से संभालने के लिए, इसके बजाय ResizeObserver इस्तेमाल करें:

js
const observer = new ResizeObserver(entries => {
  resize()
})
observer.observe(document.body)

ResizeObserver कम बार fire होता है और browser द्वारा बेहतर ऑप्टिमाइज़ किया जाता है।

Fullscreen

Fullscreen API आपको पूरी स्क्रीन ले लेने देता है:

js
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 है जो सभी आम मामलों को संभालती है:

js
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 }
}

इसे ऐसे इस्तेमाल करें:

js
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 साइज़ ऑप्टिमाइज़ करना कवर करता है।