Simulieren von Farbsehschwächen in Blink Renderer

Mathias Bynens
Mathias Bynens

In diesem Artikel wird beschrieben, warum und wie wir die Simulation der Farbblindheit in den Entwicklertools und dem Blink Renderer implementiert haben.

Hintergrund: schlechter Farbkontrast

Text mit niedrigem Kontrast ist das häufigste Problem mit automatisch erkannten Bedienungshilfen im Web.

Eine Liste mit häufigen Problemen mit der Barrierefreiheit im Web. Text mit geringem Kontrast ist mit Abstand das häufigste Problem.

Laut Analyse der Barrierefreiheit von WebAIM zu den 1 Million Websites weisen über 86% der Startseiten einen geringen Kontrast auf. Jede Startseite hat durchschnittlich 36 verschiedene Instanzen von kontrastreichen Text.

Mit den Entwicklertools Kontrastprobleme finden, verstehen und beheben

Mit den Chrome-Entwicklertools können Entwickler und Designer den Kontrast verbessern und barrierefreiere Farbschemas für Web-Apps auswählen:

Wir haben dieser Liste kürzlich ein neues Tool hinzugefügt, das sich ein wenig von den anderen unterscheidet. Die oben genannten Tools konzentrieren sich hauptsächlich auf die Anzeige von Kontrastverhältnisinformationen und bieten Optionen zur Behebung des Fehlers. Uns ist klar geworden, dass es in den Entwicklertools immer noch keine Möglichkeit gab, ein tieferes understanding für diesen Problembereich zu entwickeln. Deshalb haben wir auf dem Tab „Rendering“ der Entwicklertools eine Simulation von Sehschwächen implementiert.

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

Farbblindheit

Ungefähr 1 von 20 Personen leidet an einer Farbblindheit. Solche Beeinträchtigungen erschweren es, verschiedene Farben auseinanderzuhalten, was Kontrastprobleme verstärken kann.

Ein farbenfrohes Bild mit geschmolzenen Buntstiften, bei dem keine Farbblindheit simuliert wird
Ein buntes Bild mit geschmolzenen Buntstiften, bei dem keine Farbblindheit simuliert wird.
ALT_TEXT_HERE
Die Auswirkungen der Simulation von Achromatopsie auf ein buntes Bild geschmolzener Buntstifte.
Die Auswirkungen der Simulation der Deuteranopie auf ein buntes Bild geschmolzener Buntstifte.
Die Auswirkungen der Simulation der Deuteranopie auf ein buntes Bild geschmolzener Buntstifte.
Die Auswirkungen der Simulation von Protanopie auf ein buntes Bild geschmolzener Buntstifte.
Die Auswirkungen der Simulation von Protanopie auf ein buntes Bild geschmolzener Buntstifte.
Die Auswirkungen der Simulation von Tritanopie auf ein buntes Bild geschmolzener Buntstifte.
Die Auswirkungen der Simulation von Tritanopie auf ein buntes Bild geschmolzener Buntstifte.

Als Entwickler mit einem normalen Sehvermögen kann es vorkommen, dass die Entwicklertools ein schlechtes Kontrastverhältnis für Farbpaare aufweisen, die optisch ansprechend aussehen. Das liegt daran, dass die Kontrastverhältnis-Formeln diese Farbenblindheit berücksichtigen! Sie können zwar in einigen Fällen kontrastreichen Text noch lesen, aber Menschen mit Sehbeeinträchtigungen haben diese Berechtigung nicht.

Da Designer und Entwickler die Auswirkungen dieser Sehschwäche auf ihre eigenen Web-Apps simulieren können, möchten wir das fehlende Stück zeigen: Mit den Entwicklertools können Sie nicht nur Kontrastprobleme finden und beheben, sondern sie auch verstehen.

Simulieren von Farbblindheit mit HTML, CSS, SVG und C++

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

Sie können sich jede dieser Simulationen von Farbblindheit als Overlay vorstellen, das die gesamte Seite abdeckt. Die Webplattform bietet hierfür eine Möglichkeit: CSS-Filter. Mit der CSS-Eigenschaft filter können Sie vordefinierte Filterfunktionen wie blur, contrast, grayscale, hue-rotate und viele mehr verwenden. Um noch mehr Kontrolle zu haben, akzeptiert die filter-Eigenschaft auch eine URL, die auf eine benutzerdefinierte SVG-Filterdefinition verweisen kann:

<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. Prinzipiell wird der Farbwert [Red, Green, Blue, Alpha] jedes Pixels matrix-multipliziert, um eine neue Farbe [R′, G′, B′, A′] zu erstellen.

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

Sie fragen sich vielleicht, woher die genauen Zahlen in unserem Beispiel stammen. Warum ist diese Farbmatrix eine gute Annäherung an die Deuteranopie? Die Antwort lautet: Wissenschaft! Die Werte basieren auf einem physiologisch genauen Simulationsmodell einer Farbblindheit von Machado, Oliveira und Fernandes.

Wie auch immer, wir haben diesen SVG-Filter und können ihn nun mithilfe von CSS auf beliebige Elemente auf der Seite anwenden. Dasselbe Muster können wir auch für andere Sehschwächen wiederholen. Hier sehen Sie eine Demo, die so aussieht:

