Simuler une déficience de la vision des couleurs dans le moteur de rendu Blink

Cet article explique pourquoi et comment nous avons implémenté la simulation de déficience visuelle dans les outils de développement et le moteur de rendu Blink.

Arrière-plan: mauvais contraste des couleurs

Le texte à faible contraste est le problème d'accessibilité le plus courant détectable automatiquement sur le Web.

Liste des problèmes d'accessibilité courants sur le Web. Le texte à faible contraste est de loin le problème le plus courant.

Selon l'analyse d'accessibilité de WebAIM sur les 1 million de premiers sites Web, plus de 86% des pages d'accueil présentent un contraste faible. En moyenne, chaque page d'accueil comporte 36 instances distinctes de texte à faible contraste.

Utiliser les outils pour les développeurs pour identifier, comprendre et résoudre les problèmes de contraste

Les outils pour les développeurs Chrome peuvent aider les développeurs et les concepteurs à améliorer le contraste et à choisir des jeux de couleurs plus accessibles pour les applications Web:

Nous avons récemment ajouté un nouvel outil à cette liste, qui est un peu différent des autres. Les outils ci-dessus visent principalement à afficher des informations sur le rapport de contraste et à vous proposer des options pour le corriger. Nous avons réalisé qu'il manquait encore aux développeurs un moyen de mieux comprendre ce domaine de problèmes. Pour y remédier, nous avons implémenté la simulation de déficience visuelle dans l'onglet "Rendering" (Rendu) des outils de développement.

Dans Puppeteer, la nouvelle API page.emulateVisionDeficiency(type) vous permet d'activer ces simulations par programmation.

Déficiences de la vision des couleurs

Près d'une personne sur 20 souffre d'un trouble de la perception des couleurs (également appelé "daltonisme", un terme moins précis). Cette déficience se traduit par des difficultés à distinguer les couleurs, ce qui peut aggraver les problèmes de contraste.

Image colorée de crayons fondus, sans simulation de déficience de la vision des couleurs
Image colorée de crayons fondus, sans simulation de déficience de la vision des couleurs.
ALT_TEXT_HERE
Impact de la simulation d'achromatopsie sur une image colorée de crayons fondus.
Impact de la simulation de la deuteranopie sur une image colorée de crayons fondus.
Impact de la simulation de la deuteranopie sur une image colorée de crayons fondus.
Impact de la simulation de la protanopie sur une image colorée de crayons fondus.
Impact de la simulation de protanopie sur une image colorée de crayons fondus.
Impact de la simulation de la tritanopie sur une image colorée de crayons fondus.
Impact de la simulation de la tritanopie sur une image colorée de crayons fondus.

En tant que développeur et si votre vision est normale, il est possible que les outils de développement affichent un mauvais rapport de contraste pour des paires de couleurs qui vous semblent visuellement correctes. En effet, les formules de rapport de contraste tiennent compte de ces déficiences visuelles. Il peut arriver dans certains cas que vous puissiez lire du texte à faible contraste, à la différence des personnes qui souffrent de ces troubles de la vision.

En permettant aux concepteurs et aux développeurs de simuler l'effet de ces déficiences visuelles sur leurs propres applications Web, nous souhaitons fournir la pièce manquante: les outils de développement peuvent désormais vous aider à trouver et corriger les problèmes de contraste, mais aussi à les comprendre.

Simuler des déficiences de vision des couleurs avec HTML, CSS, SVG et C++

Avant de nous plonger dans l'implémentation de notre fonctionnalité dans le moteur de rendu Blink, il est utile de comprendre comment implémenter une fonctionnalité équivalente à l'aide de la technologie Web.

Vous pouvez considérer chacune de ces simulations de déficience visuelle comme une superposition couvrant l'intégralité de la page. La plate-forme Web dispose d'un moyen de le faire: les filtres CSS. Avec la propriété CSS filter, vous pouvez utiliser certaines fonctions de filtre prédéfinies, telles que blur, contrast, grayscale, hue-rotate, etc. Pour un contrôle encore plus poussé, la propriété filter accepte également une URL pouvant pointer vers une définition de filtre SVG personnalisée:

<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

L'exemple ci-dessus utilise une définition de filtre personnalisée basée sur une matrice de couleurs. D'un point de vue conceptuel, la valeur de couleur [Red, Green, Blue, Alpha] de chaque pixel est multipliée par une matrice pour créer une nouvelle couleur [R′, G′, B′, A′].

Chaque ligne de la matrice contient cinq valeurs: un multiplicateur pour R, G, B et A (de gauche à droite), ainsi qu'une cinquième valeur pour un décalage constant. Elle comporte quatre lignes: la première ligne de la matrice est utilisée pour calculer la nouvelle valeur de rouge, la deuxième pour le vert, la troisième pour le bleu et la dernière pour l'alpha.

