# Conception Réactive

Ceci est le deuxième article d'une série d'articles sur three.js. Le premier article traitait des fondamentaux. Si vous ne l'avez pas encore lu, vous voudrez peut-être commencer par là.

Cet article explique comment rendre votre application three.js réactive à toute situation. Rendre une page web réactive fait généralement référence à la capacité de la page à s'afficher correctement sur des écrans de différentes tailles, des ordinateurs de bureau aux tablettes et téléphones.

Pour three.js, il y a encore plus de situations à considérer. Par exemple, un éditeur 3D avec des contrôles à gauche, à droite, en haut ou en bas est quelque chose que nous pourrions vouloir gérer. Un diagramme interactif au milieu d'un document est un autre exemple.

Le dernier exemple que nous avions utilisait un simple canevas sans CSS et sans taille

    <canvas id="c"></canvas>

Ce canevas a par défaut une taille de 300x150 pixels CSS.

Dans la plateforme web, la méthode recommandée pour définir la taille d'un élément est d'utiliser CSS.

Faisons en sorte que le canevas remplisse la page en ajoutant du CSS

    <style>
    html, body {
       margin: 0;
       height: 100%;
    }
    #c {
       width: 100%;
       height: 100%;
       display: block;
    }
    </style>

En HTML, le corps (body) a une marge de 5 pixels par défaut, donc définir la marge à 0 supprime cette marge. Définir la hauteur de html et body à 100 % leur permet de remplir la fenêtre. Sinon, ils ne sont que de la taille du contenu qui les remplit.

Ensuite, nous disons à l'élément `id=c` de prendre 100 % de la taille de son conteneur, qui est ici le corps du document.

Enfin, nous définissons son mode `display` à `block`. Le mode d'affichage par défaut d'un canevas est `inline`. Les éléments inline peuvent finir par ajouter des espaces blancs à l'affichage. En définissant le canevas à `block`, ce problème disparaît.

Voici le résultat

cliquez ici pour ouvrir dans une fenêtre séparée

Vous pouvez voir que le canevas remplit maintenant la page, mais il y a 2 problèmes. Premièrement, nos cubes sont étirés. Ce ne sont pas des cubes, mais plutôt des boîtes. Trop hauts ou trop larges. Ouvrez l'exemple dans sa propre fenêtre et redimensionnez-le. Vous verrez comment les cubes s'étirent en largeur et en hauteur.

Le deuxième problème est qu'ils semblent avoir une faible résolution ou être pixellisés et flous. Élargissez beaucoup la fenêtre et vous verrez vraiment le problème.

Résolvons d'abord le problème de l'étirement. Pour ce faire, nous devons définir l'aspect de la caméra sur l'aspect de la taille d'affichage du canevas. Nous pouvons le faire en examinant les propriétés `clientWidth` et `clientHeight` du canevas.

Nous allons mettre à jour notre boucle de rendu comme ceci

    function render(time) {
      time *= 0.001;

    +  const canvas = renderer.domElement;
    +  camera.aspect = canvas.clientWidth / canvas.clientHeight;
    +  camera.updateProjectionMatrix();

      ...

Maintenant, les cubes ne devraient plus être déformés.

cliquez ici pour ouvrir dans une fenêtre séparée

Ouvrez l'exemple dans une fenêtre séparée et redimensionnez la fenêtre. Vous devriez voir que les cubes ne sont plus étirés en hauteur ou en largeur. Ils conservent le bon aspect quelle que soit la taille de la fenêtre.

Maintenant, résolvons le problème de la pixellisation.

Les éléments Canvas ont 2 tailles. Une taille est la taille à laquelle le canevas est affiché sur la page. C'est ce que nous définissons avec CSS. L'autre taille est le nombre de pixels dans le canevas lui-même. Ce n'est pas différent d'une image. Par exemple, nous pourrions avoir une image de 128x64 pixels et l'afficher en 400x200 pixels en utilisant CSS.

    <img src="some128x64image.jpg" style="width:400px; height:200px">

La taille interne d'un canevas, sa résolution, est souvent appelée sa taille de drawingbuffer. Dans three.js, nous pouvons définir la taille du drawingbuffer du canevas en appelant `renderer.setSize`. Quelle taille devrions-nous choisir ? La réponse la plus évidente est « la même taille que celle affichée par le canevas ». Encore une fois, pour ce faire, nous pouvons examiner les propriétés `clientWidth` et `clientHeight` du canevas.

Écrivons une fonction qui vérifie si le canevas du renderer n'a pas déjà la taille à laquelle il est affiché et, si ce n'est pas le cas, définit sa taille.

    function resizeRendererToDisplaySize(renderer) {
      const canvas = renderer.domElement;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;
      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        renderer.setSize(width, height, false);
      }
      return needResize;
    }

Notez que nous vérifions si le canevas a réellement besoin d'être redimensionné. Le redimensionnement du canevas est une partie intéressante de la spécification du canevas, et il est préférable de ne pas définir la même taille si elle est déjà celle que nous souhaitons.

Une fois que nous savons si nous devons redimensionner ou non, nous appelons alors `renderer.setSize` et passons la nouvelle largeur et hauteur. Il est important de passer `false` à la fin. `renderer.setSize` définit par défaut la taille CSS du canevas, mais ce n'est pas ce que nous voulons. Nous voulons que le navigateur continue à fonctionner comme il le fait pour tous les autres éléments, c'est-à-dire en utilisant CSS pour déterminer la taille d'affichage de l'élément. Nous ne voulons pas que les canevas utilisés par three soient différents des autres éléments.

