Skip to content

Duyarlı Oyun Tuvali

Doğru şekilde ölçeklenmeyen bir tuval, farklı ekran boyutlarında bulanık görünür veya bozulur. Bu sorunu sayamayacağım kadar çok kez ayıkladım; sizi bu zahmetten kurtarayım.

Farklı ölçekleme modlarını deneyin:

Temel Sorun

Canvas'ın kolayca karıştırılabilen iki boyutu vardır. CSS boyutu, ekranda ne kadar büyük görüneceğini belirler. Dahili çözünürlük ise gerçekte kaç piksel çizdiğinizi belirler. Bunlar eşleşmezse bulanıklık veya bozulma oluşur.

Retina bir ekranda CSS değeriniz 800x600 olabilir, ancak ekranın fiziksel çözünürlüğü aslında 1600x1200 pikseldir. Canvas'ın dahili çözünürlüğü yalnızca 800x600 ise tarayıcı bunu büyütür ve her şey bulanık görünür.

Temel Duyarlı Kurulum

Bir canvas'ın pencereyi doldurmasını ve net görünmesini şu şekilde sağlayabilirsiniz:

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
  
  // Dahili çözünürlüğü fiziksel piksellerle eşleşecek şekilde ayarla
  canvas.width = width * dpr
  canvas.height = height * dpr
  
  // CSS boyutunu mantıksal piksellerle eşleşecek şekilde ayarla
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  
  // Mantıksal piksellerle çizim yapabilmek için bağlamı ölçekle
  ctx.scale(dpr, dpr)
}

window.addEventListener('resize', resize)
resize()

İşin püf noktası ctx.scale(dpr, dpr) satırıdır. Bundan sonra (100, 100) konumuna çizim yapabilirsiniz ve DPR değerinden bağımsız olarak doğru yerde görünür. Canvas, 2x bir ekranda dahili olarak 200x200 çözünürlükte görüntü oluşturur, ancak kodunuzun bunu bilmesi gerekmez.

Dikkat edilmesi gereken bir nokta: ctx.scale birikimlidir. Yeniden boyutlandırma işlevini birden çok kez çağırırsanız ölçek giderek büyür. Ölçeklemeden önce ctx.setTransform(1, 0, 0, 1, 0, 0) ile dönüşümü sıfırlayın veya yalnızca bir kez ölçekleyin.

Letterbox ile Sabit En-Boy Oranı

Birçok oyun belirli bir en-boy oranına ihtiyaç duyar. 16:9 oyununuzun 21:9 ultra geniş bir monitörü doldurmak için esnetilmesini istemezsiniz. Letterbox, oranı korumak için siyah şeritler ekler.

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) {
    // Pencere oyundan daha genişse yanlara şerit ekle
    height = windowHeight
    width = height * ASPECT
  } else {
    // Pencere oyundan daha uzunsa üste ve alta şerit ekle
    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'
  
  // Dahili çözünürlük sabit kalır
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
}

Letterbox şeritlerinin görünmesi için gövde arka planını siyah yapın:

css
body {
  margin: 0;
  background: #000;
  overflow: hidden;
}

Piksel Sanatı İçin Tam Sayılı Ölçekleme

Piksel sanatı tam sayılı ölçeklerde (1x, 2x, 3x) net, 2.7x gibi kesirli ölçeklerde ise bulanık görünür. image-rendering: pixelated kullanılsa bile 2.5x ölçek, bazı piksellerin 2 ekran pikseli, diğerlerinin ise 3 ekran pikseli genişliğinde olması anlamına gelir.

js
const GAME_WIDTH = 320
const GAME_HEIGHT = 180

function resizePixelPerfect() {
  const maxWidth = window.innerWidth
  const maxHeight = window.innerHeight
  
  // Sığabilecek en büyük tam sayılı ölçeği bul
  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'
}

Bunun karşılığında oyununuz ekranı tam olarak doldurmaz. 320x180 boyutundaki bir oyun 1920x1080 ekranda 5x ölçeklenir (1600x900) ve kenarlarda siyah şeritler oluşur. Bazı oyuncular bunu tercih ederken diğerleri, biraz bulanık olsa bile oyunun ekranı doldurmasını ister.

WebGL Canvas Ölçekleme

WebGL için canvas boyutuna ek olarak görünüm alanını da güncellemeniz gerekir:

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 // Yeniden boyutlandırıldı; projeksiyon matrislerini güncellemeniz gerekebilir
  }
  return false
}

