Gesimuleerde kleurwaarnemingstekortkomingen in de Blink Renderer

Dit artikel beschrijft waarom en hoe we simulatie van kleurzichtdeficiëntie hebben geïmplementeerd in DevTools en de Blink Renderer.

Achtergrond: slecht kleurcontrast

Tekst met laag contrast is het meest voorkomende automatisch detecteerbare toegankelijkheidsprobleem op internet.

Een lijst met veelvoorkomende toegankelijkheidsproblemen op internet. Tekst met laag contrast is veruit het meest voorkomende probleem.

Volgens WebAIM's toegankelijkheidsanalyse van de 1 miljoen beste websites heeft meer dan 86% van de startpagina's een laag contrast. Gemiddeld heeft elke startpagina 36 verschillende exemplaren van tekst met laag contrast.

DevTools gebruiken om contrastproblemen te vinden, te begrijpen en op te lossen

Chrome DevTools kan ontwikkelaars en ontwerpers helpen het contrast te verbeteren en toegankelijker kleurenschema's voor webapps te kiezen:

We hebben onlangs een nieuwe tool aan deze lijst toegevoegd, en deze is een beetje anders dan de andere. De bovenstaande hulpmiddelen zijn voornamelijk gericht op het naar boven halen van informatie over de contrastverhouding en het bieden van opties om deze te repareren . We realiseerden ons dat DevTools nog steeds een manier miste voor ontwikkelaars om een ​​dieper inzicht in dit probleemgebied te krijgen. Om dit aan te pakken, hebben we een visie-deficiëntiesimulatie geïmplementeerd op het tabblad DevTools Rendering.

In Puppeteer kunt u met de nieuwe page.emulateVisionDeficiency(type) API deze simulaties programmatisch inschakelen.

Tekortkomingen in het kleurzicht

Ongeveer 1 op de 20 mensen lijdt aan een tekort aan kleurenzien (ook bekend als de minder nauwkeurige term "kleurenblindheid"). Dergelijke beperkingen maken het moeilijker om verschillende kleuren van elkaar te onderscheiden, wat contrastproblemen kan versterken .

Een kleurrijke afbeelding van gesmolten kleurpotloden, zonder gesimuleerde kleurwaarnemingstekortkomingen
Een kleurrijke afbeelding van gesmolten kleurpotloden , zonder gesimuleerde kleurwaarnemingstekortkomingen.
ALT_TEXT_HIER
De impact van het simuleren van achromatopsie op een kleurrijk beeld van gesmolten kleurpotloden.
De impact van het simuleren van deuteranopie op een kleurrijk beeld van gesmolten kleurpotloden.
De impact van het simuleren van deuteranopie op een kleurrijk beeld van gesmolten kleurpotloden.
De impact van het simuleren van protanopie op een kleurrijk beeld van gesmolten kleurpotloden.
De impact van het simuleren van protanopie op een kleurrijk beeld van gesmolten kleurpotloden.
De impact van het simuleren van tritanopie op een kleurrijk beeld van gesmolten kleurpotloden.
De impact van het simuleren van tritanopie op een kleurrijk beeld van gesmolten kleurpotloden.

Als ontwikkelaar met een normaal gezichtsvermogen ziet u mogelijk dat DevTools een slechte contrastverhouding weergeeft voor kleurparen die er visueel goed uitzien. Dit gebeurt omdat de contrastverhoudingsformules rekening houden met deze tekortkomingen in het kleurzicht! In sommige gevallen kunt u nog steeds tekst met laag contrast lezen, maar mensen met een visuele beperking hebben dat voorrecht niet.

Door ontwerpers en ontwikkelaars het effect van deze visuele tekortkomingen op hun eigen webapps te laten simuleren, willen we het ontbrekende stukje leveren: niet alleen kunnen DevTools u helpen contrastproblemen te vinden en op te lossen , u kunt ze nu ook begrijpen !

Simulatie van tekortkomingen in het kleurzicht met HTML, CSS, SVG en C++

Voordat we ingaan op de Blink Renderer-implementatie van onze functie, helpt het om te begrijpen hoe u gelijkwaardige functionaliteit kunt implementeren met behulp van webtechnologie.

U kunt elk van deze simulaties van kleurwaarnemingsdeficiëntie beschouwen als een overlay die de hele pagina beslaat. Het webplatform heeft een manier om dat te doen: CSS-filters! Met de CSS- filter kunt u een aantal vooraf gedefinieerde filterfuncties gebruiken, zoals blur , contrast , grayscale , hue-rotate en nog veel meer. Voor nog meer controle accepteert de filter ook een URL die kan verwijzen naar een aangepaste SVG-filterdefinitie:

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

