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

Mathias Bynens
Mathias Bynens

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

Arrière-plan: le contraste des couleurs est mauvais

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

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

D'après l'analyse de l'accessibilité réalisée par WebAIM sur le top 1 million de sites Web, plus de 86% des pages d'accueil présentent un faible contraste. En moyenne, chaque page d'accueil possède 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é à cette liste un nouvel outil, légèrement différent des autres. Les outils ci-dessus se concentrent principalement sur l'affichage des informations sur le rapport de contraste et vous donnent la possibilité de le corriger. Nous avons réalisé que les outils de développement manquaient toujours aux développeurs de understanding cet espace problématique. Pour résoudre ce problème, nous avons implémenté la simulation de la déficience visuelle dans l'onglet de rendu des outils de développement.

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

déficience de la vision des couleurs

Près d'une personne sur 20 souffre de daltonisme (ou daltonisme). De telles déficiences rendent plus difficile la distinction entre les différentes couleurs, ce qui peut amplifier les problèmes de contraste.

Image colorée de crayons fondus, sans simulation de déficience de vision des couleurs
Image colorée de crayons fondus, sans simulation de déficience de la vision des couleurs.
ALT_TEXT_HERE
Impact d'une simulation d'achromatopsie sur une image colorée de crayons fondus.
L'effet d'une simulation de deutéranopie sur une image colorée de crayons fondus.
Impact de la simulation de la deutéranopie sur une image colorée de crayons fondus.
L'impact de la simulation de la protanopie sur une image colorée de crayons fondus.
Simulez l'effet de la 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 ayant une vision normale, vous pouvez constater que les outils de développement affichent un mauvais rapport de contraste pour les paires de couleurs qui vous semblent correctes. 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 pouvez toujours lire du texte à faible contraste, mais les personnes malvoyantes ne disposent pas de ce privilège.

En permettant aux graphistes et aux développeurs de simuler l'effet de ces déficiences visuelles dans leurs propres applications Web, nous voulons leur fournir l'élément manquant: les outils de développement peuvent non seulement vous aider à trouver 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'examiner en détail l'implémentation du moteur de rendu Blink de notre fonctionnalité, 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 de la vision des couleurs comme une superposition couvrant toute la page. 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 un contrôle accru, la propriété filter accepte également une URL qui peut 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. Conceptuellement, la valeur de couleur [Red, Green, Blue, Alpha] de chaque pixel est multipliée par une matrice pour créer une couleur [R′, G′, B′, A′].

Chaque ligne de la matrice contient cinq valeurs: un multiplicateur pour (de gauche à droite) R, G, B et A, ainsi qu'une cinquième valeur pour une valeur de décalage 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 verte, la troisième ligne bleue et la dernière ligne alpha.

Vous vous demandez peut-être d'où proviennent 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 de la déficience visuelle physiologiquement précis conçu par Machado, Oliveira et Fernandes.

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

Si nous le souhaitons, nous pourrions créer notre fonctionnalité DevTools comme suit: lorsque l'utilisateur émule une déficience visuelle dans l'UI 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. Toutefois, cette approche présente plusieurs problèmes:

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

Mis à part les cas particuliers, le principal problème avec cette approche est que nous apporterions des modifications observables de manière programmatique à la page. 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 réduire cette intrusion. Cette solution comporte deux parties à 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 lui apporter quelques modifications. Le format SVG intégré dans le code HTML suit les règles d'analyse HTML. Vous pouvez donc utiliser des méthodes telles que l'omission de guillemets autour des valeurs d'attribut dans certains cas. Toutefois, les fichiers SVG dans des fichiers distincts sont censés être du code XML valide, et l'analyse XML est beaucoup plus stricte que le code HTML. Voici de nouveau l'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 (et donc XML) soit valide, nous devons 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 d'espace de noms XML en haut. Le deuxième ajout est ce que l'on appelle "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 pourrions nous contenter de la balise de fermeture </feColorMatrix> explicite à la place), mais comme XML et SVG-in-HTML prennent en charge ce raccourci />, nous pouvons aussi l'utiliser.

Quoi qu'il en soit, avec ces modifications, nous pouvons enfin enregistrer ce fichier au format SVG valide, puis pointer vers celui-ci à partir de la valeur de la propriété CSS filter dans notre document HTML:

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

