Combien de pixels contient réellement un canevas ?
Depuis Chrome 84, ResizeObserver est compatible avec une nouvelle mesure de boîte appelée devicePixelContentBox
, qui mesure la dimension de l'élément en pixels physiques. Cela permet d'afficher des graphiques d'une précision optimale, en particulier sur les écrans haute densité.
Contexte : pixels CSS, pixels du canevas et pixels physiques
Bien que nous travaillions souvent avec des unités de longueur abstraites telles que em
, %
ou vh
, tout se résume à des pixels. Chaque fois que nous spécifions la taille ou la position d'un élément en CSS, le moteur de mise en page du navigateur finit par convertir cette valeur en pixels (px
). Il s'agit de "pixels CSS", qui ont une longue histoire et qui n'ont qu'un lien lâche avec les pixels de votre écran.
Pendant longtemps, il était assez raisonnable d'estimer la densité de pixels de l'écran de n'importe qui à 96 DPI ("dots per inch", points par pouce), ce qui signifie que n'importe quel moniteur donné aurait environ 38 pixels par cm. Au fil du temps, les écrans ont grandi et/ou diminué, ou ont commencé à avoir plus de pixels sur la même surface. Si l'on ajoute à cela le fait que de nombreux contenus sur le Web définissent leurs dimensions, y compris la taille de police, en px
, on obtient un texte illisible sur ces écrans haute densité ("HiDPI"). Pour contrer ce problème, les navigateurs masquent la densité de pixels réelle du moniteur et font croire que l'utilisateur dispose d'un écran de 96 DPI. L'unité px
en CSS représente la taille d'un pixel sur cet écran virtuel de 96 DPI, d'où le nom "pixel CSS". Cette unité n'est utilisée que pour la mesure et le positionnement. Avant tout rendu réel, une conversion en pixels physiques a lieu.
Comment passer de cet écran virtuel à l'écran réel de l'utilisateur ? Saisissez devicePixelRatio
. Cette valeur globale indique le nombre de pixels physiques nécessaires pour former un seul pixel CSS. Si devicePixelRatio
(dPR) est défini sur 1
, vous travaillez sur un écran avec une résolution d'environ 96 PPP. Si vous avez un écran Retina, votre dPR est probablement de 2
. Sur les téléphones, il n'est pas rare de rencontrer des valeurs de dPR plus élevées (et plus étranges) comme 2
, 3
ou même 2.65
. Il est essentiel de noter que cette valeur est exacte, mais ne vous permet pas de déduire la valeur DPI réelle de l'écran. Un DPR de 2
signifie qu'un pixel CSS correspond exactement à deux pixels physiques.
1
…Il a une largeur de 3 440 pixels et une zone d'affichage de 79 cm de large.
Cela donne une résolution de 110 DPI. Presque 96, mais pas tout à fait.
C'est également la raison pour laquelle un <div style="width: 1cm; height: 1cm">
ne mesurera pas exactement 1 cm sur la plupart des écrans.
Enfin, le DPR peut également être affecté par la fonctionnalité de zoom de votre navigateur. Si vous effectuez un zoom avant, le navigateur augmente le DPR indiqué, ce qui agrandit tous les éléments. Si vous cochez devicePixelRatio
dans la console des outils de développement pendant que vous effectuez un zoom, vous pouvez voir des valeurs fractionnaires s'afficher.

devicePixelRatio
fractionnaires en raison du zoom.Ajoutons l'élément <canvas>
. Vous pouvez spécifier le nombre de pixels que le canevas doit comporter à l'aide des attributs width
et height
. <canvas width=40 height=30>
correspond donc à un canevas de 40 x 30 pixels. Toutefois, cela ne signifie pas qu'il sera affiché en 40 x 30 pixels. Par défaut, le canevas utilise les attributs width
et height
pour définir sa taille intrinsèque, mais vous pouvez le redimensionner arbitrairement à l'aide de toutes les propriétés CSS que vous connaissez et appréciez. Avec tout ce que nous avons appris jusqu'à présent, vous vous êtes peut-être rendu compte que cette approche n'est pas idéale dans tous les scénarios. Un pixel sur le canevas peut finir par couvrir plusieurs pixels physiques ou seulement une fraction d'un pixel physique. Cela peut entraîner des artefacts visuels désagréables.
En résumé, les éléments Canvas ont une taille donnée pour définir la zone sur laquelle vous pouvez dessiner. Le nombre de pixels du canevas est totalement indépendant de la taille d'affichage du canevas, spécifiée en pixels CSS. Le nombre de pixels CSS n'est pas le même que le nombre de pixels physiques.
La perfection Pixel
Dans certains scénarios, il est souhaitable d'avoir un mappage exact des pixels du canevas vers les pixels physiques. Si ce mappage est réalisé, on parle de "pixel perfect". Le rendu parfait au pixel près est essentiel pour que le texte soit lisible, en particulier lorsque vous utilisez le rendu de sous-pixels ou lorsque vous affichez des graphiques avec des lignes étroitement alignées de luminosité alternée.
Pour obtenir un canevas aussi proche que possible d'un canevas parfait au pixel près sur le Web, l'approche suivante a été plus ou moins la solution de référence :
<style>
/* … styles that affect the canvas' size … */
</style>
<canvas id="myCanvas"></canvas>
<script>
const cvs = document.querySelector('#myCanvas');
// Get the canvas' size in CSS pixels
const rectangle = cvs.getBoundingClientRect();
// Convert it to real pixels. Ish.
cvs.width = rectangle.width * devicePixelRatio;
cvs.height = rectangle.height * devicePixelRatio;
// Start drawing…
</script>
Le lecteur attentif se demandera peut-être ce qui se passe lorsque le DPR n'est pas une valeur entière. C'est une bonne question, car c'est précisément là que réside le cœur du problème. De plus, si vous spécifiez la position ou la taille d'un élément à l'aide de pourcentages, de vh
ou d'autres valeurs indirectes, il est possible qu'elles soient résolues en valeurs de pixels CSS fractionnaires. Un élément avec margin-left: 33%
peut se retrouver avec un rectangle comme celui-ci :

