Skip to content

Canvas Game Responsif

Canvas yang tidak diskalakan dengan benar akan terlihat buram atau rusak pada ukuran layar yang berbeda. Saya sudah menangani masalah ini berkali-kali, jadi biarkan saya membantu Anda menghindari kerepotan tersebut.

Coba berbagai mode penskalaan:

Masalah Utama

Canvas memiliki dua ukuran yang mudah tertukar. Ukuran CSS mengatur seberapa besar tampilannya di layar. Resolusi internal mengatur berapa banyak piksel yang benar-benar Anda gambar. Jika keduanya tidak cocok, hasilnya akan buram atau terdistorsi.

Pada layar Retina, CSS Anda mungkin menentukan ukuran 800x600, tetapi layar tersebut sebenarnya memiliki 1600x1200 piksel fisik. Jika resolusi internal canvas hanya 800x600, browser akan memperbesarnya sehingga semuanya terlihat buram.

Penyiapan Responsif Dasar

Berikut cara membuat canvas memenuhi jendela dan tetap terlihat tajam:

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
  
  // Atur resolusi internal agar sesuai dengan piksel fisik
  canvas.width = width * dpr
  canvas.height = height * dpr
  
  // Atur ukuran CSS agar sesuai dengan piksel logis
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  
  // Skalakan konteks agar Anda dapat menggambar dalam piksel logis
  ctx.scale(dpr, dpr)
}

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

Baris ctx.scale(dpr, dpr) adalah kuncinya. Setelah itu, Anda dapat menggambar pada posisi (100, 100) dan hasilnya akan muncul di tempat yang tepat, berapa pun DPR-nya. Secara internal, canvas dirender pada ukuran 200x200 di layar 2x, tetapi kode Anda tidak perlu mengetahuinya.

Satu hal yang perlu diperhatikan: ctx.scale bersifat kumulatif. Jika Anda memanggil fungsi resize beberapa kali, skalanya akan terus bertambah. Atur ulang transformasi terlebih dahulu dengan ctx.setTransform(1, 0, 0, 1, 0, 0) sebelum melakukan penskalaan, atau lakukan penskalaan hanya sekali.

Rasio Aspek Tetap dengan Letterboxing

Banyak game membutuhkan rasio aspek tertentu. Anda tentu tidak ingin game 16:9 direntangkan untuk memenuhi monitor ultrawide 21:9. Letterboxing menambahkan bilah hitam untuk mempertahankan rasio tersebut.

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) {
    // Jendela lebih lebar daripada game, tambahkan bilah di sisi-sisinya
    height = windowHeight
    width = height * ASPECT
  } else {
    // Jendela lebih tinggi daripada game, tambahkan bilah di atas/bawah
    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'
  
  // Resolusi internal tetap
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
}

Agar bilah letterbox terlihat, atur latar belakang body menjadi hitam:

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

Penskalaan Bilangan Bulat untuk Seni Piksel

Seni piksel terlihat tajam pada skala bilangan bulat (1x, 2x, 3x) dan buram pada skala pecahan seperti 2,7x. Bahkan dengan image-rendering: pixelated, skala 2,5x berarti sebagian piksel memiliki lebar 2 piksel layar dan sebagian lainnya 3 piksel.

js
const GAME_WIDTH = 320
const GAME_HEIGHT = 180

function resizePixelPerfect() {
  const maxWidth = window.innerWidth
  const maxHeight = window.innerHeight
  
  // Cari skala bilangan bulat terbesar yang muat
  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'
}

Konsekuensinya, game Anda tidak akan memenuhi layar secara persis. Pada layar 1920x1080 dengan game berukuran 320x180, Anda mendapatkan penskalaan 5x (1600x900) dengan bilah hitam di sekeliling tepinya. Sebagian pemain lebih menyukai ini; pemain lain ingin game memenuhi layar meskipun hasilnya sedikit buram.

Penskalaan Canvas WebGL

Untuk WebGL, Anda perlu memperbarui viewport selain ukuran canvas:

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 // Ukuran berubah, Anda mungkin perlu memperbarui matriks proyeksi
  }
  return false
}

Panggil fungsi ini pada awal loop render Anda, bukan hanya saat peristiwa perubahan ukuran terjadi. Beberapa browser mengubah DPR ketika Anda menyeret jendela dari satu monitor ke monitor lain, dan perubahan tersebut perlu terdeteksi.

Debouncing Peristiwa Perubahan Ukuran

Peristiwa perubahan ukuran dipicu dengan cepat ketika pengguna menyeret tepi jendela. Jika fungsi resize Anda berat, tampilan akan tersendat.

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

Jeda 100 milidetik berarti perubahan ukuran hanya dilakukan setelah pengguna berhenti menyeret. Agar penanganan lebih mulus selama proses menyeret, gunakan ResizeObserver:

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

ResizeObserver dipicu lebih jarang dan dioptimalkan dengan lebih baik oleh browser.

Layar Penuh

Fullscreen API memungkinkan game mengambil alih seluruh layar:

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

document.addEventListener('fullscreenchange', resize)

Anda harus memanggil requestFullscreen sebagai respons terhadap gestur pengguna seperti klik. Jika tidak, browser akan memblokirnya. Jangan lupa mengubah ukuran saat masuk atau keluar dari mode layar penuh karena ruang yang tersedia berubah.

Satu keterbatasan platform: hingga pertengahan 2026, Fullscreen API masih belum mendukung layar penuh untuk elemen di Safari pada iPhone. requestFullscreen berfungsi di browser desktop dan iPadOS, tetapi di iPhone fitur ini tidak didukung untuk elemen sembarang seperti canvas (hanya layar penuh video native yang berfungsi). Deteksi dukungan fitur dengan if (canvas.requestFullscreen) dan sediakan alternatif yang baik, misalnya dengan mengatur ukuran canvas agar memenuhi viewport yang terlihat atau meminta pengguna menambahkan game ke layar utama sebagai PWA.

Menggabungkan Semuanya

Berikut adalah class yang menangani semua kasus umum:

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

Gunakan seperti ini:

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

// Dalam kode game Anda, gunakan responsive.width dan responsive.height

Kendala Umum

Beberapa hal yang sering mengecoh orang:

Canvas dikosongkan saat ukurannya berubah. Ketika Anda mengatur canvas.width atau canvas.height, canvas akan dikosongkan. Jika Anda tidak menggambar ulang pada setiap frame, Anda perlu menggambar ulang setelah ukurannya berubah.

DPR dapat berubah. Jika pengguna menyeret jendela browser dari layar Retina ke layar non-Retina, DPR akan berubah. Periksa nilainya di dalam loop render, bukan hanya saat terjadi perubahan ukuran.

Keunikan viewport Safari di iOS. Tinggi viewport berubah ketika bilah alamat ditampilkan atau disembunyikan. Gunakan window.visualViewport untuk menentukan ukuran secara lebih akurat di perangkat seluler.

State konteks diatur ulang. Mengatur dimensi canvas akan mengatur ulang state konteks, termasuk transformasi, gaya isian, dan imageSmoothingEnabled. Terapkan kembali semuanya setelah perubahan ukuran.

Referensi Lainnya

Loop game Canvas 2D membahas dasar-dasar loop pembaruan/render.

Rendering seni piksel membahas lebih dalam cara menghasilkan grafis piksel yang tajam.

Game web yang ramah perangkat seluler membahas input sentuh dan masalah viewport.

Dasar-dasar WebGL membahas penyiapan viewport dan canvas untuk konteks WebGL.

Rilis game web yang dimuat dengan cepat membahas pengoptimalan ukuran canvas untuk meningkatkan performa.