Vous vous demandez peut-être d'où viennent les chiffres exacts de notre exemple. Pourquoi cette matrice de couleurs est-elle une bonne approximation de la deuteranopie ? La réponse est: la science ! Ces valeurs sont basées sur un modèle de simulation de déficience de la vision des couleurs physiologiquement précis, développé par Machado, Oliveira et Fernandes.

Nous avons donc ce filtre SVG, et nous pouvons maintenant l'appliquer à des éléments arbitraires de la page à l'aide de CSS. Nous pouvons répéter le même schéma pour d'autres déficiences visuelles. Voici une démonstration:

Si nous le souhaitions, nous pourrions créer notre fonctionnalité DevTools comme suit: lorsque l'utilisateur émule un trouble de la vision dans l'interface utilisateur de DevTools, nous injectons le filtre SVG dans le document inspecté, puis nous appliquons le style de filtre à l'élément racine. Toutefois, cette approche présente plusieurs problèmes:

  • La page peut déjà comporter un filtre sur son élément racine, que notre code peut ensuite remplacer.
  • La page peut déjà contenir un élément avec id="deuteranopia", ce qui entre en conflit avec notre définition de filtre.
  • La page peut s'appuyer sur une certaine structure DOM, et en insérant <svg> dans le DOM, nous pourrions enfreindre ces hypothèses.

Mis à part les cas particuliers, le principal problème de cette approche est que nous apporterions des modifications observables par programmation à la page. Si un utilisateur de DevTools inspecte le DOM, il peut soudainement voir un élément <svg> qu'il n'a jamais ajouté ou un filter CSS qu'il n'a jamais écrit. Ce serait déroutant ! Pour implémenter cette fonctionnalité dans DevTools, nous avons besoin d'une solution qui ne présente pas ces inconvénients.

Voyons comment rendre cela moins intrusif. Cette solution comporte deux parties que nous devons masquer: 1) le style CSS avec la propriété filter et 2) la définition du filtre SVG, qui fait actuellement partie du DOM.

<!-- Part 1: the CSS style with the filter property -->
<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<!-- Part 2: the SVG filter definition -->
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

Éviter la dépendance SVG dans le document

Commençons par la partie 2: comment éviter d'ajouter le SVG au DOM ? Vous pouvez par exemple le déplacer vers un fichier SVG distinct. Nous pouvons copier le <svg>…</svg> du code HTML ci-dessus et l'enregistrer en tant que filter.svg, mais nous devons d'abord apporter quelques modifications. Le code SVG intégré au code HTML suit les règles d'analyse HTML. Cela signifie que vous pouvez omettre les guillemets autour des valeurs d'attribut dans certains cas. Toutefois, les fichiers SVG distincts sont censés être des fichiers XML valides, et l'analyse XML est beaucoup plus stricte que l'analyse HTML. Voici à nouveau notre extrait SVG-in-HTML:

<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

Pour que ce fichier SVG autonome (et donc XML) soit valide, nous devons apporter quelques modifications. Pouvez-vous deviner lequel ?

<svg xmlns="http://www.w3.org/2000/svg">
 
<filter id="deuteranopia">
   
<feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000"
/>
 
</filter>
</svg>

La première modification concerne la déclaration d'espace de noms XML en haut. Le second ajout est le "solidus", la barre oblique qui indique que la balise <feColorMatrix> ouvre et ferme l'élément. Cette dernière modification n'est pas vraiment nécessaire (nous pourrions nous contenter de la balise de fermeture </feColorMatrix> explicite), mais comme XML et SVG-in-HTML acceptent cette abréviation />, nous pouvons tout aussi bien l'utiliser.

Quoi qu'il en soit, avec ces modifications, nous pouvons enfin enregistrer le fichier au format SVG valide et y faire référence à partir de la valeur de la propriété CSS filter dans notre document HTML:

