Symulowanie niedoskonałości widzenia kolorów w narzędziu renderowania Blink Renderer

Z tego artykułu dowiesz się, dlaczego i jak wdrożyliśmy symulację niedoboru widzenia kolorów w DevTools i w renderowaniu Blink.

Tło: zły kontrast kolorów

Tekst o niskim kontraście to najczęstszy problem z ułatwieniami dostępu w internecie, który można wykryć automatycznie.

lista typowych problemów z dostępnością w internecie; Tekst o niskim kontraście jest zdecydowanie najczęstszym problemem.

Według analizy ułatwień dostępu WebAIM dotyczącej miliona najpopularniejszych witryn ponad 86% stron głównych ma niski kontrast. Średnio na każdej stronie głównej występuje 36 różnych przypadków tekstu o niskim kontraście.

Wykrywanie, analizowanie i rozwiązywanie problemów z kontrastem za pomocą Narzędzi deweloperskich

Narzędzia deweloperskie w Chrome mogą pomóc deweloperom i projektantom w zwiększeniu kontrastu i wybraniu bardziej dostępnych schematów kolorów w aplikacjach internetowych:

Niedawno dodaliśmy do tej listy nowe narzędzie, które różni się nieco od pozostałych. Opisane powyżej narzędzia skupiają się głównie na wyświetlaniu informacji o stosunku kontrastu i udostępnianiu opcji naprawy. Zdaliśmy sobie sprawę, że w DevTools nadal brakuje sposobu na to, aby deweloperzy mogli lepiej zrozumieć ten obszar problemów. Aby rozwiązać ten problem, wprowadziliśmy symulację ślepoty barw na karcie Renderowanie w Narzędziach deweloperskich.

W Puppeteer nowy interfejs API page.emulateVisionDeficiency(type) umożliwia uruchamianie symulacji w ramach programowania.

ślepota barw,

Około 1 na 20 osób cierpi na zaburzenia rozpoznawania barw (znane też pod mniej dokładnym określeniem „ślepota barw”). Takie zaburzenia utrudniają odróżnianie różnych kolorów, co może pogłębiać problemy z kontrastem.

Kolorowe zdjęcie roztopionych kredek bez symulacji ślepoty barw
Zdjęcie kolorowych stopionych kredek bez symulowania zaburzeń rozpoznawania barw.
ALT_TEXT_HERE
Wyświetlanie kolorowego zdjęcia stopionych kredek w symulacji achromatopsji.
Wpływ symulacji deuteranopii na kolorowe zdjęcie stopionych kredek.
Wyraźny wpływ symulacji deuteranopii na kolorowy obraz stopionych kredek.
Wpływ symulacji protanopii na kolorowe zdjęcie stopionych kredek.
Skutek symulacji protanopii na kolorowym obrazie stopionych kredek.
Wpływ symulacji tritanopii na kolorowe zdjęcie stopionych kredek.
Wpływ symulacji tritanopii na kolorowe zdjęcie stopionych kredek.

Jako deweloper ze zwykłym wzrokiem możesz zobaczyć, że Narzędzia deweloperskie wyświetlają zły współczynnik kontrastu w przypadku par kolorów, które wyglądają na prawidłowe. Dzieje się tak, ponieważ wzory współczynnika kontrastu uwzględniają te niedobory. Ty możesz w niektórych przypadkach odczytać tekst o niskim kontraście, ale osoby z wadami wzroku nie mają tego przywileju.

Dajemy projektantom i programistom możliwość symulowania efektu tych niedoborów widzenia w ich własnych aplikacjach internetowych, aby zapewnić im brakujący element: DevTools nie tylko pomagają znaleźćnaprawić problemy z kontrastem, ale teraz można je też zrozumieć.

Symulowanie ślepoty barw za pomocą HTML, CSS, SVG i C++

Zanim przejdziemy do wdrażania naszej funkcji w renderowaniu Blink, warto zrozumieć, jak za pomocą technologii internetowych można wdrożyć równoważną funkcjonalność.

