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é une simulation de déficience visuelle dans les outils de développement et le moteur de rendu Blink.

Arrière-plan: un mauvais contraste des couleurs

Le texte à faible contraste est le problème d'accessibilité le plus courant, qui peut être détecté automatiquement sur le Web.

Liste des problèmes d'accessibilité courants sur le Web. Les textes à faible contraste sont de loin le problème le plus courant.

Selon une analyse d'accessibilité d'un million de sites Web principaux réalisée par WebAIM, plus de 86% des pages d'accueil présentent un faible contraste. En moyenne, chaque page d'accueil comporte 36 instances distinctes de texte à faible contraste.

Identifier, comprendre et résoudre les problèmes de contraste à l'aide des outils de développement

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, et il est un peu différent des autres. Les outils ci-dessus se concentrent principalement sur les informations sur le rapport de contraste et vous offrent des options pour le corriger. Nous avons réalisé qu'il manquait toujours aux développeurs un moyen de mieux comprendre cet espace problématique. Pour résoudre ce problème, nous avons implémenté la simulation de déficience visuelle dans l'onglet "Affichage" des outils de développement.

Dans Puppeteer, la nouvelle API page.emulateVisionDeficiency(type) vous permet d'activer ces simulations de manière programmatique.

Déficiences de la vision des couleurs

Près d'une personne sur 20 souffre d'une déficience de la vision des couleurs (également appelée "daltonisme", un terme moins précis). De tels déficiences rendent plus difficile la distinction entre les couleurs, ce qui peut amplifier les problèmes de contraste.

<ph type="x-smartling-placeholder">
</ph> Une image colorée de crayons fondus, sans simulation de déficience de la vision des couleurs <ph type="x-smartling-placeholder">
</ph> Une image colorée de crayons fondus, qui ne montre aucune déficience visuelle des couleurs.
<ph type="x-smartling-placeholder">
</ph> ALT_TEXT_HERE <ph type="x-smartling-placeholder">
</ph> L'impact de la simulation de l'achromatopsie sur une image colorée de crayons fondus.
<ph type="x-smartling-placeholder">
</ph> L&#39;impact de la simulation de deutéranopie sur une image colorée de crayons fondus. <ph type="x-smartling-placeholder">
</ph> L'impact de la simulation de deutéranopie sur une image colorée de crayons fondus.
<ph type="x-smartling-placeholder">
</ph> L&#39;impact de la simulation de la protanopie sur une image colorée de crayons fondus. <ph type="x-smartling-placeholder">
</ph> L'impact de la simulation de la protanopie sur une image colorée de crayons fondus.
<ph type="x-smartling-placeholder">
</ph> L&#39;impact de la simulation de la tritanopie sur une image colorée de crayons fondus. <ph type="x-smartling-placeholder">
</ph> L'impact de la simulation de la tritanopie sur une image colorée de crayons fondus.

En tant que développeur ayant une vision régulière, vous constaterez peut-être que le rapport de contraste est médiocre pour les paires de couleurs qui vous conviennent. Cela est dû au fait que les formules de rapport de contraste tiennent compte de ces déficiences de la vision des couleurs ! Dans certains cas, vous pourrez peut-être lire du texte à faible contraste, mais les personnes malvoyantes ne disposeront pas de ce privilège.

En permettant aux concepteurs et aux développeurs de simuler l'effet de ces déficiences visuelles sur leurs propres applications Web, notre objectif est de fournir l'élément manquant: non seulement les outils de développement peuvent vous aider à identifier et à résoudre les problèmes de contraste, mais aussi à les comprendre !

Simuler une déficience de la vision des couleurs avec HTML, CSS, SVG et C++

Avant d'aborder plus en détail l'implémentation du moteur de rendu Blink, il est utile de comprendre comment implémenter une fonctionnalité équivalente à l'aide d'une technologie Web.

Vous pouvez considérer chacune de ces simulations de déficience de la vision des couleurs comme une superposition recouvrant toute la page. Pour cela, la plate-forme Web propose des 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 et bien d'autres. Pour encore plus de contrôle, la propriété filter accepte également une URL qui peut pointer vers une définition de filtre SVG personnalisé:

<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é basée sur une matrice de couleurs. Conceptuellement, 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 5 valeurs: un multiplicateur pour (de gauche à droite) R, V, B et A, ainsi qu'une cinquième valeur pour une valeur de changement constant. Il y a quatre lignes: la première ligne de la matrice est utilisée pour calculer la nouvelle valeur rouge, la deuxième ligne de vert, la troisième ligne de bleu et la dernière ligne Alpha.