In het bovenstaande voorbeeld wordt een aangepaste filterdefinitie gebruikt op basis van een kleurenmatrix. Conceptueel gezien wordt de kleurwaarde [Red, Green, Blue, Alpha] elke pixel matrixvermenigvuldigd om een ​​nieuwe kleur [R′, G′, B′, A′] te creëren.

Elke rij in de matrix bevat 5 waarden: een vermenigvuldiger voor (van links naar rechts) R, G, B en A, evenals een vijfde waarde voor een constante verschuivingswaarde. Er zijn vier rijen: de eerste rij van de matrix wordt gebruikt om de nieuwe rode waarde te berekenen, de tweede rij groen, de derde rij blauw en de laatste rij alfa.

U vraagt ​​zich misschien af ​​waar de exacte cijfers in ons voorbeeld vandaan komen. Wat maakt deze kleurenmatrix een goede benadering van deuteranopie? Het antwoord is: wetenschap! De waarden zijn gebaseerd op een fysiologisch accuraat simulatiemodel voor kleurwaarnemingsdeficiëntie van Machado, Oliveira en Fernandes .

Hoe dan ook, we hebben dit SVG-filter en we kunnen het nu toepassen op willekeurige elementen op de pagina met behulp van CSS. We kunnen hetzelfde patroon herhalen voor andere gezichtsstoornissen. Hier is een demo van hoe dat eruit ziet:

Als we dat zouden willen, zouden we onze DevTools-functie als volgt kunnen bouwen: wanneer de gebruiker een visuele tekortkoming emuleert in de DevTools-gebruikersinterface, injecteren we het SVG-filter in het geïnspecteerde document en passen we vervolgens de filterstijl toe op het hoofdelement. Er zijn echter verschillende problemen met deze aanpak:

  • De pagina heeft mogelijk al een filter op het hoofdelement, dat onze code dan kan overschrijven.
  • De pagina bevat mogelijk al een element met id="deuteranopia" , wat in strijd is met onze filterdefinitie.
  • De pagina kan afhankelijk zijn van een bepaalde DOM-structuur, en door de <svg> in de DOM in te voegen, kunnen we deze aannames schenden.

Afgezien van de randgevallen is het grootste probleem met deze aanpak dat we programmatisch waarneembare wijzigingen in de pagina aanbrengen . Als een DevTools-gebruiker de DOM inspecteert, ziet hij mogelijk plotseling een <svg> -element dat hij nooit heeft toegevoegd, of een CSS- filter dat hij nooit heeft geschreven. Dat zou verwarrend zijn! Om deze functionaliteit in DevTools te implementeren, hebben we een oplossing nodig die deze nadelen niet heeft.

Laten we eens kijken hoe we dit minder opdringerig kunnen maken. Deze oplossing bestaat uit twee delen die we moeten verbergen: 1) de CSS-stijl met de filter , en 2) de SVG-filterdefinitie, die momenteel deel uitmaakt van de 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>

Het vermijden van de SVG-afhankelijkheid in het document

Laten we beginnen met deel 2: hoe kunnen we voorkomen dat de SVG aan de DOM wordt toegevoegd? Eén idee is om het naar een afzonderlijk SVG-bestand te verplaatsen. We kunnen de <svg>…</svg> uit de bovenstaande HTML kopiëren en opslaan als filter.svg —maar we moeten eerst enkele wijzigingen aanbrengen! Inline SVG in HTML volgt de HTML-parseerregels. Dat betekent dat u in sommige gevallen weg kunt komen met zaken als het weglaten van aanhalingstekens rond attribuutwaarden . SVG in afzonderlijke bestanden wordt echter verondersteld geldige XML te zijn, en het parseren van XML is veel strenger dan HTML. Hier is nogmaals ons SVG-in-HTML-fragment:

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

Om deze geldige standalone SVG (en dus XML) te maken, moeten we enkele wijzigingen aanbrengen. Kun jij raden welke?

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

De eerste wijziging is de XML-naamruimtedeclaratie bovenaan. De tweede toevoeging is de zogenaamde “solidus” – de schuine streep die aangeeft dat de <feColorMatrix> -tag het element zowel opent als sluit. Deze laatste wijziging is eigenlijk niet nodig (we zouden in plaats daarvan gewoon de expliciete </feColorMatrix> afsluitende tag kunnen gebruiken), maar aangezien zowel XML als SVG-in-HTML deze /> afkorting ondersteunen, kunnen we er net zo goed gebruik van maken.