Każdą z tych symulacji niedowidzenia możesz traktować jako nakładkę na całą stronę. Platforma internetowa ma na to sposób: filtry CSS. Właściwość CSS filter umożliwia korzystanie z niektórych wstępnie zdefiniowanych funkcji filtra, takich jak blur, contrast, grayscale, hue-rotate i wiele innych. Aby uzyskać jeszcze większą kontrolę, możesz użyć właściwości filter, która przyjmuje adres URL, który może wskazywać niestandardową definicję filtra SVG:

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

W tym przykładzie użyto niestandardowej definicji filtra na podstawie matrycy kolorów. Oznacza to, że wartość koloru [Red, Green, Blue, Alpha] każdego piksela jest mnożona przez tablicę, aby utworzyć nowy kolor [R′, G′, B′, A′].

Każdy wiersz w macierzy zawiera 5 wartości: mnożnik dla (od lewej do prawej) R, G, B i A, a także piątą wartość dla stałej wartości przesunięcia. Są 4 wiersze: pierwszy wiersz macierzy służy do obliczania nowej wartości czerwonego, drugi – zielonego, trzeci – niebieskiego, a ostatni – alfa.

Zastanawiasz się pewnie, skąd pochodzą dokładne wartości w naszym przykładzie. Dlaczego ta matryca kolorów jest dobrym przybliżeniem deuteranopii? Odpowiedź: nauka. Wartości te są oparte na modelu symulacji niedoboru widzenia barw Machado, Oliveiry i Fernandesa, który jest fizjologicznie dokładny.

Mamy już filtr SVG, który możemy zastosować do dowolnych elementów na stronie za pomocą CSS. Ten sam schemat można powtórzyć w przypadku innych wad wzroku. Oto demonstracja tego, jak to działa:

Jeśli chcemy, możemy stworzyć funkcję DevTools w ten sposób: gdy użytkownik symuluje w interfejsie DevTools niedowidzenie, wstrzykujemy filtr SVG do dokumentu poddanego inspekcji, a potem stosujemy styl filtra do elementu ukorzeniającego. Takie podejście ma jednak kilka wad:

  • Strona może mieć już filtr na elemencie głównym, który może zostać zastąpiony przez nasz kod.
  • Strona może już zawierać element z wartością id="deuteranopia", który koliduje z definicją filtra.
  • Strona może wykorzystywać pewną strukturę DOM, a wstawiając <svg> do DOM, możemy naruszyć te założenia.

Poza przypadkami szczególnymi głównym problemem związanym z tym podejściem jest to, że wprowadzilibyśmy zmiany na stronie widoczne programowo. Jeśli użytkownik DevTools zbada DOM, może nagle zobaczyć element <svg>, którego nigdy nie dodał, lub styl CSS filter, którego nigdy nie napisał. To byłoby mylące. Aby wdrożyć tę funkcję w Narzędziach deweloperskich, potrzebujemy rozwiązania, które nie będzie miało tych wad.

Zobaczmy, jak możemy to zrobić w bardziej dyskretny sposób. Rozwiązanie to składa się z 2 części, które musimy ukryć: 1) styl CSS z właściwością filter oraz 2) definicją filtra SVG, która jest obecnie częścią 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>

Unikanie zależności SVG w dokumencie

Zacznijmy od części 2: jak uniknąć dodawania SVG do DOM? Możesz na przykład przenieść go do osobnego pliku SVG. Możemy skopiować element <svg>…</svg> z powyższego kodu HTML i zapisać go jako filter.svg, ale najpierw musimy wprowadzić pewne zmiany. Wstawione elementy SVG w kodzie HTML są interpretowane zgodnie z regułami parsowania HTML. Oznacza to, że w niektórych przypadkach możesz zaniechać stosowania cudzysłowów w wartościach atrybutów. Jednak pliki SVG w osobnych plikach powinny być prawidłowymi plikami XML, a analiza kodu XML jest dużo bardziej rygorystyczna niż analiza kodu HTML. Oto ponownie fragment kodu SVG w 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>

Aby to było prawidłowe samodzielne SVG (a więc XML), musimy wprowadzić pewne zmiany. Czy wiesz, który?

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