Vous vous demandez peut-être d'où viennent les chiffres exacts de notre exemple. Qu'est-ce qui fait de cette matrice de couleurs une bonne approximation de la deutéranopie ? La réponse est: la science ! Les valeurs sont basées sur un modèle de simulation physiologiquement précis du déficit de la vision des couleurs par Machado, Oliveira et Fernandes.

Quoi qu'il en soit, nous avons ce filtre SVG et nous pouvons maintenant l'appliquer à des éléments arbitraires sur la page à l'aide de CSS. Nous pouvons répéter le même schéma pour d'autres déficiences visuelles. Voici à quoi cela ressemble:

Si nous le souhaitons, nous pourrions créer notre fonctionnalité des outils de développement comme suit: lorsque l'utilisateur émule une déficience visuelle dans l'interface utilisateur des outils de développement, nous injectons le filtre SVG dans le document inspecté, puis nous appliquons le style de filtre à l'élément racine. Cependant, cette approche présente plusieurs problèmes:

  • Il est possible que l'élément racine de la page comporte déjà un filtre que notre code peut alors remplacer.
  • Il est possible que la page contienne déjà un élément id="deuteranopia", ce qui présente un conflit avec la définition de notre filtre.
  • La page peut s'appuyer sur une certaine structure DOM, et l'insertion du <svg> dans le DOM peut entraîner une violation de ces hypothèses.

Mis à part les cas limites, le principal problème de cette approche est que nous apporterions des modifications observables de la page de manière programmatique. Si un utilisateur des outils de développement 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 les outils de développement, nous avons besoin d'une solution qui ne présente pas ces inconvénients.

Voyons comment rendre cette fonctionnalité moins intrusive. 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 deuxième partie: comment éviter d'ajouter le SVG au DOM ? Vous pouvez le déplacer dans un fichier SVG distinct. Nous pouvons copier le <svg>…</svg> à partir du code HTML ci-dessus et l'enregistrer sous le nom filter.svg, mais nous devons d'abord apporter quelques modifications. Le SVG intégré dans le code HTML respecte les règles d'analyse HTML. Cela signifie que, dans certains cas, vous pouvez omettre les valeurs d'attribut entre guillemets, par exemple. Toutefois, le format SVG dans des fichiers distincts est censé être du code XML valide, et l'analyse XML est beaucoup plus stricte que 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 y apporter quelques modifications. Saurez-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 est la déclaration de l'espace de noms XML en haut. Le deuxième ajout est appelé "solidus", la barre oblique qui indique que la balise <feColorMatrix> ouvre et ferme l'élément. Cette dernière modification n'est pas nécessaire (nous pouvons nous en tenir à la balise de fermeture explicite </feColorMatrix> à la place), mais comme XML et SVG-in-HTML prennent tous deux en charge ce raccourci />, nous pouvons également l'utiliser.

Quoi qu'il en soit, avec ces modifications, nous pouvons enfin enregistrer le fichier en tant que fichier SVG valide et faire pointer dessus à partir de la valeur de la propriété CSS filter dans notre document HTML:

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

Bravo, nous n'avons plus besoin d'injecter du SVG dans le document ! C'est déjà beaucoup mieux. Mais... nous dépendons maintenant d'un fichier distinct. Cela reste une dépendance. Pouvons-nous s'en débarrasser d'une manière ou d'une autre ?

Il s'avère que nous n'avons pas réellement besoin d'un 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 précédemment, 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 qu'à présent, nous n'avons plus besoin de stocker le fichier où que ce soit, ni de le charger à partir du disque ou sur le réseau simplement pour l'utiliser dans notre document HTML. Ainsi, au lieu de faire référence au nom de fichier comme nous l'avons fait précédemment, nous pouvons maintenant 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>

Comme précédemment, nous spécifions l'ID du filtre à utiliser à la fin de l'URL. Notez qu'il n'est pas nécessaire d'encoder le document SVG en base64 dans l'URL. Cela n'affecterait que la lisibilité et augmenterait la taille du fichier. Nous avons ajouté des barres obliques inverses à la fin de chaque ligne pour garantir que les caractères de retour à la ligne de l'URL de données ne terminent pas le littéral de chaîne CSS.

Jusqu'à présent, nous n'avons abordé que la simulation de déficiences visuelles à l'aide de la technologie Web. Il est intéressant de noter que notre implémentation finale dans le moteur de rendu Blink est en fait assez similaire. Voici un utilitaire d'aide C++ que nous avons ajouté pour créer une URL de données avec une définition de filtre donnée, en nous appuyant sur 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;
}

Et 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 donne accès à toute la puissance des filtres SVG sans avoir à réimplémenter quoi que ce soit ni à réinventer la roue. Nous mettons en œuvre une fonctionnalité Blink Renderer, mais nous le faisons par le biais de la plate-forme Web.

