Simulieren von Farbsehschwächen in Blink Renderer

In diesem Artikel wird beschrieben, warum und wie wir die Simulation von Farbfehlsichtigkeit in DevTools und dem Blink-Renderer implementiert haben.

Hintergrund: Schlechter Farbkontrast

Text mit geringem Kontrast ist das häufigste automatisch erkennbare Problem mit der Barrierefreiheit im Web.

Eine Liste häufiger Probleme mit der Barrierefreiheit im Web. Text mit geringem Kontrast ist bei weitem das häufigste Problem.

Laut der WebAIM-Analyse der Barrierefreiheit der 1 Million beliebtesten Websites haben über 86% der Startseiten einen geringen Kontrast. Im Durchschnitt enthält jede Startseite 36 verschiedene Textelemente mit niedrigem Kontrast.

Mit den DevTools Kontrastprobleme finden, nachvollziehen und beheben

Die Chrome-Entwicklertools können Entwicklern und Designern helfen, den Kontrast zu verbessern und barrierefreiere Farbschemata für Webanwendungen auszuwählen:

Vor Kurzem haben wir dieser Liste ein neues Tool hinzugefügt, das sich ein wenig von den anderen unterscheidet. Die oben genannten Tools konzentrieren sich hauptsächlich darauf, Informationen zum Kontrastverhältnis zu liefern und Ihnen Optionen zum Korrigieren anzubieten. Wir haben festgestellt, dass Entwicklern in den DevTools noch keine Möglichkeit geboten wird, dieses Problemfeld besser zu verstehen. Deshalb haben wir auf dem Tab „Rendering“ in den DevTools eine Simulation von Sehschwäche implementiert.

In Puppeteer können Sie diese Simulationen mithilfe der neuen page.emulateVisionDeficiency(type) API programmatisch aktivieren.

Farbsehschwäche

Etwa 1 von 20 Personen leidet an einer Farbsehschwäche (auch als „Farbenblindheit“ bezeichnet). Solche Beeinträchtigungen erschweren es, verschiedene Farben zu unterscheiden, was Kontrastprobleme verstärken kann.

Ein buntes Bild von geschmolzenen Buntstiften, ohne simulierte Farbfehlsichtigkeit
Ein buntes Bild von geschmolzenen Buntstiften, ohne simulierte Farbsehschwäche.
ALT_TEXT_HERE
Die Auswirkungen der Simulation von Achromatopsie auf ein buntes Bild von geschmolzenen Buntstiften.
Die Auswirkungen der Simulation von Deuteranopie auf ein buntes Bild von geschmolzenen Buntstiften.
Die Auswirkungen der Simulation von Deuteranopie auf ein buntes Bild von geschmolzenen Buntstiften.
Die Auswirkungen der Simulation von Protanopie auf ein buntes Bild von geschmolzenen Buntstiften.
Die Auswirkungen der Simulation von Protanopie auf ein buntes Bild von geschmolzenen Buntstiften.
Die Auswirkungen der Simulation von Tritanopie auf ein buntes Bild von geschmolzenen Buntstiften.
Die Auswirkungen der Simulation von Tritanopie auf ein buntes Bild von geschmolzenen Buntstiften.

Als Entwickler mit normalem Sehvermögen sehen Sie in den DevTools möglicherweise ein schlechtes Kontrastverhältnis für Farbpaare, die für Sie optisch in Ordnung sind. Das liegt daran, dass die Formeln für das Kontrastverhältnis diese Farbfehlsichtigkeiten berücksichtigen. Sie können in einigen Fällen möglicherweise noch Text mit geringem Kontrast lesen, aber Menschen mit Sehbehinderung haben dieses Privileg nicht.

Mit der Möglichkeit, die Auswirkungen dieser Sehbeeinträchtigungen in ihren eigenen Web-Apps zu simulieren, möchten wir das fehlende Puzzleteil liefern: Mit DevTools können Sie nicht nur Kontrastprobleme finden und beheben, sondern sie jetzt auch verstehen.

Simulieren von Farbsehschwäche mit HTML, CSS, SVG und C++

Bevor wir uns mit der Implementierung unserer Funktion im Blink-Renderer befassen, ist es hilfreich zu verstehen, wie Sie eine entsprechende Funktion mithilfe von Webtechnologien implementieren würden.

