Rendu au pixel près avec devicePixelContentBox

Combien de pixels y a-t-il vraiment dans 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 au pixel près, en particulier sur les écrans haute densité.

Navigateurs pris en charge

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: non compatible.

Source

Bien que nous utilisions souvent 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 n'ont qu'une relation vague avec les pixels que vous avez à l'écran.

Pendant longtemps, il était assez raisonnable d'estimer la densité de pixels de l'écran de n'importe quel utilisateur à 96 ppp (points par pouce), ce qui signifie qu'un écran donné compte environ 38 pixels par cm. Au fil du temps, les écrans ont augmenté et/ou diminué de taille, ou ont commencé à comporter plus de pixels sur la même surface. Ajoutez à cela le fait que de nombreux contenus sur le Web définissent leurs dimensions, y compris les tailles de police, en px, et vous obtenez un texte illisible sur ces écrans haute densité (HiDPI). Pour contrerattaquer, les navigateurs masquent la densité de pixels réelle de l'écran et prétendent que l'utilisateur dispose d'un écran 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 les mesures et le positionnement. Avant tout rendu, une conversion en pixels physiques est effectuée.

Comment passer de cet écran virtuel à l'écran réel de l'utilisateur ? Saisissez devicePixelRatio. Cette valeur globale indique le nombre de pixels physiques dont vous avez besoin pour former un seul pixel CSS. Si devicePixelRatio (dPR) est 1, vous travaillez sur un écran d'environ 96 PPP. Si vous disposez d'un écran Retina, votre dPR est probablement 2. Sur les téléphones, il n'est pas rare de rencontrer des valeurs dPR plus élevées (et plus étranges) telles que 2, 3 ou même 2.65. Il est essentiel de noter que cette valeur est exacte, mais qu'elle ne vous permet pas de déduire la valeur DPI réelle de l'écran. Un DPR de 2 signifie qu'un pixel CSS sera mappé sur exactement deux pixels physiques.

Il mesure 3 440 pixels de large et la zone d'affichage fait 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 mesure pas exactement 1 cm sur la plupart des écrans.

Enfin, la dPR peut également être affectée par la fonctionnalité de zoom de votre navigateur. Si vous effectuez un zoom avant, le navigateur augmente la valeur dPR signalée, ce qui a pour effet d'agrandir tous les éléments. Si vous cochez devicePixelRatio dans la console DevTools lorsque vous effectuez un zoom, des valeurs fractionnaires s'affichent.

Outils de développement affichant différents devicePixelRatio fractionnaires en raison du zoom.

Ajoutons l'élément <canvas>. Vous pouvez spécifier le nombre de pixels que vous souhaitez que le canevas contienne à 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é à 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 redimensionner le canevas de manière arbitraire à l'aide de toutes les propriétés CSS que vous connaissez et appréciez. Compte tenu de tout ce que nous avons appris jusqu'à présent, vous vous demandez peut-être si ce n'est pas une solution idéale dans tous les cas. 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 peu agréables.

Pour résumer: les éléments de canevas ont une taille donnée pour définir la zone sur laquelle vous pouvez dessiner. Le nombre de pixels du canevas est complètement 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.

Pixel Perfect

Dans certains cas, il est souhaitable d'avoir une mise en correspondance exacte des pixels du canevas avec les pixels physiques. Si ce mappage est réalisé, on parle de "pixel-perfect". Le rendu au pixel près est essentiel pour rendre le texte lisible, en particulier lorsque vous utilisez le rendu au sous-pixel ou lorsque vous affichez des graphiques avec des lignes étroitement alignées de luminosité alternée.

Pour obtenir un canevas le plus proche possible d'un canevas au pixel près sur le Web, l'approche suivante était généralement utilisée:

<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 perspicace se demande peut-être ce qui se passe lorsque dPR n'est pas une valeur entière. C'est une bonne question, et c'est là que réside l'essentiel du problème. En outre, 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 converties en valeurs fractionnaires de pixels CSS. Un élément avec margin-left: 33% peut se terminer par un rectangle comme suit:

DevTools affichant des valeurs de pixel fractionnaires à la suite d'un appel getBoundingClientRect().

Les pixels CSS sont purement virtuels. En théorie, il est donc acceptable d'avoir des fractions de pixel. Mais comment le navigateur détermine-t-il la mise en correspondance avec les pixels physiques ? En effet, les pixels physiques fractionnaires n'existent pas.

Ancrage au pixel

La partie du processus de conversion d'unités qui s'occupe d'aligner les éléments sur les pixels physiques s'appelle "pixel snapping" (alignement sur les pixels). Elle fait ce qu'elle dit: elle aligne les valeurs de pixels fractionnaires sur des valeurs de pixels physiques entières. La procédure exacte varie d'un navigateur à l'autre. Si un élément a une largeur de 791.984px sur un écran dont le dPR est de 1, un navigateur peut l'afficher à 792px pixels physiques, tandis qu'un autre peut l'afficher à 791px. Il ne s'agit que d'un seul pixel, mais un seul pixel peut être préjudiciable aux rendus qui doivent être parfaits. Cela peut entraîner un flou ou même des artefacts plus visibles, comme l'effet moiré.

L'image du haut est un raster de pixels de différentes couleurs. L'image du bas est identique à celle ci-dessus, mais sa largeur et sa hauteur ont été réduites d'un pixel à l'aide d'un étalonnage bilinéaire. Le motif qui apparaît est appelé "effet moiré".
(Vous devrez peut-être ouvrir cette image dans un nouvel onglet pour la voir sans aucune mise à l'échelle appliquée.)

devicePixelContentBox

devicePixelContentBox vous indique la zone de contenu d'un élément en unités de pixels de l'appareil (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 ou version ultérieure pour le moment.

Comme indiqué dans ResizeObserver: c'est comme document.onresize pour les éléments, la fonction de rappel d'un ResizeObserver est 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 qu'ils ne soient peints. Dans le contexte du problème de canevas décrit ci-dessus, nous pouvons profiter de cette opportunité pour ajuster le nombre de pixels sur notre canevas, en nous assurant d'obtenir un mappage exact "un pour un" 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, bien que chaque ResizeObserverEntry fournisse toujours borderBoxSize, contentBoxSize et devicePixelContentBoxSize (à condition que le navigateur le prenne en charge), le rappel ne sera appelé que si l'une des métriques de la zone observée change.

Avec cette nouvelle propriété, nous pouvons même animer la taille et la position de notre canevas (ce qui garantit efficacement des valeurs de pixel fractionnaires) et ne voir aucun effet 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, regardez la démo dans Chrome 84 ou version ultérieure.

Détection de fonctionnalités

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 le 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 ne pouviez pas 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 au pixel près avec <canvas>. devicePixelContentBox est compatible avec Chrome 84 et versions ultérieures.