Nous avons donc compris comment créer des filtres SVG et les transformer en URL de données que nous pouvons utiliser dans la valeur de notre propriété CSS filter. Pouvez-vous penser à un problème avec cette technique ? Il s'avère que nous ne pouvons pas réellement nous fier à l'URL de données en cours de chargement 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 Blink veille tout particulièrement à contourner la CSP pour ces URL de données "internes" lors du chargement.

Mis à part les cas limites, nous avons bien progressé. Comme nous ne dépendons plus de la présence de <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 ! Maintenant, nous allons également nous débarrasser de cela.

Éviter la dépendance CSS intégrée au document

Pour récapituler, voici où nous en sommes actuellement:

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

Nous dépendons toujours de cette propriété CSS filter, qui peut remplacer une filter dans le vrai document et interrompre les éléments. Il apparaîtra également lors de l'inspection des styles calculés dans les outils de développement, ce qui pourrait prêter à confusion. Comment éviter ces problèmes ? Nous devons trouver un moyen d'ajouter un filtre au document sans qu'il ne soit observable de manière programmatique pour les développeurs.

L'une de ces idées a été de créer une propriété CSS interne à Chrome qui se comporte comme filter, mais porte un nom différent, par exemple --internal-devtools-filter. Nous pourrions ensuite ajouter une logique spéciale pour nous assurer que cette propriété ne s'affiche jamais dans les outils de développement ni dans les styles calculés dans le DOM. Nous pourrions même nous assurer qu'elle ne fonctionne que sur le seul élément dont nous avons besoin: l'élément racine. Cependant, cette solution n'est pas idéale: nous dupliquons les fonctionnalités qui existent déjà avec filter. Même si nous nous efforçons de masquer cette propriété non standard, les développeurs Web pourraient tout de même la découvrir et commencer à l'utiliser, ce qui serait nuisible 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 des suggestions ?

La spécification CSS comporte une section qui présente le modèle de mise en forme visuelle qu'elle utilise, et l'un des concepts clés est la fenêtre d'affichage. Il s'agit de la vue visuelle à travers laquelle les utilisateurs consultent la page Web. Le bloc conteneur initial est un concept étroitement lié, qui ressemble à une <div> de fenêtre d'affichage personnalisables qui n'existe qu'au niveau de la spécification. La spécification fait référence à ce concept de "fenêtre d'affichage" à tous les niveaux. Par exemple, savez-vous comment le navigateur affiche les barres de défilement lorsque le contenu ne rentre pas dans le bon format ? 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 fenêtre d'affichage par défaut conformément à la spécification:

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 C++ ni les subtilités du moteur de style de Blink pour voir que ce code gère les éléments z-index, display, position et overflow de la fenêtre d'affichage (ou, plus précisément, du bloc contenant le premier bloc). Ce sont tous des concepts CSS que vous connaissez peut-être ! Il existe d'autres possibilités liées à l'empilement des contextes. Cela ne se traduit pas directement par une propriété CSS, mais dans l'ensemble, vous pouvez considérer cet objet viewport comme un élément pouvant être stylisé à l'aide de CSS dans Blink, comme un élément DOM, sauf qu'il ne fait pas partie du DOM.

Cela nous donne 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 avec le DOM.

Conclusion

Pour récapituler ce petit voyage, nous avons commencé par créer un prototype en utilisant la technologie Web au lieu de C++, puis nous avons commencé à travailler sur le transfert de certaines parties vers le moteur de rendu Blink.

  • Nous avons d'abord rendu notre prototype plus autonome en intégrant les URL de données.
  • Nous avons ensuite adapté ces URL de données internes à CSP, en mettant leur chargement dans une casse spéciale.
  • Nous avons rendu notre implémentation indépendante du DOM et non observable de manière programmatique en déplaçant les styles vers le viewport interne de Blink.

La spécificité 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

Vous pouvez utiliser Chrome Canary, Dev ou Bêta comme navigateur de développement par défaut. Ces versions preview vous permettent d'accéder aux dernières fonctionnalités des outils de développement, de tester des API de plates-formes Web de pointe et de 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 et des modifications dans l'article, ou de tout autre sujet lié aux outils de développement.

  • Envoyez-nous une suggestion ou un commentaire via crbug.com.
  • Signalez un problème lié aux outils de développement en cliquant sur Autres options   Plus > Aide > Signalez un problème dans les outils de développement.
  • Tweetez à l'adresse @ChromeDevTools.
  • Faites-nous part de vos commentaires sur les vidéos YouTube sur les nouveautés des outils de développement ou sur les vidéos YouTube de conseils pour les outils de développement.