Hoe dan ook, met deze wijzigingen kunnen we dit eindelijk opslaan als een geldig SVG-bestand en ernaar verwijzen vanuit de CSS- filter in ons HTML-document:

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

Hoera, we hoeven geen SVG meer in het document te injecteren! Dat is al een stuk beter. Maar… we zijn nu afhankelijk van een apart dossier. Dat is nog steeds een afhankelijkheid. Kunnen we er op een of andere manier vanaf komen?

Het blijkt dat we eigenlijk geen bestand nodig hebben. We kunnen het volledige bestand binnen een URL coderen door een gegevens-URL te gebruiken. Om dit mogelijk te maken, nemen we letterlijk de inhoud van het SVG-bestand dat we eerder hadden, voegen we het data: prefix toe, configureren we het juiste MIME-type, en hebben we een geldige gegevens-URL die precies hetzelfde SVG-bestand vertegenwoordigt:

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>

Het voordeel is dat we het bestand nu nergens meer hoeven op te slaan, of van schijf of via het netwerk te laden, alleen maar om het in ons HTML-document te gebruiken. Dus in plaats van te verwijzen naar de bestandsnaam zoals we eerder deden, kunnen we nu naar de gegevens-URL verwijzen:

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

Aan het einde van de URL specificeren we nog steeds de ID van het filter dat we willen gebruiken, net als voorheen. Houd er rekening mee dat het niet nodig is om het SVG-document in de URL te coderen met Base64; dit zou de leesbaarheid alleen maar schaden en de bestandsgrootte vergroten. We hebben backslashes aan het einde van elke regel toegevoegd om ervoor te zorgen dat de tekens voor de nieuwe regel in de gegevens-URL de letterlijke CSS-tekenreeks niet beëindigen.

Tot nu toe hebben we het alleen gehad over hoe je gezichtsstoornissen kunt simuleren met behulp van webtechnologie. Interessant genoeg is onze uiteindelijke implementatie in de Blink Renderer eigenlijk vrij gelijkaardig. Hier is een C++-hulpprogramma dat we hebben toegevoegd om een ​​gegevens-URL te maken met een bepaalde filterdefinitie, gebaseerd op dezelfde techniek:

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

En zo gebruiken we het om alle filters te maken die we nodig hebben :

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

Merk op dat deze techniek ons ​​toegang geeft tot de volledige kracht van SVG-filters zonder dat we iets opnieuw hoeven te implementeren of wielen opnieuw hoeven uit te vinden. We implementeren een Blink Renderer-functie, maar we doen dit door gebruik te maken van het webplatform.

Oké, dus we hebben ontdekt hoe we SVG-filters kunnen construeren en deze kunnen omzetten in gegevens-URL's die we kunnen gebruiken binnen onze CSS- filter . Kun je een probleem bedenken met deze techniek? Het blijkt dat we er niet in alle gevallen op kunnen vertrouwen dat de gegevens-URL wordt geladen, omdat de doelpagina mogelijk een Content-Security-Policy heeft dat gegevens-URL's blokkeert. Bij onze uiteindelijke implementatie op Blink-niveau wordt er speciaal op gelet dat CSP voor deze “interne” gegevens-URL’s tijdens het laden wordt omzeild.

Afgezien van de randgevallen hebben we goede vooruitgang geboekt. Omdat we niet langer afhankelijk zijn van de aanwezigheid van inline <svg> in hetzelfde document, hebben we onze oplossing effectief teruggebracht tot slechts één op zichzelf staande CSS- filter . Geweldig! Laten we daar nu ook vanaf komen.

Het vermijden van de CSS-afhankelijkheid in het document

Om het samen te vatten: dit is waar we tot nu toe staan:

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

We zijn nog steeds afhankelijk van deze CSS- filter , die een filter in het echte document zou kunnen overschrijven en dingen kapot zou kunnen maken. Het zou ook verschijnen bij het inspecteren van de berekende stijlen in DevTools, wat verwarrend zou zijn. Hoe kunnen we deze problemen vermijden? We moeten een manier vinden om een ​​filter aan het document toe te voegen zonder dat het programmatisch waarneembaar is voor ontwikkelaars.