Sie können sich jede dieser Simulationen von Farbfehlsichtigkeiten als Overlay vorstellen, das die gesamte Seite bedeckt. Die Webplattform bietet dafür eine Möglichkeit: CSS-Filter. Mit der CSS-Eigenschaft filter können Sie einige vordefinierte Filterfunktionen wie blur, contrast, grayscale und hue-rotate verwenden. Für noch mehr Kontrolle kann für die Property filter auch eine URL angegeben werden, die auf eine benutzerdefinierte SVG-Filterdefinition verweist:

<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>

Im obigen Beispiel wird eine benutzerdefinierte Filterdefinition verwendet, die auf einer Farbmatrix basiert. Der [Red, Green, Blue, Alpha]-Farbwert jedes Pixels wird konzeptionell mit einer Matrix multipliziert, um eine neue Farbe [R′, G′, B′, A′] zu erzeugen.

Jede Zeile in der Matrix enthält fünf Werte: einen Multiplikator für R, G, B und A (von links nach rechts) sowie einen fünften Wert für einen konstanten Verschiebewert. Die Matrix hat vier Zeilen: Die erste Zeile der Matrix wird verwendet, um den neuen Rot-Wert zu berechnen, die zweite Zeile den Grün-Wert, die dritte Zeile den Blau-Wert und die letzte Zeile den Alpha-Wert.

Sie fragen sich vielleicht, woher die genauen Zahlen in unserem Beispiel stammen. Was macht diese Farbmatrix zu einer guten Annäherung an Deuteranopie? Die Antwort lautet: Wissenschaft! Die Werte basieren auf einem physiologisch korrekten Simulationsmodell für Farbfehlsichtigkeit von Machado, Oliveira und Fernandes.

Wir haben diesen SVG-Filter und können ihn jetzt mit CSS auf beliebige Elemente auf der Seite anwenden. Das gleiche Muster lässt sich auch auf andere Sehbeeinträchtigungen anwenden. Hier eine Demo, wie das aussieht:

Wir könnten unsere DevTools-Funktion so implementieren: Wenn der Nutzer in der DevTools-Benutzeroberfläche eine Sehschwäche emuliert, wird der SVG-Filter in das geprüfte Dokument eingefügt und dann wird der Filterstil auf das Stammelement angewendet. Dieser Ansatz hat jedoch mehrere Probleme:

  • Die Seite hat möglicherweise bereits einen Filter auf dem Stammelement, der dann von unserem Code überschrieben wird.
  • Auf der Seite befindet sich möglicherweise bereits ein Element mit id="deuteranopia", das mit unserer Filterdefinition kollidiert.
  • Die Seite basiert möglicherweise auf einer bestimmten DOM-Struktur und durch das Einfügen des <svg> in das DOM werden diese Annahmen möglicherweise verletzt.

Abgesehen von Grenzfällen besteht das Hauptproblem bei diesem Ansatz darin, dass wir programmatisch beobachtbare Änderungen an der Seite vornehmen würden. Wenn ein DevTools-Nutzer das DOM untersucht, sieht er möglicherweise plötzlich ein <svg>-Element, das er nie hinzugefügt hat, oder einen CSS-filter, den er nie geschrieben hat. Das wäre verwirrend. Um diese Funktion in den Entwicklertools zu implementieren, benötigen wir eine Lösung, die diese Nachteile nicht hat.

Sehen wir uns an, wie wir das weniger aufdringlich gestalten können. Bei dieser Lösung müssen wir zwei Teile ausblenden: 1) den CSS-Stil mit der Property filter und 2) die SVG-Filterdefinition, die derzeit Teil des DOM ist.

<!-- 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>

SVG-Abhängigkeit im Dokument vermeiden

Beginnen wir mit Teil 2: Wie können wir vermeiden, das SVG dem DOM hinzuzufügen? Eine Möglichkeit besteht darin, sie in eine separate SVG-Datei zu verschieben. Wir können die <svg>…</svg> aus dem obigen HTML-Code kopieren und als filter.svg speichern. Dazu müssen wir aber zuerst einige Änderungen vornehmen. Für Inline-SVG in HTML gelten die HTML-Parseregeln. Das bedeutet, dass Sie in einigen Fällen Attributwerte auch ohne Anführungszeichen eingeben können. SVG in separaten Dateien muss jedoch gültiges XML sein. Das XML-Parsen ist viel strenger als das HTML-Parsen. Hier ist noch einmal unser SVG-in-HTML-Snippet:

<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>

Damit das ein gültiges eigenständiges SVG (und damit XML) wird, müssen wir einige Änderungen vornehmen. Können Sie erraten, welche?

<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>