Wenn wir möchten, könnten wir unsere Entwicklertools-Funktion wie folgt erstellen: Wenn der Nutzer auf der DevTools-Benutzeroberfläche eine Sehschwäche emuliert, fügen wir den SVG-Filter in das geprüfte Dokument ein und wenden dann den Filterstil auf das Stammelement an. Bei diesem Ansatz treten jedoch einige Probleme auf:

  • Möglicherweise hat die Seite bereits einen Filter im Stammelement, den unser Code dann überschreiben könnte.
  • Die Seite hat möglicherweise bereits ein Element mit id="deuteranopia", das mit unserer Filterdefinition in Konflikt steht.
  • Die Seite beruht möglicherweise auf einer bestimmten DOM-Struktur. Wenn wir <svg> in das DOM einfügen, verstoßen wir möglicherweise gegen diese Annahmen.

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

Sehen wir uns an, wie wir dies weniger aufdringlich machen können. Bei dieser Lösung müssen zwei Teile ausgeblendet werden: 1) den CSS-Stil mit der Eigenschaft filter und 2) die SVG-Filterdefinition, die derzeit Teil des DOMs 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-Element dem DOM hinzuzufügen? Eine Möglichkeit besteht darin, die Datei in eine separate SVG-Datei zu verschieben. Wir können die <svg>…</svg> aus dem HTML-Code oben kopieren und als filter.svg speichern, aber zuerst müssen wir ein paar Änderungen vornehmen. Das Inline-SVG in HTML folgt den Regeln für das HTML-Parsing. Das bedeutet, dass Sie in manchen Fällen Anführungszeichen bei Attributwerten weglassen können. SVG in separaten Dateien muss jedoch gültiger XML-Code sein, und das XML-Parsing ist wesentlich strenger als HTML. Hier 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 diese eigenständige SVG- und somit XML-Datei gültig ist, müssen wir einige Änderungen vornehmen. Kannst du erraten, was?

<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 ist die oben stehende XML-Namespace-Deklaration. Die zweite Ergänzung ist der sogenannte „Solidus“ – der Schrägstrich, der das <feColorMatrix>-Tag angibt, sodass das Element sowohl geöffnet als auch geschlossen wird. Diese letzte Änderung ist eigentlich nicht erforderlich. Wir könnten uns stattdessen einfach an das explizite schließende </feColorMatrix>-Tag halten, aber da sowohl XML als auch SVG-in-HTML diese />-Kürzel unterstützen, können wir sie auch nutzen.

Mit diesen Änderungen können wir die Datei schließlich als gültige SVG-Datei speichern und vom CSS-Eigenschaftswert filter in unserem HTML-Dokument darauf verweisen:

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

Hurra, wir müssen keine SVG-Dateien mehr in das Dokument einfügen. Das ist schon viel besser. Aber... wir hängen jetzt von einer separaten Datei ab. Das ist immer noch eine Abhängigkeit. Können wir es irgendwie beseitigen?

Wie sich herausstellt, benötigen wir eigentlich keine Datei. Mithilfe einer Daten-URL kann die gesamte Datei innerhalb einer URL codiert werden. Dazu nehmen wir buchstäblich den Inhalt der vorherigen SVG-Datei, fügen das Präfix data: hinzu, konfigurieren den korrekten MIME-Typ und haben eine gültige Daten-URL, die genau dieser SVG-Datei entspricht:

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 der Festplatte oder über das Netzwerk laden müssen, nur 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 zuvor die ID des zu verwendenden Filters an. Es ist nicht nötig, das SVG-Dokument in der URL mit Base64 zu codieren. Dies würde nur die Lesbarkeit beeinträchtigen und die Dateigröße erhöhen. Wir haben am Ende jeder Zeile umgekehrte Schrägstriche hinzugefügt, um sicherzustellen, dass die Zeilenumbruchzeichen in der Daten-URL nicht im CSS-String-Literal enden.

Bisher haben wir nur darüber gesprochen, wie sich Sehschwächen 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, die auf derselben Technik basiert:

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

Und so erstellen wir 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 "";
  }
}

Beachten Sie, dass wir mit dieser Technik die volle Leistung von SVG-Filtern nutzen können, ohne dass wir etwas neu implementieren oder neue Räder erfinden müssen. Wir implementieren einen Blink-Renderer, nutzen dazu aber die Webplattform.

Wir wissen nun, wie SVG-Filter erstellt und in Daten-URLs umgewandelt werden, die wir in unserem CSS-Eigenschaftswert filter verwenden können. Fällt Ihnen ein Problem bei dieser Technik ein? Wie sich herausstellt, können wir nicht in allen Fällen sicher sein, dass die Daten-URL geladen wird, da die Landingpage möglicherweise einen Content-Security-Policy enthält, der Daten-URLs blockiert. Bei unserer abschließenden Implementierung auf Blink-Ebene wird besonders darauf geachtet, die CSP für diese „internen“ Daten-URLs beim Laden zu umgehen.

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 effektiv auf eine einzige eigenständige CSS-Eigenschaftsdefinition filter reduziert. Sehr gut! Lassen Sie uns auch diese beseitigen.