Attention, nous n'avons plus besoin d'injecter de SVG dans le document ! C'est déjà beaucoup mieux. Mais nous dépendons maintenant d'un fichier distinct. Cela reste une dépendance. Peut-on s'en débarrasser ?

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 précédent, nous ajoutons le préfixe data:, nous configurons le type MIME approprié, puis 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 où que vous soyez, ni de le charger à partir d'un disque ou du réseau uniquement pour l'utiliser dans notre document HTML. Ainsi, au lieu de faire référence au nom de fichier comme précédemment, nous pouvons 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 l'ID du filtre à utiliser, comme précédemment. Notez qu'il n'est pas nécessaire d'encoder en base64 le document SVG dans l'URL, car cela nuirait à 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 des données ne terminent pas le littéral de chaîne CSS.

Jusqu'à présent, nous n'avons parlé que de la façon de simuler des 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'assistance C++ que nous avons ajouté pour créer une URL de données avec une définition de filtre donnée, en utilisant 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 permet d'accéder à toute la puissance des filtres SVG sans avoir à réimplémenter quoi que ce soit ni à réinventer aucune roue. Nous implémentons une fonctionnalité de moteur de rendu de clignotement, mais nous le faisons en utilisant la plate-forme Web.

Nous avons compris comment créer des filtres SVG et les convertir en URL de données que nous pouvons utiliser dans la valeur de notre propriété CSS filter. Pouvez-vous citer un problème avec cette technique ? Il s'avère que, dans tous les cas, nous ne pouvons pas compter sur le chargement de l'URL de données, car la page cible peut comporter un Content-Security-Policy qui bloque les URL de données. Lors de notre implémentation finale au niveau du flash, nous prenons un soin particulier de contourner CSP pour ces URL de données "internes" lors du chargement.

Mis à part les cas limites, nous avons fait de bons progrès. 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, éliminons-nous aussi.

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

Pour récapituler, voici où nous en sommes jusqu'à présent:

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

Nous dépendons toujours de cette propriété CSS filter, qui peut remplacer une propriété filter dans le document réel et interrompre des éléments. Il s'affichait également lors de l'inspection des styles calculés dans les outils de développement, ce qui pouvait être déroutant. Comment éviter ces problèmes ? Nous devons trouver un moyen d'ajouter un filtre au document sans qu'il soit observable de manière programmatique pour les développeurs.

L'idée a été de créer une propriété CSS interne à Chrome qui se comporte comme filter, mais porte un nom différent, comme --internal-devtools-filter. Nous pourrions ensuite ajouter une logique spéciale pour garantir 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'il ne fonctionne que sur l'élément dont nous en avons besoin: l'élément racine. Cependant, cette solution ne serait pas idéale: nous dupliquerions les fonctionnalités qui existent déjà avec filter, et 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 présentant le modèle de mise en forme visuelle qu'elle utilise, et l'un des concepts clés est la la fenêtre d'affichage. 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 ressemble à une fenêtre d'affichage stylisée <div> qui n'existe qu'au niveau des spécifications. La spécification fait référence à ce concept de "fenêtre d'affichage". Par exemple, savez-vous comment le navigateur affiche les barres de défilement lorsque le contenu ne tient pas ? Tout 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 de l'affichage Blink, en tant que détail d'implémentation. Voici le code qui applique les styles de fenêtre d'affichage par défaut en fonction de 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;
}

Il n'est pas nécessaire 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 qui le contient initial). Ce sont tous des concepts CSS que vous connaissez peut-être. Il existe une autre magie liée aux contextes d'empilement, qui 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 qui peut être stylisé avec CSS depuis Blink, tout 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 a un impact visuel sur le rendu, sans interférer avec les styles de page observables ni avec le DOM.

Conclusion

Pour récapituler notre petit voyage, nous avons commencé par construire un prototype en utilisant la technologie Web au lieu de C++, puis nous avons commencé à en déplacer des parties vers le moteur de rendu Blink.

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

Ce qui rend cette implémentation unique, c'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 dans Chromium, qui fait référence à tous les correctifs associés.

Télécharger les canaux de prévisualisation

Nous vous conseillons d'utiliser Chrome Canary, Dev ou Beta 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 pointe de plates-formes Web et de détecter les problèmes sur votre site avant même que vos utilisateurs ne le fassent.

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

Utilisez les options suivantes pour discuter des nouvelles fonctionnalités et des modifications dans le message, ou de tout autre sujet lié aux outils de développement.

  • Envoyez-nous une suggestion ou des commentaires via crbug.com.
  • Signalez un problème lié aux outils de développement en accédant à Plus d'options   More > Aide > Signaler un problème dans les outils de développement.
  • Envoyez un tweet à @ChromeDevTools.
  • Laissez des commentaires sur les nouveautés des outils de développement vidéos YouTube ou les vidéos YouTube de nos conseils sur les outils de développement.