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 ułatwieniami dostępu 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ę od innych. Opisane wyżej narzędzia koncentrują się na wyświetlaniu informacji o współczynniku kontrastu i umożliwiają poprawienie tych informacji. 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 włączanie tych symulacji za pomocą kodu.

Zaburzenia rozpoznawania barw

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

Kolorowe zdjęcie roztopionych kredek bez symulacji ślepoty barw
Kolorowy obraz roztopionych kredek, na którym nie występują symulowane zaburzenia rozpoznawania barw.
ALT_TEXT_HERE
Wpływ symulowania achromatopsji na kolorowy obraz roztopionych kredek.
Wpływ symulacji deuteranopii na kolorowe zdjęcie stopionych kredek.
Wyraźny wpływ symulacji deuteranopii na kolorowy obraz stopionych kredek.
Wpływ symulowania protanopii na kolorowy obraz roztopionych kredek.
Skutek symulacji protanopii na kolorowym obrazie stopionych kredek.
Wpływ symulacji tritanopii na kolorowe zdjęcie stopionych kredek.
Wpływ symulowania tritanopii na kolorowy obraz roztopionych 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. W niektórych przypadkach możesz czytać tekst o niskim kontraście, ale osoby niedowidzące nie mają takich uprawnień.

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 macierzy zawiera 5 wartości: mnożnik dla (od lewej do prawej) R, G, B i A, a także piąta wartość stałego przesunięcia. Są 4 wiersze: pierwszy wiersz macierzy służy do obliczania nowej wartości czerwonego, drugi – zielonego, trzeci – niebieskiego, a ostatni – alfa.

Być może zastanawiasz się, skąd pochodzą dokładne liczby podane 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 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, co możemy zrobić, aby ta funkcja była mniej uciążliwa. 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ć <svg>…</svg> z powyższego kodu HTML i zapisać go jako filter.svg, ale najpierw musimy wprowadzić pewne zmiany. Napisy SVG w kodzie HTML są zgodne z regułami analizy HTML. Oznacza to, że w niektórych przypadkach możesz zaniechać umieszczania wartości atrybutów w cudzysłowie. 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 fragment kodu 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>

Aby to było prawidłowe samodzielne SVG (a więc XML), musimy wprowadzić pewne zmiany. Czy potrafisz zrozumieć, co?

<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 na górze. 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 niej 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>

Hurra, 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 w rzeczywistości plik nie jest potrzebny. Możemy zakodować cały plik w adresie URL, używając adresu URL danych. W tym celu dosłownie bierzemy pod uwagę zawartość pliku SVG, dodajemy prefiks data:, konfigurujemy odpowiedni typ MIME i otrzymujemy prawidłowy adres URL danych, który 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 narzędzie pomocnicze w języku C++, które dodaliśmy do tej samej techniki, aby utworzyć adres URL danych z określoną definicją filtra:

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

A oto, jak wykorzystujemy go do tworzenia wszystkich potrzebnych 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 we wszystkich przypadkach polecać na wczytywanym adresie URL danych, ponieważ strona docelowa może mieć element Content-Security-Policy, który blokuje adresy URL danych. Na etapie ostatecznej implementacji na poziomie Blink dbamy o omijanie CSP w przypadku tych „wewnętrznych” adresów URL 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, jesteśmy na tym etapie:

<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ć błędy. 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.

Wpadliśmy na pomysł, aby utworzyć nową, wewnętrzną właściwość CSS Chrome, która działa jak filter, ale ma inną nazwę, np. --internal-devtools-filter. Następnie możemy dodać specjalne mechanizmy logiczne, aby ta właściwość nigdy nie pojawiała się w Narzędziach deweloperskich ani w stylach obliczeniowych w DOM. Mogliśmy nawet mieć pewność, że działa tylko z tym jednym elementem, którego potrzebujemy, czyli elementem 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 jest 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 kontentowy, który jest w pewien sposób podobny do okna nawigacyjnego z możliwością zmiany stylu <div>, które istnieje tylko na poziomie specyfikacji. Specyfikacja często wspomina o koncepcji „widoku”. Wiesz na przykład, jak przeglądarka wyświetla paski przewijania, gdy treść nie mieści się w dowolnym miejscu? 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 z nakładaniem 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 tą różnicą, ż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 ani 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.
  • Wprowadziliśmy w naszej implementacji niezależnej od DOM i uczyniliśmy ją nieobserwacyjną w sposób zautomatyzowany, przenosząc style do interfejsu viewport Blink-internal.

Wyjątkową cechą tej implementacji jest to, że 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.

Pobierz kanały podglądu

Rozważ użycie przeglądarki Chrome Canary, Dev lub Beta jako domyślnej przeglądarki deweloperskiej. 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.