getBoundingClientRect()
.Les pixels CSS sont purement virtuels. Il est donc théoriquement possible d'avoir des fractions de pixel. Mais comment le navigateur détermine-t-il le mappage avec les pixels physiques ? Les pixels physiques fractionnaires n'existent pas.
Alignement sur les pixels
La partie du processus de conversion d'unité qui permet d'aligner les éléments sur les pixels physiques est appelée "alignement sur les pixels". Elle fait ce qu'elle dit : elle aligne les valeurs de pixels fractionnaires sur les valeurs de pixels physiques entières. La façon dont cela se produit exactement varie d'un navigateur à l'autre. Si nous avons un élément d'une largeur de 791.984px
sur un écran où le DPR est de 1, un navigateur peut afficher l'élément à 792px
pixels physiques, tandis qu'un autre navigateur peut l'afficher à 791px
. Il ne s'agit que d'un seul pixel, mais cela peut nuire aux rendus qui doivent être parfaits. Cela peut entraîner un flou ou des artefacts encore plus visibles, comme l'effet moiré.

(Vous devrez peut-être ouvrir cette image dans un nouvel onglet pour la voir sans mise à l'échelle.)
devicePixelContentBox
devicePixelContentBox
vous donne la boîte de contenu d'un élément en unités de pixels de l'appareil (c'est-à-dire en pixels physiques). Il fait partie de ResizeObserver
. Bien que ResizeObserver soit désormais compatible avec tous les principaux navigateurs depuis Safari 13.1, la propriété devicePixelContentBox
n'est disponible que dans Chrome 84 et versions ultérieures pour le moment.
Comme indiqué dans ResizeObserver
: c'est comme document.onresize
pour les éléments, la fonction de rappel d'un ResizeObserver
sera appelée avant la peinture et après la mise en page. Cela signifie que le paramètre entries
du rappel contiendra les tailles de tous les éléments observés juste avant leur affichage. Dans le contexte du problème de canevas décrit ci-dessus, nous pouvons profiter de cette occasion pour ajuster le nombre de pixels sur notre canevas, en veillant à obtenir un mappage exact entre les pixels du canevas et les pixels physiques.
const observer = new ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === canvas);
canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
canvas.height = entry.devicePixelContentBoxSize[0].blockSize;
/* … render to canvas … */
});
observer.observe(canvas, {box: ['device-pixel-content-box']});
La propriété box
de l'objet d'options pour observer.observe()
vous permet de définir les tailles que vous souhaitez observer. Ainsi, même si chaque ResizeObserverEntry
fournit toujours borderBoxSize
, contentBoxSize
et devicePixelContentBoxSize
(à condition que le navigateur le prenne en charge), le rappel n'est appelé que si l'une des métriques de boîte observées change.
Grâce à cette nouvelle propriété, nous pouvons même animer la taille et la position de notre canevas (en garantissant effectivement des valeurs de pixels fractionnaires) sans voir d'effets de moiré sur le rendu. Si vous souhaitez voir l'effet Moiré sur l'approche utilisant getBoundingClientRect()
et comment la nouvelle propriété ResizeObserver
vous permet de l'éviter, consultez la démonstration dans Chrome 84 ou version ultérieure.
Détection de caractéristiques
Pour vérifier si le navigateur d'un utilisateur est compatible avec devicePixelContentBox
, nous pouvons observer n'importe quel élément et vérifier si la propriété est présente sur ResizeObserverEntry
:
function hasDevicePixelContentBox() {
return new Promise((resolve) => {
const ro = new ResizeObserver((entries) => {
resolve(entries.every((entry) => 'devicePixelContentBoxSize' in entry));
ro.disconnect();
});
ro.observe(document.body, {box: ['device-pixel-content-box']});
}).catch(() => false);
}
if (!(await hasDevicePixelContentBox())) {
// The browser does NOT support devicePixelContentBox
}
Conclusion
Les pixels sont un sujet étonnamment complexe sur le Web. Jusqu'à présent, vous n'aviez aucun moyen de connaître le nombre exact de pixels physiques qu'un élément occupe sur l'écran de l'utilisateur. La nouvelle propriété devicePixelContentBox
sur un ResizeObserverEntry
vous fournit cette information et vous permet d'effectuer des rendus parfaits au pixel près avec <canvas>
. devicePixelContentBox
est compatible avec Chrome 84 et versions ultérieures.