Pierwszą zmianą jest deklaracja przestrzeni nazw XML u góry. Drugim dodatkiem jest tzw. „solidus” – ukośnik, który wskazuje, że tag <feColorMatrix> otwiera i zamyka element. Ta ostatnia zmiana nie jest w ogóle konieczna (zamiast tego moglibyśmy użyć wyraźnego zamykającego tagu </feColorMatrix>), ale ponieważ zarówno XML, jak i SVG-in-HTML obsługują skrót </feColorMatrix>, możemy z niego skorzystać./>

Po wprowadzeniu tych zmian możemy zapisać plik jako prawidłowy plik SVG i odwołać się do niego z wartości właściwości CSS filter w dokumencie HTML:

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

Hura, nie musimy już wstrzykiwać SVG do dokumentu. Już dużo lepiej. Ale… teraz zależymy od osobnego pliku. To nadal jest zależność. Czy da się go jakoś pozbyć?

Okazuje się, że nie potrzebujemy pliku. Możemy zakodować cały plik w adresie URL, używając adresu URL danych. Aby to zrobić, pobieramy zawartość pliku SVG, dodajemy prefiks data:, konfigurujemy odpowiedni typ MIME i uzyskiwane jest prawidłowe URL danych, które reprezentuje ten sam plik 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>

Dzięki temu nie musimy już przechowywać pliku w żadnym miejscu ani wczytywać go z dysku lub sieci, aby użyć go w dokumencie HTML. Zamiast odwoływać się do nazwy pliku, jak to miało miejsce wcześniej, możemy teraz wskazać adres URL danych:

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

Na końcu adresu URL nadal podajemy identyfikator filtra, którego chcemy użyć, tak jak wcześniej. Pamiętaj, że nie musisz kodować dokumentu SVG w formacie Base64 w adresie URL. Spowoduje to tylko pogorszenie czytelności i zwiększenie rozmiaru pliku. Dodaliśmy ukośniki odwrotne na końcu każdego wiersza, aby mieć pewność, że znaki nowej linii w adresie URL danych nie kończą ciągu znaków w CSS.

Jak dotąd omawialiśmy tylko symulowanie niedostatków wzroku za pomocą technologii internetowych. Co ciekawe, nasza ostateczna implementacja w Blink Renderer jest w istocie bardzo podobna. Oto pomocnicza biblioteka C++, którą dodaliśmy, aby utworzyć adres URL danych z określoną definicją filtra na podstawie tej samej techniki:

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

Oto jak używamy tego narzędzia do tworzenia wszystkich potrzebnych nam filtrów:

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

Pamiętaj, że ta metoda daje nam dostęp do pełnej mocy filtrów SVG bez konieczności ponownego implementowania czegokolwiek lub wymyślania koła na nowo. Wprowadzamy funkcję Blink Renderer, ale wykorzystujemy do tego platformę internetową.

Wiemy już, jak tworzyć filtry SVG i przekształcać je w adresy URL danych, których możemy używać w wartości właściwości filter w arkuszu CSS. Czy wiesz, jakie są problemy z tą techniką? Okazuje się, że nie możemy polecać wczytywania adresu URL danych we wszystkich przypadkach, ponieważ strona docelowa może mieć Content-Security-Policy, który blokuje adresy URL danych. Nasze ostateczne wdrożenie na poziomie Blinka z szczególną starannością omija CSP w przypadku tych „wewnętrznych” adresów URL danych podczas wczytywania.

Poza przypadkami szczególnymi zrobiliśmy już spory postęp. Ponieważ nie zależy nam już na tym, aby tag <svg> był obecny w tym samym dokumencie, udało nam się ograniczyć nasze rozwiązanie do jednej samodzielnej definicji właściwości CSS filter. Świetnie. Pozbądźmy się tego.

Unikanie zależności CSS w dokumencie

Podsumowując, do tej pory udało nam się:

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

Nadal zależymy od tej właściwości CSS filter, która może zastąpić filter w rzeczywistym dokumencie i spowodować nieprawidłowe działanie. Pojawia się też podczas sprawdzania obliczonych stylów w Narzędziach deweloperskich, co może być mylące. Jak możemy uniknąć tych problemów? Musimy znaleźć sposób na dodanie filtra do dokumentu bez możliwości jego zaobserwowania przez programistów.