Notez que notre fonction retourne true si le canevas a été redimensionné. Nous pouvons l'utiliser pour vérifier s'il y a d'autres éléments que nous devrions mettre à jour. Modifions notre boucle de rendu pour utiliser la nouvelle fonction

    function render(time) {
      time *= 0.001;

    +  if (resizeRendererToDisplaySize(renderer)) {
    +    const canvas = renderer.domElement;
    +    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    +    camera.updateProjectionMatrix();
    +  }

      ...

Comme l'aspect ne changera que si la taille d'affichage du canevas a changé, nous ne définissons l'aspect de la caméra que si `resizeRendererToDisplaySize` retourne `true`.

cliquez ici pour ouvrir dans une fenêtre séparée

Il devrait maintenant rendre avec une résolution qui correspond à la taille d'affichage du canevas.

Pour illustrer le fait de laisser CSS gérer le redimensionnement, prenons notre code et mettons-le dans un fichier `.js` séparé. Voici ensuite quelques exemples supplémentaires où nous laissons CSS choisir la taille, et notez que nous n'avons eu à modifier aucun code pour qu'ils fonctionnent.

Mettons nos cubes au milieu d'un paragraphe de texte.

cliquez ici pour ouvrir dans une fenêtre séparée

et voici notre même code utilisé dans une disposition de style éditeur où la zone de contrôle à droite peut être redimensionnée.

cliquez ici pour ouvrir dans une fenêtre séparée

Le point important à noter est qu'aucun code n'a été modifié. Seuls notre HTML et notre CSS ont changé.

## Gestion des écrans HD-DPI

HD-DPI signifie écrans à haute densité de points par pouce (high-density dot per inch displays). C'est le cas de la plupart des Macs actuels et de nombreuses machines Windows, ainsi que de la quasi-totalité des smartphones.

La façon dont cela fonctionne dans le navigateur est qu'ils utilisent des pixels CSS pour définir les tailles, qui sont censées être les mêmes quelle que soit la résolution de l'écran. Le navigateur se contentera de rendre le texte avec plus de détails, mais avec la même taille physique.

Il existe différentes façons de gérer les écrans HD-DPI avec three.js.

La première consiste simplement à ne rien faire de spécial. C'est sans doute la méthode la plus courante. Le rendu graphique 3D nécessite beaucoup de puissance de traitement GPU. Les GPU mobiles ont moins de puissance que les ordinateurs de bureau, du moins en 2018, et pourtant les téléphones portables ont souvent des écrans à très haute résolution. Les téléphones haut de gamme actuels ont un rapport HD-DPI de 3x, ce qui signifie que pour chaque pixel d'un écran non HD-DPI, ces téléphones ont 9 pixels. Cela signifie qu'ils doivent effectuer 9 fois le rendu.

Calculer 9 fois les pixels représente beaucoup de travail, donc si nous laissons le code tel quel, nous calculerons 1x les pixels et le navigateur se contentera de l'afficher à 3x la taille (3x par 3x = 9x pixels).

Pour toute application three.js lourde, c'est probablement ce que vous voudrez, sinon vous risquez d'avoir une fréquence d'images lente.

Cela dit, si vous souhaitez réellement rendre à la résolution de l'appareil, il existe plusieurs façons de le faire dans three.js.

L'une consiste à indiquer à three.js un multiplicateur de résolution en utilisant `renderer.setPixelRatio`. Vous demandez au navigateur quel est le multiplicateur entre les pixels CSS et les pixels de l'appareil, et vous le passez à three.js

     renderer.setPixelRatio(window.devicePixelRatio);

Après cela, tout appel à `renderer.setSize` utilisera magiquement la taille demandée multipliée par le rapport de pixels que vous avez passé. Ceci est fortement DÉCONSEILLÉ. Voir ci-dessous

L'autre méthode consiste à le faire vous-même lorsque vous redimensionnez le canevas.

        function resizeRendererToDisplaySize(renderer) {
          const canvas = renderer.domElement;
          const pixelRatio = window.devicePixelRatio;
          const width  = Math.floor( canvas.clientWidth  * pixelRatio );
          const height = Math.floor( canvas.clientHeight * pixelRatio );
          const needResize = canvas.width !== width || canvas.height !== height;
          if (needResize) {
            renderer.setSize(width, height, false);
          }
          return needResize;
        }

Cette seconde méthode est objectivement meilleure. Pourquoi ? Parce que cela signifie que j'obtiens ce que je demande. Il existe de nombreux cas lors de l'utilisation de three.js où nous devons connaître la taille réelle du drawingBuffer du canevas. Par exemple, lors de la création d'un filtre de post-traitement, ou si nous créons un shader qui accède à `gl_FragCoord`, si nous faisons une capture d'écran, ou lisons des pixels pour la sélection GPU, pour dessiner dans un canevas 2D, etc... Il existe de nombreux cas où si nous utilisons `setPixelRatio`, notre taille réelle sera différente de la taille demandée, et nous devrons deviner quand utiliser la taille que nous avons demandée et quand utiliser la taille que three.js utilise réellement. En le faisant nous-mêmes, nous savons toujours que la taille utilisée est la taille que nous avons demandée. Il n'y a pas de cas particulier où de la magie opère en coulisses.

Voici un exemple utilisant le code ci-dessus.

cliquez ici pour ouvrir dans une fenêtre séparée

Il peut être difficile de voir la différence, mais si vous avez un écran HD-DPI et que vous comparez cet exemple à ceux ci-dessus, vous devriez remarquer que les bords sont plus nets.

Cet article a couvert un sujet très basique mais fondamental. Ensuite, passons rapidement en revue les primitives de base que three.js fournit.