Die erste Änderung betrifft die XML-Namespacedeklaration oben. Die zweite Ergänzung ist der sogenannte Schrägstrich, der angibt, dass das <feColorMatrix>-Tag das Element sowohl öffnet als auch schließt. Diese letzte Änderung ist eigentlich nicht erforderlich. Wir könnten stattdessen einfach das explizite </feColorMatrix>-Schließungs-Tag verwenden. Da aber sowohl XML als auch SVG-in-HTML diese />-Kurzschreibweise unterstützen, können wir sie auch verwenden.

Mit diesen Änderungen können wir die Datei endlich als gültige SVG-Datei speichern und im HTML-Dokument über den CSS-Attributwert filter darauf verweisen:

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

Hurra, wir müssen SVG nicht mehr in das Dokument einfügen! Das ist schon viel besser. Aber… wir benötigen jetzt eine separate Datei. Das ist immer noch eine Abhängigkeit. Können wir das irgendwie loswerden?

Wie sich herausstellt, benötigen wir keine Datei. Wir können die gesamte Datei in einer URL codieren, indem wir eine Daten-URL verwenden. Dazu nehmen wir den Inhalt der SVG-Datei, fügen das Präfix data: hinzu und konfigurieren den richtigen MIME-Typ. So erhalten wir eine gültige Daten-URL, die dieselbe SVG-Datei darstellt:

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>

Der Vorteil besteht darin, dass wir die Datei jetzt nicht mehr irgendwo speichern oder von einem Laufwerk oder über das Netzwerk laden müssen, um sie in unserem HTML-Dokument zu verwenden. Anstatt wie zuvor auf den Dateinamen zu verweisen, können wir jetzt auf die Daten-URL verweisen:

<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>

Am Ende der URL geben wir wie gewohnt die ID des gewünschten Filters an. Das SVG-Dokument muss nicht Base64-codiert in die URL eingefügt werden. Das würde nur die Lesbarkeit beeinträchtigen und die Dateigröße erhöhen. Wir haben am Ende jeder Zeile Backslashes hinzugefügt, damit die Zeilenumbruchzeichen in der Daten-URL das CSS-Stringliteral nicht beenden.

Bisher haben wir nur darüber gesprochen, wie sich Sehbehinderungen mithilfe von Webtechnologie simulieren lassen. Interessanterweise ist unsere endgültige Implementierung im Blink-Renderer ziemlich ähnlich. Hier ist ein C++-Hilfsprogramm, das wir hinzugefügt haben, um eine Daten-URL mit einer bestimmten Filterdefinition zu erstellen. Es basiert auf derselben Methode:

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;
}

So erstellen wir damit alle benötigten Filter:

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 "";
  }
}

Mit dieser Technik haben wir Zugriff auf die volle Leistung von SVG-Filtern, ohne etwas neu implementieren oder neu erfinden zu müssen. Wir implementieren eine Blink-Renderer-Funktion, nutzen dabei aber die Webplattform.

Wir haben also herausgefunden, wie wir SVG-Filter erstellen und in Daten-URLs umwandeln, die wir im Wert der CSS-Property filter verwenden können. Können Sie sich ein Problem mit dieser Technik vorstellen? Wir können uns jedoch nicht darauf verlassen, dass die Daten-URL in allen Fällen geladen wird, da die Zielseite möglicherweise eine Content-Security-Policy enthält, die Daten-URLs blockiert. Bei unserer endgültigen Implementierung auf Blink-Ebene wird darauf geachtet, dass CSP für diese „internen“ Daten-URLs beim Laden umgangen wird.

Abgesehen von Grenzfällen haben wir gute Fortschritte gemacht. Da wir nicht mehr darauf angewiesen sind, dass Inline-<svg> im selben Dokument vorhanden ist, haben wir unsere Lösung auf eine einzige eigenständige CSS-filter-Property-Definition reduziert. Sehr gut! Jetzt beseitigen wir auch das.

CSS-Abhängigkeit im Dokument vermeiden

Zur Wiederholung: So weit sind wir bisher:

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

Wir sind noch auf diese CSS-Eigenschaft filter angewiesen, die eine filter im tatsächlichen Dokument überschreiben und zu Fehlern führen kann. Außerdem würde es beim Prüfen der berechneten Stile in den Entwicklertools angezeigt werden, was verwirrend wäre. Wie können wir diese Probleme vermeiden? Wir müssen eine Möglichkeit finden, dem Dokument einen Filter hinzuzufügen, ohne dass er für Entwickler programmatisch sichtbar ist.