CSS-Abhängigkeit im Dokument vermeiden

Zur Erinnerung: Das ist unsere bisherige Entwicklung:

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

Wir sind immer noch von dieser CSS-Eigenschaft filter abhängig, die ein filter im echten Dokument überschreiben und Fehler auftreten kann. Er tauchte auch bei der Prüfung der berechneten Stile in den Entwicklertools auf, 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 beobachtet werden kann.

Ein Vorschlag war, eine neue Chrome-interne CSS-Eigenschaft zu erstellen, die sich wie filter verhält, aber einen anderen Namen wie --internal-devtools-filter hat. Wir könnten dann eine spezielle Logik hinzufügen, um sicherzustellen, dass diese Eigenschaft nie in den Entwicklertools oder den berechneten Stilen im DOM angezeigt wird. Wir könnten sogar sicherstellen, dass es nur bei dem einen Element funktioniert, für das wir es benötigen: dem Stammelement. Diese Lösung wäre jedoch nicht ideal: Wir würden Funktionen duplizieren, die bereits mit filter vorhanden sind, und selbst wenn wir versuchen, diese nicht standardmäßige Property auszublenden, könnten Webentwickler trotzdem davon erfahren und sie verwenden, was schlecht für die Webplattform wäre. Wir brauchen eine andere Methode, um einen CSS-Stil anzuwenden, ohne dass er im DOM beobachtet werden kann. Haben Sie Ideen dazu?

Die CSS-Spezifikation enthält einen Abschnitt, in dem das verwendete visuelle Formatierungsmodell vorgestellt wird. Zu den wichtigsten Konzepten gehört der Darstellungsbereich. Dies ist die visuelle Ansicht, über die Nutzende die Webseite aufrufen. Ein ähnliches Konzept ist der anfänglich enthaltene Block, der ähnlich wie ein anpassbarer Darstellungsbereich <div> aussieht, der nur auf Spezifikationsebene vorhanden ist. Die Spezifikation bezieht sich überall auf dieses Konzept des Darstellungsbereichs. Weißt du beispielsweise, wie der Browser Bildlaufleisten anzeigt, wenn der Inhalt nicht passt? All dies ist in der CSS-Spezifikation definiert, die auf diesem Darstellungsbereich basiert.

Dieses viewport ist auch im Blink-Renderer sowie ein Implementierungsdetail vorhanden. Hier ist der Code, mit dem die Standardstile des Darstellungsbereichs 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 mit C++ noch mit den Feinheiten der Style-Engine von Blink vertraut sein, um zu sehen, dass dieser Code die z-index, display, position und overflow des Darstellungsbereichs (oder genauer gesagt die anfänglichen enthaltenden Blocks) verarbeitet. Das sind alles Konzepte, die Sie von CSS kennen. Es gibt noch einige andere Magie im Zusammenhang mit dem Stapeln von Kontexten, die sich nicht direkt in eine CSS-Eigenschaft übertragen. Insgesamt können Sie sich dieses viewport-Objekt jedoch als etwas vorstellen, das mit CSS in Blink formatiert werden kann, genau wie ein DOM-Element, allerdings ist es nicht Teil des DOMs.

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

Fazit

Wir haben zuerst einen Prototyp mit Webtechnologie anstelle von C++ erstellt und dann Teile davon in den Blink Renderer verschoben.

  • Zuerst haben wir unseren Prototyp eigenständiger gestaltet, indem wir Daten-URLs inline eingefügt haben.
  • Wir haben diese internen Daten-URLs dann CSP-freundlich gestaltet, indem wir beim Laden spezielle Groß- und Kleinschreibung verwendet haben.
  • Wir haben unsere Implementierung DOM-unabhängig und programmatisch nicht beobachtbar gemacht, indem wir Stile in das blink-interne viewport verschoben haben.

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

Weitere Informationen finden Sie in unserem Designvorschlag oder in dem Fehler beim Chromium-Tracking, in dem alle zugehörigen Patches aufgeführt sind.

Vorschaukanäle herunterladen

Du kannst Chrome Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Mit diesen Vorschaukanälen erhalten Sie Zugriff auf die neuesten Funktionen der Entwicklertools, können bahnbrechende Webplattform-APIs testen und Probleme auf Ihrer Website erkennen, noch bevor Ihre Nutzer dies tun.

Chrome-Entwicklertools-Team kontaktieren

Verwende die folgenden Optionen, um die neuen Funktionen und Änderungen im Beitrag oder andere Themen im Zusammenhang mit den Entwicklertools zu besprechen.

  • Sende uns über crbug.com einen Vorschlag oder Feedback.
  • Wenn du ein Problem mit den Entwicklertools melden möchtest, klicke in den Entwicklertools auf Weitere Optionen   Mehr   > Hilfe > Probleme mit Entwicklertools melden.
  • Senden Sie einen Tweet an @ChromeDevTools.
  • Hinterlasse Kommentare unter YouTube-Videos oder YouTube-Videos mit Tipps zu DevTools.