Eén idee dat naar voren kwam, was om een ​​nieuwe Chrome-interne CSS-eigenschap te maken die zich gedraagt ​​als filter , maar een andere naam heeft, zoals --internal-devtools-filter . We zouden dan speciale logica kunnen toevoegen om ervoor te zorgen dat deze eigenschap nooit verschijnt in DevTools of in de berekende stijlen in de DOM. We kunnen er zelfs voor zorgen dat het alleen werkt op dat ene element waarvoor we het nodig hebben: het rootelement. Deze oplossing zou echter niet ideaal zijn: we zouden functionaliteit dupliceren die al bestaat met filter , en zelfs als we ons best zouden doen om deze niet-standaard eigenschap te verbergen, zouden webontwikkelaars er nog steeds achter kunnen komen en deze kunnen gaan gebruiken, wat zou slecht zijn voor het webplatform. We hebben een andere manier nodig om een ​​CSS-stijl toe te passen zonder dat deze waarneembaar is in de DOM. Om het even welke ideeën?

De CSS-specificatie bevat een sectie waarin het visuele opmaakmodel wordt geïntroduceerd dat het gebruikt, en een van de belangrijkste concepten daar is de viewport . Dit is de visuele weergave waarmee gebruikers de webpagina raadplegen. Een nauw verwant concept is het initiële bevattende blok , dat lijkt op een stijlbare viewport <div> die alleen op specificatieniveau bestaat. De specificaties verwijzen overal naar dit ‘viewport’-concept. Weet u bijvoorbeeld hoe de browser schuifbalken toont als de inhoud niet past? Dit wordt allemaal gedefinieerd in de CSS-specificatie, gebaseerd op deze “viewport”.

Deze viewport bestaat ook binnen de Blink Renderer, als implementatiedetail. Hier is de code die de standaard viewport-stijlen toepast volgens de specificaties:

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

U hoeft C++ of de fijne kneepjes van de Style-engine van Blink niet te begrijpen om te zien dat deze code de z-index , display , position en overflow van de viewport (of beter gezegd: de initiaal die het blok bevat) afhandelt. Dat zijn allemaal concepten die je misschien kent van CSS! Er is nog meer magie gerelateerd aan het stapelen van contexten, wat zich niet direct vertaalt in een CSS-eigenschap, maar over het algemeen zou je dit viewport object kunnen zien als iets dat kan worden opgemaakt met behulp van CSS vanuit Blink, net als een DOM-element, maar dat is het niet. onderdeel van de DOM.

Dit geeft ons precies wat we willen! We kunnen onze filter toepassen op het viewport object, wat de weergave visueel beïnvloedt, zonder op enigerlei wijze de waarneembare paginastijlen of de DOM te verstoren.

Conclusie

Om onze kleine reis hier samen te vatten: we zijn begonnen met het bouwen van een prototype met behulp van webtechnologie in plaats van C++, en zijn vervolgens begonnen met het verplaatsen van delen ervan naar de Blink Renderer.

  • We hebben ons prototype eerst meer op zichzelf staand gemaakt door gegevens-URL's in te lijnen.
  • Vervolgens hebben we die interne gegevens-URL's CSP-vriendelijk gemaakt door het laden ervan in een speciale behuizing te plaatsen.
  • We hebben onze implementatie DOM-agnostisch en programmatisch niet-waarneembaar gemaakt door stijlen naar de Blink-internal viewport te verplaatsen.

Het unieke aan deze implementatie is dat ons HTML/CSS/SVG-prototype uiteindelijk het uiteindelijke technische ontwerp beïnvloedde. We hebben een manier gevonden om het webplatform te gebruiken, zelfs binnen de Blink Renderer!

Bekijk voor meer achtergrondinformatie ons ontwerpvoorstel of de Chromium-trackingbug die verwijst naar alle gerelateerde patches.

Download de voorbeeldkanalen

Overweeg het gebruik van Chrome Canary , Dev of Beta als uw standaard ontwikkelingsbrowser. Deze preview-kanalen geven u toegang tot de nieuwste DevTools-functies, testen geavanceerde webplatform-API's en ontdekken problemen op uw site voordat uw gebruikers dat doen!

Neem contact op met het Chrome DevTools-team

Gebruik de volgende opties om de nieuwe functies en wijzigingen in het bericht te bespreken, of iets anders gerelateerd aan DevTools.

  • Stuur ons een suggestie of feedback via crbug.com .
  • Rapporteer een DevTools-probleem met behulp van de opties MeerMeer > Help > Rapporteer een DevTools-probleem in DevTools.
  • Tweet op @ChromeDevTools .
  • Laat reacties achter op onze Wat is er nieuw in DevTools YouTube-video's of DevTools Tips YouTube-video's .