Skip to content

Canvas de jogo responsivo

Um canvas que não é dimensionado corretamente fica borrado ou apresenta problemas em diferentes tamanhos de tela. Já depurei esse problema mais vezes do que consigo contar, então vou poupar você desse trabalho.

Experimente diferentes modos de dimensionamento:

O problema central

O canvas tem dois tamanhos que são fáceis de confundir. O tamanho definido pelo CSS controla o tamanho com que ele aparece na tela. A resolução interna controla quantos pixels você realmente desenha. Se esses valores não corresponderem, o resultado será desfoque ou distorção.

Em uma tela Retina, o CSS pode definir 800x600, mas a tela na verdade tem 1600x1200 pixels físicos. Se a resolução interna do canvas for apenas 800x600, o navegador o amplia e tudo fica borrado.

Configuração responsiva básica

Veja como fazer um canvas preencher a janela e manter a nitidez:

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
  
  // Define a resolução interna para corresponder aos pixels físicos
  canvas.width = width * dpr
  canvas.height = height * dpr
  
  // Define o tamanho do CSS para corresponder aos pixels lógicos
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  
  // Dimensiona o contexto para que você possa desenhar em pixels lógicos
  ctx.scale(dpr, dpr)
}

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

A linha ctx.scale(dpr, dpr) é o segredo. Depois disso, você pode desenhar na posição (100, 100), e o conteúdo aparecerá no lugar certo independentemente do DPR. Internamente, o canvas renderiza em 200x200 em uma tela 2x, mas seu código não precisa saber disso.

Uma armadilha: ctx.scale é cumulativo. Se você chamar a função de redimensionamento várias vezes, a escala continuará aumentando. Redefina primeiro a transformação com ctx.setTransform(1, 0, 0, 1, 0, 0) antes de aplicar a escala, ou aplique-a apenas uma vez.

Proporção fixa com barras

Muitos jogos precisam de uma proporção específica. Você não quer que seu jogo em 16:9 seja esticado para preencher um monitor ultrawide de 21:9. As barras pretas preservam a proporção.

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) {
    // A janela é mais larga que o jogo; adiciona barras nas laterais
    height = windowHeight
    width = height * ASPECT
  } else {
    // A janela é mais alta que o jogo; adiciona barras em cima e embaixo
    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'
  
  // A resolução interna permanece fixa
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
}

Para que as barras apareçam, defina o fundo do body como preto:

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

Escala inteira para pixel art

Pixel art fica nítida com escalas inteiras (1x, 2x, 3x) e borrada com escalas fracionárias, como 2,7x. Mesmo com image-rendering: pixelated, uma escala de 2,5x significa que alguns pixels terão 2 pixels de tela de largura e outros terão 3.

js
const GAME_WIDTH = 320
const GAME_HEIGHT = 180

function resizePixelPerfect() {
  const maxWidth = window.innerWidth
  const maxHeight = window.innerHeight
  
  // Encontra a maior escala inteira que cabe na tela
  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'
}

A desvantagem é que o jogo não preencherá a tela por completo. Em uma tela de 1920x1080 com um jogo de 320x180, você obtém uma escala de 5x (1600x900), com barras pretas ao redor das bordas. Alguns jogadores preferem isso; outros querem que o jogo preencha a tela, mesmo que fique um pouco borrado.

Dimensionamento do canvas WebGL

No WebGL, você precisa atualizar a viewport além do tamanho do 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 // Redimensionado; talvez seja necessário atualizar as matrizes de projeção
  }
  return false
}

Chame essa função no início do loop de renderização, não apenas em eventos de redimensionamento. Alguns navegadores alteram o DPR quando você arrasta uma janela entre monitores, e é importante detectar essa mudança.

Aplicando debounce aos eventos de redimensionamento

Os eventos de redimensionamento são disparados rapidamente enquanto o usuário arrasta a borda da janela. Se sua função de redimensionamento for pesada, ocorrerão travamentos.

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

O atraso de 100 ms significa que o redimensionamento só acontece depois que o usuário para de arrastar. Para um tratamento mais suave durante o arraste, use ResizeObserver:

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

ResizeObserver é acionado com menos frequência e tem uma otimização melhor pelo navegador.

Tela cheia

A API Fullscreen permite ocupar a tela inteira:

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

document.addEventListener('fullscreenchange', resize)

Você deve chamar requestFullscreen em resposta a um gesto do usuário, como um clique. Caso contrário, os navegadores bloqueiam a solicitação. E lembre-se de redimensionar ao entrar ou sair do modo de tela cheia, pois o espaço disponível muda.

Uma ressalva de plataforma: em meados de 2026, a API Fullscreen ainda não oferece tela cheia para elementos no Safari do iPhone. requestFullscreen funciona em navegadores para desktop e no iPadOS, mas não é compatível no iPhone com elementos arbitrários, como um canvas — apenas a reprodução nativa de vídeo em tela cheia funciona. Detecte a disponibilidade do recurso com if (canvas.requestFullscreen) e ofereça uma alternativa adequada, como dimensionar o canvas para preencher a viewport visível ou sugerir que os usuários adicionem o jogo à tela inicial como um PWA.

Juntando tudo

Veja uma classe que lida com todos os casos comuns:

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

Use assim:

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

// No código do jogo, use responsive.width e responsive.height

Armadilhas comuns

Alguns detalhes que costumam causar problemas:

O canvas é apagado ao ser redimensionado. Quando você define canvas.width ou canvas.height, o canvas é apagado. Se você não redesenha a cada frame, precisa redesenhar após o redimensionamento.

O DPR pode mudar. Se o usuário arrastar a janela do navegador de uma tela Retina para uma tela que não seja Retina, o DPR mudará. Verifique-o no loop de renderização, não apenas ao redimensionar.

Peculiaridades da viewport no Safari do iOS. A altura da viewport muda quando a barra de endereço aparece ou desaparece. Use window.visualViewport para obter dimensões mais precisas em dispositivos móveis.

O estado do contexto é redefinido. Definir as dimensões do canvas redefine o estado do contexto, incluindo transformações, estilos de preenchimento e imageSmoothingEnabled. Reaplique essas configurações após o redimensionamento.

Mais recursos

Loop de jogo com Canvas 2D aborda os fundamentos dos loops de atualização e renderização.

Renderização de pixel art se aprofunda em gráficos de pixels nítidos.

Jogos web adaptados para dispositivos móveis aborda controles por toque e problemas de viewport.

Fundamentos de WebGL aborda a configuração da viewport e do canvas para contextos WebGL.

Publique um jogo web que carregue rapidamente aborda a otimização do tamanho do canvas para melhorar o desempenho.