Jednym z rozwiązań było utworzenie nowej właściwości CSS wewnętrznej w Chrome, która zachowuje się jak filter, ale ma inną nazwę, np. --internal-devtools-filter. Następnie możemy dodać specjalną logikę, aby ta właściwość nigdy nie pojawiała się w Narzędziach deweloperskich ani w obliczonych stylach w DOM. Możemy nawet zadbać o to, aby działało tylko na jednym elemencie, którego potrzebujemy: elemencie głównym. To rozwiązanie nie byłoby jednak idealne: duplikowałybyśmy funkcję, która już istnieje w filter. Nawet jeśli uda nam się ukryć tę niestandardową właściwość, deweloperzy internetowi mogliby się o niej dowiedzieć i zacząć z niej korzystać, co byłoby niekorzystne dla platformy internetowej. Potrzebujemy innego sposobu stosowania stylu CSS, który nie będzie widoczny w DOM. Czy zna Pan/Pani taką osobę?

Specyfikacja CSS zawiera sekcję, w której przedstawiono używany model formatowania wizualnego. Jednym z kluczowych pojęć jest widok. To wizualizacja, która pozwala użytkownikom przeglądać stronę internetową. Pojęcie ściśle powiązane z tym zagadnieniem to początkowy blok kontentu, który jest w pewien sposób podobny do widoku <div>, który można stylizować, i istnieje tylko na poziomie specyfikacji. Specyfikacja często wspomina o koncepcji „widoku”. Czy wiesz na przykład, jak przeglądarka wyświetla suwaki, gdy treści nie mieszczą się w ramach ekranu? Wszystko to jest zdefiniowane w specyfikacji CSS na podstawie tego „widocznego obszaru”.

Ten viewport występuje też w Blink Rendererze jako szczegół implementacji. Oto kod, który stosuje domyślne style widocznego obszaru zgodnie ze specyfikacją:

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

Nie musisz znać języka C++ ani zawiłości silnika stylów Blink, aby zauważyć, że ten kod obsługuje z-index, display, positionoverflow widoku (a ściślej mówiąc: początkowego bloku zawierającego). To są wszystkie pojęcia, które znasz z CSS. Istnieje też inna magia związana ze sztancowaniem kontekstów, która nie jest bezpośrednio przekształcana w właściwość CSS, ale ogólnie można uznać, że obiekt viewport to coś, co można stylizować za pomocą CSS z poziomu Blink, tak jak element DOM, z tym że nie jest on częścią DOM.

Właśnie tego potrzebujemy! Możemy zastosować style filter do obiektu viewport, co wpływa na renderowanie wizualnie, bez wpływu na obserwowalne style strony lub interfejs DOM.

Podsumowanie

Podsumowując naszą małą przygodę, zaczęliśmy od stworzenia prototypu za pomocą technologii internetowej zamiast C++, a potem zaczęliśmy przenosić jego części do silnika Blink.

  • Najpierw ulepszyliśmy nasz prototyp, dodając do niego adresy URL danych.
  • Następnie dodaliśmy do tych adresów URL danych wewnętrznych obsługę CSP, stosując specjalne zasady ich ładowania.
  • Nasze wdrożenie jest niezależne od DOM i nie da się go zaobserwować programowo, ponieważ przenieśliśmy style do wewnętrznego viewport w Blink.

Ta implementacja jest wyjątkowa, ponieważ nasz prototyp HTML/CSS/SVG wpłynął na ostateczny projekt techniczny. Znaleźliśmy sposób na korzystanie z platformy internetowej nawet w ramach renderowania Blink.

Więcej informacji znajdziesz w naszej propozycji projektu lub w artykule o błędzie śledzenia w Chromium, który zawiera odniesienia do wszystkich powiązanych poprawek.

Pobieranie kanałów podglądu

Rozważ użycie jako domyślnej przeglądarki deweloperskiej wersji Canary, Dev lub Beta przeglądarki Chrome. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.