Canvas de jeu responsive
Un canvas qui ne se redimensionne pas correctement devient flou ou s’affiche mal selon la taille de l’écran. J’ai dû résoudre ce problème plus souvent que je ne saurais le compter, alors laissez-moi vous épargner ces difficultés.
Essayez différents modes de mise à l’échelle :
Le problème fondamental
Un canvas possède deux tailles qu’il est facile de confondre. La taille CSS détermine ses dimensions apparentes à l’écran. La résolution interne détermine le nombre de pixels que vous dessinez réellement. Si elles ne correspondent pas, l’image devient floue ou déformée.
Sur un écran Retina, votre CSS peut indiquer 800x600, alors que l’écran possède en réalité 1600x1200 pixels physiques. Si la résolution interne de votre canvas n’est que de 800x600, le navigateur l’agrandit et tout paraît flou.
Configuration responsive de base
Voici comment faire en sorte qu’un canvas remplisse la fenêtre tout en restant net :
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()La ligne ctx.scale(dpr, dpr) est l’astuce essentielle. Vous pouvez ensuite dessiner à la position (100, 100), qui s’affichera au bon endroit quelle que soit la valeur du DPR. En interne, le canvas effectue un rendu en 200x200 sur un écran 2x, mais votre code n’a pas besoin de le savoir.
Attention à un piège : ctx.scale est cumulatif. Si vous appelez plusieurs fois la fonction de redimensionnement, la mise à l’échelle continuera d’augmenter. Réinitialisez d’abord la transformation avec ctx.setTransform(1, 0, 0, 1, 0, 0) avant d’appliquer la mise à l’échelle, ou ne l’appliquez qu’une seule fois.
Format d’image fixe avec bandes noires
De nombreux jeux nécessitent un format d’image précis. Vous ne voulez pas que votre jeu en 16:9 soit étiré pour remplir un moniteur ultralarge en 21:9. L’ajout de bandes noires permet de préserver les proportions.
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
}Pour que les bandes noires soient visibles, définissez l’arrière-plan du corps de la page en noir :
body {
margin: 0;
background: #000;
overflow: hidden;
}Mise à l’échelle entière pour le pixel art
Le pixel art reste net avec des facteurs d’échelle entiers (1x, 2x, 3x), mais devient flou avec des facteurs fractionnaires comme 2,7x. Même avec image-rendering: pixelated, une échelle de 2,5x signifie que certains pixels occupent 2 pixels à l’écran et d’autres 3.
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'
}En contrepartie, votre jeu ne remplira pas exactement l’écran. Sur un écran de 1920x1080 avec un jeu en 320x180, vous obtenez une mise à l’échelle 5x (1600x900), avec des bandes noires sur les bords. Certains joueurs préfèrent ce résultat ; d’autres souhaitent que le jeu remplisse tout l’écran, même s’il devient légèrement flou.
Mise à l’échelle d’un canvas WebGL
Avec WebGL, vous devez mettre à jour la zone d’affichage en plus de la taille du 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 // Resized, you may need to update projection matrices
}
return false
}Appelez cette fonction au début de votre boucle de rendu, et pas uniquement lors des événements de redimensionnement. Certains navigateurs modifient le DPR lorsqu’une fenêtre passe d’un moniteur à un autre, et vous devez pouvoir détecter ce changement.
Temporisation des événements de redimensionnement
Les événements de redimensionnement se déclenchent très rapidement lorsque l’utilisateur fait glisser le bord de la fenêtre. Si votre fonction de redimensionnement est coûteuse, l’affichage manquera de fluidité.
let resizeTimeout
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(resize, 100)
})Le délai de 100 ms signifie que le redimensionnement ne s’effectue qu’une fois que l’utilisateur a cessé de faire glisser la fenêtre. Pour une gestion plus fluide pendant le déplacement, utilisez plutôt ResizeObserver :
const observer = new ResizeObserver(entries => {
resize()
})
observer.observe(document.body)ResizeObserver se déclenche moins fréquemment et bénéficie d’une meilleure optimisation par le navigateur.
Plein écran
L’API Fullscreen permet d’occuper la totalité de l’écran :
async function toggleFullscreen() {
if (!document.fullscreenElement) {
await canvas.requestFullscreen()
} else {
await document.exitFullscreen()
}
}
document.addEventListener('fullscreenchange', resize)Vous devez appeler requestFullscreen en réponse à une action de l’utilisateur, comme un clic. Sinon, les navigateurs bloquent la requête. Pensez également à redimensionner le canvas lors de l’entrée et de la sortie du mode plein écran, car l’espace disponible change.
Une réserve concernant les plateformes : à la mi-2026, l’API Fullscreen ne permet toujours pas d’afficher un élément en plein écran dans Safari sur iPhone. requestFullscreen fonctionne dans les navigateurs de bureau et sur iPadOS, mais l’iPhone ne le prend pas en charge pour des éléments arbitraires comme un canvas — seul le plein écran vidéo natif fonctionne. Vérifiez la disponibilité de cette fonctionnalité avec if (canvas.requestFullscreen) et prévoyez une solution de repli élégante, par exemple en dimensionnant le canvas pour remplir la zone d’affichage visible ou en invitant les utilisateurs à ajouter le jeu à leur écran d’accueil sous forme de PWA.
Assemblage de l’ensemble
Voici une classe qui gère tous les cas courants :
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 }
}Utilisez-la comme ceci :
const responsive = new ResponsiveCanvas(document.getElementById('game'), {
width: 320,
height: 180,
pixelArt: true,
letterbox: true
})
// In your game code, use responsive.width and responsive.heightPièges courants
Voici quelques points qui posent souvent problème :
Le canvas s’efface lors du redimensionnement. Lorsque vous définissez canvas.width ou canvas.height, le canvas s’efface. Si vous ne redessinez pas l’image à chaque frame, vous devez le faire après le redimensionnement.
Le DPR peut changer. Si l’utilisateur fait passer la fenêtre du navigateur d’un écran Retina à un écran non-Retina, le DPR change. Vérifiez sa valeur dans votre boucle de rendu, et pas uniquement lors du redimensionnement.
Particularités de la zone d’affichage dans Safari sur iOS. La hauteur de la zone d’affichage change lorsque la barre d’adresse apparaît ou disparaît. Utilisez window.visualViewport pour obtenir des dimensions plus précises sur mobile.
L’état du contexte est réinitialisé. La modification des dimensions du canvas réinitialise l’état du contexte, notamment les transformations, les styles de remplissage et imageSmoothingEnabled. Réappliquez-les après le redimensionnement.
Ressources complémentaires
Boucle de jeu Canvas 2D présente les principes fondamentaux des boucles de mise à jour et de rendu.
Rendu du pixel art approfondit la création de graphismes en pixels parfaitement nets.
Jeux web adaptés aux mobiles aborde les entrées tactiles et les problèmes liés à la zone d’affichage.
Principes fondamentaux de WebGL explique la configuration de la zone d’affichage et du canvas pour les contextes WebGL.
Publier un jeu web qui se charge rapidement explique comment optimiser la taille du canvas pour améliorer les performances.