<style>
  :root {
    filter: url(filters.svg#deuteranopia);
  }
</style>

Hourra ! Nous n'avons plus besoin d'injecter de SVG dans le document. C'est déjà beaucoup mieux. Mais… nous dépendons désormais d'un fichier distinct. Il s'agit toujours d'une dépendance. Pouvons-nous nous en débarrasser ?

Il s'avère que nous n'avons pas besoin de fichier. Nous pouvons encoder l'intégralité du fichier dans une URL à l'aide d'une URL de données. Pour ce faire, nous prenons littéralement le contenu du fichier SVG que nous avions auparavant, ajoutons le préfixe data:, configurons le type MIME approprié, et nous obtenons une URL de données valide qui représente le même fichier SVG:

data:image/svg+xml,
  <svg xmlns="http://www.w3.org/2000/svg">
    <filter id="deuteranopia">
      <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                             0.280  0.673  0.047  0.000  0.000
                            -0.012  0.043  0.969  0.000  0.000
                             0.000  0.000  0.000  1.000  0.000" />
    </filter>
  </svg>

L'avantage est que nous n'avons plus besoin de stocker le fichier n'importe où, ni de le charger à partir du disque ou du réseau pour l'utiliser dans notre document HTML. Au lieu de faire référence au nom de fichier comme nous l'avons fait précédemment, nous pouvons désormais pointer vers l'URL des données:

<style>
  :root {
    filter: url('data:image/svg+xml,\
      <svg xmlns="http://www.w3.org/2000/svg">\
        <filter id="deuteranopia">\
          <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000\
                                 0.280  0.673  0.047  0.000  0.000\
                                -0.012  0.043  0.969  0.000  0.000\
                                 0.000  0.000  0.000  1.000  0.000" />\
        </filter>\
      </svg>#deuteranopia');
  }
</style>

À la fin de l'URL, nous spécifions toujours l'ID du filtre que nous souhaitons utiliser, comme précédemment. Notez qu'il n'est pas nécessaire d'encoder le document SVG en Base64 dans l'URL. Cela ne ferait qu'altérer la lisibilité et augmenter la taille du fichier. Nous avons ajouté des barres obliques inverses à la fin de chaque ligne pour nous assurer que les caractères de nouvelle ligne de l'URL des données ne terminent pas la chaîne littérale CSS.

Jusqu'à présent, nous n'avons parlé que de la simulation de déficiences visuelles à l'aide de la technologie Web. Fait intéressant, notre implémentation finale dans le moteur de rendu Blink est en fait assez similaire. Voici un utilitaire d'assistance C++ que nous avons ajouté pour créer une URL de données avec une définition de filtre donnée, en fonction de la même technique:

AtomicString CreateFilterDataUrl(const char* piece) {
  AtomicString url =
      "data:image/svg+xml,"
        "<svg xmlns=\"http://www.w3.org/2000/svg\">"
          "<filter id=\"f\">" +
            StringView(piece) +
          "</filter>"
        "</svg>"
      "#f";
  return url;
}

Voici comment nous l'utilisons pour créer tous les filtres dont nous avons besoin:

AtomicString CreateVisionDeficiencyFilterUrl(VisionDeficiency vision_deficiency) {
  switch (vision_deficiency) {
    case VisionDeficiency::kAchromatopsia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kBlurredVision:
      return CreateFilterDataUrl("<feGaussianBlur stdDeviation=\"2\"/>");
    case VisionDeficiency::kDeuteranopia:
      return CreateFilterDataUrl(
          "<feColorMatrix values=\""
          " 0.367  0.861 -0.228  0.000  0.000 "
          " 0.280  0.673  0.047  0.000  0.000 "
          "-0.012  0.043  0.969  0.000  0.000 "
          " 0.000  0.000  0.000  1.000  0.000 "
          "\"/>");
    case VisionDeficiency::kProtanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kTritanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kNoVisionDeficiency:
      NOTREACHED();
      return "";
  }
}

Notez que cette technique nous permet d'accéder à toute la puissance des filtres SVG sans avoir à réimplémenter quoi que ce soit ni à réinventer la roue. Nous implémentons une fonctionnalité de rendu Blink, mais nous le faisons en exploitant la plate-forme Web.

Nous avons donc découvert comment créer des filtres SVG et les transformer en URL de données que nous pouvons utiliser dans la valeur de la propriété CSS filter. Pouvez-vous imaginer un problème avec cette technique ? En réalité, nous ne pouvons pas nous fier à ce que l'URL de données soit chargée dans tous les cas, car la page cible peut comporter un Content-Security-Policy qui bloque les URL de données. Notre implémentation finale au niveau de Blink prend soin de contourner le CSP pour ces URL de données "internes" lors du chargement.

Mis à part les cas limites, nous avons fait de bons progrès. Étant donné que nous ne dépendons plus de la présence d'<svg> intégré dans le même document, nous avons réduit notre solution à une seule définition de propriété CSS filter autonome. Parfait ! Supprimons-la également.

Éviter la dépendance CSS dans le document

Pour résumer, voici où nous en sommes:

<style>
  :root {
    filter: url('data:…');
  }
</style>

Nous dépendons toujours de cette propriété CSS filter, qui peut remplacer un filter dans le document réel et causer des problèmes. Il s'afficherait également lors de l'inspection des styles calculés dans DevTools, ce qui serait source de confusion. Comment pouvons-nous éviter ces problèmes ? Nous devons trouver un moyen d'ajouter un filtre au document sans que les développeurs puissent l'observer de manière programmatique.

Une idée a été proposée : créer une propriété CSS interne à Chrome qui se comporte comme filter, mais qui porte un nom différent, comme --internal-devtools-filter. Nous pourrions ensuite ajouter une logique spéciale pour nous assurer que cette propriété ne s'affiche jamais dans DevTools ni dans les styles calculés du DOM. Nous pouvons même nous assurer qu'il ne fonctionne que sur l'élément dont nous avons besoin: l'élément racine. Toutefois, cette solution n'est pas idéale: nous dupliquerions une fonctionnalité qui existe déjà avec filter. Même si nous nous efforçons de masquer cette propriété non standard, les développeurs Web pourraient quand même la découvrir et commencer à l'utiliser, ce qui serait mauvais pour la plate-forme Web. Nous avons besoin d'un autre moyen d'appliquer un style CSS sans qu'il soit observable dans le DOM. Auriez-vous quelqu'un en tête ?

La spécification CSS comporte une section présentant le modèle de mise en forme visuelle qu'elle utilise. L'un des concepts clés est le voile de fenêtre. Il s'agit de la vue visuelle à travers laquelle les utilisateurs consultent la page Web. Un concept étroitement lié est le bloc contenant initial, qui est un peu comme un viewport <div> stylable qui n'existe qu'au niveau des spécifications. La spécification fait référence à ce concept de "fenêtre d'affichage" partout. Par exemple, savez-vous comment le navigateur affiche des barres de défilement lorsque le contenu ne tient pas dans l'écran ? Tout cela est défini dans la spécification CSS, en fonction de cette "fenêtre d'affichage".

Ce viewport existe également dans le moteur de rendu Blink, en tant que détail d'implémentation. Voici le code qui applique les styles de vue par défaut conformément aux spécifications:

scoped_refptr<ComputedStyle> StyleResolver::StyleForViewport() {
  scoped_refptr<ComputedStyle> viewport_style =
      InitialStyleForElement(GetDocument());
  viewport_style->SetZIndex(0);
  viewport_style->SetIsStackingContextWithoutContainment(true);
  viewport_style->SetDisplay(EDisplay::kBlock);
  viewport_style->SetPosition(EPosition::kAbsolute);
  viewport_style->SetOverflowX(EOverflow::kAuto);
  viewport_style->SetOverflowY(EOverflow::kAuto);
  // …
  return viewport_style;
}

Vous n'avez pas besoin de comprendre le langage C++ ni les subtilités du moteur de style de Blink pour voir que ce code gère les z-index, display, position et overflow du viewport (ou plus précisément du bloc contenant initial). Ce sont tous des concepts que vous connaissez peut-être déjà en CSS. Il existe d'autres éléments magiques liés aux contextes d'empilement, qui ne se traduisent pas directement en propriété CSS. Toutefois, vous pouvez considérer cet objet viewport comme un élément pouvant être stylisé à l'aide de CSS depuis Blink, comme un élément DOM, sauf qu'il ne fait pas partie du DOM.

Nous obtenons ainsi exactement ce que nous voulons. Nous pouvons appliquer nos styles filter à l'objet viewport, ce qui affecte visuellement le rendu, sans interférer avec les styles de page observables ni le DOM.

Conclusion

Pour récapituler notre petit voyage, nous avons commencé par créer un prototype à l'aide de la technologie Web plutôt qu'avec C++, puis nous avons commencé à déplacer certaines parties vers le moteur de rendu Blink.

  • Nous avons d'abord rendu notre prototype plus autonome en insérant des URL de données dans le code.
  • Nous avons ensuite rendu ces URL de données internes compatibles avec le CSP en faisant une exception pour leur chargement.
  • Nous avons rendu notre implémentation indépendante du DOM et non observable par programmation en déplaçant les styles vers viewport, qui est interne à Blink.

La particularité de cette implémentation est que notre prototype HTML/CSS/SVG a fini par influencer la conception technique finale. Nous avons trouvé un moyen d'utiliser la plate-forme Web, même dans le moteur de rendu Blink.

Pour en savoir plus, consultez notre proposition de conception ou le bug de suivi Chromium, qui fait référence à tous les correctifs associés.

Télécharger les canaux de prévisualisation

Envisagez d'utiliser Chrome Canary, Dev ou Bêta comme navigateur de développement par défaut. Ces canaux de prévisualisation vous donnent accès aux dernières fonctionnalités de DevTools, vous permettent de tester les API de plate-forme Web de pointe et vous aident à détecter les problèmes sur votre site avant vos utilisateurs.

Contacter l'équipe des outils pour les développeurs Chrome

Utilisez les options suivantes pour discuter des nouvelles fonctionnalités, des mises à jour ou de tout autre élément lié aux outils pour les développeurs.