Eine Idee war, eine neue interne CSS-Property in Chrome zu erstellen, die sich wie filter verhält, aber einen anderen Namen hat, z. B. --internal-devtools-filter. Wir könnten dann eine spezielle Logik hinzufügen, damit diese Eigenschaft nie in den DevTools oder in den berechneten Stilen im DOM angezeigt wird. Wir könnten sogar dafür sorgen, dass es nur für das Element funktioniert, für das wir es benötigen: das Stammelement. Diese Lösung wäre jedoch nicht ideal: Wir würden Funktionen duplizieren, die bereits mit filter vorhanden sind. Und selbst wenn wir uns bemühen würden, diese nicht standardmäßige Eigenschaft zu verbergen, könnten Webentwickler sie trotzdem finden und verwenden, was für die Webplattform schädlich wäre. Wir brauchen eine andere Möglichkeit, einen CSS-Stil anzuwenden, ohne dass er im DOM sichtbar ist. Könnten Sie mir weiterhelfen?

Die CSS-Spezifikation enthält einen Abschnitt, in dem das verwendete visuelle Formatierungsmodell vorgestellt wird. Eines der wichtigsten Konzepte dort ist der Bildschirmbereich. Das ist die visuelle Ansicht, über die Nutzer die Webseite aufrufen. Ein eng verwandtes Konzept ist der initiale enthaltende Block. Er ist eine Art stilisierbarer Viewport <div>, der nur auf Spezifikationsebene existiert. In der Spezifikation wird dieses Konzept des „Darstellungsbereichs“ immer wieder erwähnt. Wissen Sie beispielsweise, wie der Browser Bildlaufleisten anzeigt, wenn der Inhalt nicht passt? All dies ist in der CSS-Spezifikation basierend auf diesem „Darstellungsbereich“ definiert.

Diese viewport gibt es auch im Blink-Renderer als Implementierungsdetail. Hier ist der Code, mit dem die Standard-Darstellungsbereichsstile gemäß der Spezifikation angewendet werden:

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;
}

Sie müssen weder C++ noch die Feinheiten der Blink-Style-Engine verstehen, um zu erkennen, dass dieser Code z-index, display, position und overflow des Viewports (oder genauer: des ursprünglichen enthaltenden Blocks) verarbeitet. Das sind alles Konzepte, die Sie vielleicht aus CSS kennen. Es gibt noch einige andere Dinge im Zusammenhang mit Stapelkontexten, die sich nicht direkt in eine CSS-Property umwandeln lassen. Insgesamt können Sie sich dieses viewport-Objekt jedoch als etwas vorstellen, das wie ein DOM-Element mit CSS innerhalb von Blink gestaltet werden kann – mit der Ausnahme, dass es nicht Teil des DOM ist.

Das ist genau das, was wir wollen! Wir können unsere filter-Stile auf das viewport-Objekt anwenden, was sich visuell auf das Rendering auswirkt, ohne die sichtbaren Seitenstile oder das DOM in irgendeiner Weise zu beeinträchtigen.

Fazit

Um unsere kleine Reise hier zusammenzufassen: Wir haben mit dem Erstellen eines Prototyps mit Webtechnologie anstelle von C++ begonnen und dann damit begonnen, Teile davon in den Blink-Renderer zu verschieben.

  • Zuerst haben wir unseren Prototyp durch Einfügen von Daten-URLs eigenständiger gemacht.
  • Anschließend haben wir diese internen Daten-URLs CSP-freundlich gemacht, indem wir das Laden speziell behandelt haben.
  • Wir haben unsere Implementierung DOM-unabhängig und programmatisch nicht beobachtbar gemacht, indem wir Stile in die interne viewport von Blink verschoben haben.

Das Besondere an dieser Implementierung ist, dass unser HTML-/CSS-/SVG-Prototyp das endgültige technische Design beeinflusst hat. Wir haben eine Möglichkeit gefunden, die Webplattform auch im Blink-Renderer zu verwenden.

Weitere Informationen finden Sie in unserem Designvorschlag oder im Chromium-Tracking-Bug, der alle zugehörigen Patches enthält.

Vorschaukanäle herunterladen

Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Ihnen Zugriff auf die neuesten DevTools-Funktionen, ermöglichen es Ihnen, innovative Webplattform-APIs zu testen, und helfen Ihnen, Probleme auf Ihrer Website zu finden, bevor Ihre Nutzer sie bemerken.

Chrome-Entwicklertools-Team kontaktieren

Mit den folgenden Optionen können Sie über neue Funktionen, Updates oder andere Themen im Zusammenhang mit den DevTools sprechen.