Bunu yalnızca yeniden boyutlandırma olaylarında değil, görüntü oluşturma döngünüzün başında çağırın. Bazı tarayıcılar bir pencereyi monitörler arasında sürüklediğinizde DPR değerini değiştirir; bunu da yakalamak istersiniz.

Yeniden Boyutlandırma Olaylarını Seyrekleştirme

Kullanıcı pencerenin kenarını sürüklerken yeniden boyutlandırma olayları hızla tetiklenir. Yeniden boyutlandırma işleviniz maliyetliyse takılmalar yaşanır.

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

100 ms'lik gecikme, yeniden boyutlandırmanın yalnızca kullanıcı sürüklemeyi bıraktıktan sonra gerçekleşmesini sağlar. Sürükleme sırasında daha akıcı bir yönetim için bunun yerine ResizeObserver kullanın:

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

ResizeObserver daha seyrek tetiklenir ve tarayıcı tarafından daha iyi optimize edilir.

Tam Ekran

Fullscreen API, ekranın tamamını kullanmanızı sağlar:

js
async function toggleFullscreen() {
  if (!document.fullscreenElement) {
    await canvas.requestFullscreen()
  } else {
    await document.exitFullscreen()
  }
}

document.addEventListener('fullscreenchange', resize)

requestFullscreen işlevini tıklama gibi bir kullanıcı etkileşimine yanıt olarak çağırmalısınız. Aksi takdirde tarayıcılar bunu engeller. Ayrıca kullanılabilir alan değişeceği için tam ekrana girerken ve tam ekrandan çıkarken yeniden boyutlandırmayı unutmayın.

Platformla ilgili bir sınırlama: 2026 ortası itibarıyla Fullscreen API, iPhone Safari'de öğeleri tam ekrana alma özelliğini hâlâ kapsamıyor. requestFullscreen masaüstü tarayıcılarda ve iPadOS'te çalışır, ancak iPhone'da canvas gibi rastgele öğeler için desteklenmez (yalnızca yerel video tam ekranı çalışır). Desteği if (canvas.requestFullscreen) ile algılayın ve desteklenmediğinde canvas'ı görünür görünüm alanını dolduracak şekilde boyutlandırmak veya kullanıcılardan oyunu PWA olarak ana ekranlarına eklemelerini istemek gibi uygun bir alternatif sunun.

Her Şeyi Bir Araya Getirme

Yaygın durumların tümünü yöneten bir sınıf:

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

Şu şekilde kullanın:

js
const responsive = new ResponsiveCanvas(document.getElementById('game'), {
  width: 320,
  height: 180,
  pixelArt: true,
  letterbox: true
})

// Oyun kodunuzda responsive.width ve responsive.height kullanın

Sık Karşılaşılan Sorunlar

İnsanların sıkça gözden kaçırdığı birkaç nokta:

Canvas yeniden boyutlandırıldığında temizlenir. canvas.width veya canvas.height değerini ayarladığınızda canvas temizlenir. Her karede yeniden çizim yapmıyorsanız yeniden boyutlandırmadan sonra tekrar çizmeniz gerekir.

DPR değişebilir. Kullanıcı tarayıcı penceresini Retina bir ekrandan Retina olmayan bir ekrana sürüklerse DPR değişir. Bunu yalnızca yeniden boyutlandırma sırasında değil, görüntü oluşturma döngünüzde de kontrol edin.

iOS Safari görünüm alanı tuhaflıkları. Adres çubuğu gösterildiğinde veya gizlendiğinde görünüm alanının yüksekliği değişir. Mobil cihazlarda daha doğru boyutlandırma için window.visualViewport kullanın.

Bağlam durumu sıfırlanır. Canvas boyutlarını ayarlamak; dönüşümler, dolgu stilleri ve imageSmoothingEnabled dâhil olmak üzere bağlam durumunu sıfırlar. Yeniden boyutlandırmadan sonra bunları tekrar uygulayın.

Daha Fazla Kaynak

Canvas 2D oyun döngüsü, güncelleme ve görüntü oluşturma döngülerinin temellerini ele alır.

Piksel sanatı görüntüleme, net piksel grafiklerini daha ayrıntılı olarak inceler.

Mobil uyumlu web oyunları, dokunmatik girişi ve görünüm alanı sorunlarını ele alır.

WebGL temelleri, WebGL bağlamları için görünüm alanı ve canvas kurulumunu ele alır.

Hızlı yüklenen bir web oyunu yayınlama, performans için canvas boyutunu optimize etmeyi ele alır.