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:
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.
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:
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.
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:
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.
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:
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:
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:
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:
const responsive = new ResponsiveCanvas(document.getElementById('game'), {
width: 320,
height: 180,
pixelArt: true,
letterbox: true
})
// Dalam kode game Anda, gunakan responsive.width dan responsive.heightKendala 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.