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

Mathias Bynens
Mathias Bynens

Ten artykuł opisuje, dlaczego i w jaki sposób wdrożyliśmy symulację niedostrzegania kolorów w Narzędziach deweloperskich i mechanizmie renderowania Blink.

Tło: słaby kontrast kolorów

Tekst o niskim kontraście to najczęstszy automatycznie wykrywalny problem z ułatwieniami dostępu w internecie.

Lista typowych problemów z ułatwieniami dostępu w internecie. Najczęstszym problemem jest tekst o niskim kontraście.

Według przeprowadzonej przez WebAIM analizy ułatwień dostępu obejmującej milion najpopularniejszych witryn ponad 86% stron głównych ma niski kontrast. Każda strona główna ma średnio 36 odrębnych wystąpień tekstu o niskim kontraście.

Korzystanie z Narzędzi deweloperskich do znajdowania, analizowania i rozwiązywania problemów z kontrastem

Narzędzia deweloperskie w Chrome mogą pomóc programistom i projektantom poprawić kontrast i wybrać bardziej przystępne schematy kolorów dla aplikacji internetowych:

Do tej listy dodaliśmy niedawno nowe narzędzie, które różni się od pozostałych. Powyższe narzędzia koncentrują się głównie na wyświetlaniu informacji o współczynniku kontrastu i udostępnianiu opcji poprawiania go. Zdaliśmy sobie sprawę, że w Narzędziach deweloperskich wciąż brakuje sposobu, który pozwala deweloperom lepiej understanding ten problem. Aby rozwiązać ten problem, wdrożyliśmy symulację wad wzroku na karcie Renderowanie w Narzędziach deweloperskich.

W Puppeteer nowy interfejs API page.emulateVisionDeficiency(type) umożliwia automatyczne włączanie tych symulacji.

Wady wzroku

Około 1 na 20 osób cierpi na zaburzenia rozpoznawania barw (nazywane też mniej precyzyjnym terminem „daltonizm”). Takie wady utrudniają rozróżnianie kolorów, co może nasilać problemy z kontrastem.

Kolorowy obraz roztopionych kredek bez symulowania zaburzeń rozpoznawania barw
Kolorowy obraz roztopionych kredek, bez symulowanych zaburzeń rozpoznawania barw.
ALT_TEXT_HERE
Wpływ symulacji achromatopsji na kolorowy obraz roztopionych kredek.
Wpływ symulacji deuteranopii na kolorowy obraz roztopionych kredek.
Wpływ symulowania deuteranopii na kolorowy obraz roztopionych kredek.
Wpływ symulacji protanopię na kolorowy obraz roztopionych kredek.
Wpływ symulacji protanozji na kolorowy obraz roztopionych kredek.
Wpływ symulacji tritanopii na kolorowy obraz roztopionych kredek.
Wpływ symulacji tritanopii na kolorowy obraz roztopionych kredek.

Jako programista możesz zauważyć, że w Narzędziach deweloperskich wyświetla się słaby współczynnik kontrastu dla par kolorów, które są dla Ciebie odpowiednie. Dzieje się tak, ponieważ wzory dotyczące współczynnika kontrastu uwzględniają te braki w rozpoznawaniu kolorów. W niektórych przypadkach możesz czytać tekst o niskim kontraście, ale osoby z wadą wzroku nie mają tego uprawnienia.

Umożliwiając projektantom i programistom symulowanie wpływu tych wad wzroku na ich aplikacje internetowe, staramy się znaleźć ten element, którego brakuje: Narzędzia deweloperskie nie tylko pomagają w znajdowaniu i naprawianiu problemów z kontrastem, ale także do ich zrozumienia.

Symulowanie zaburzeń rozpoznawania barw w językach HTML, CSS, SVG i C++

Zanim przejdziemy do wdrożenia naszej funkcji w ramach mechanizmu renderowania migawek, warto dowiedzieć się, jak można by ją wdrożyć, korzystając z technologii internetowej.

Każdą symulację zaburzeń rozpoznawania barw możesz traktować jako nakładkę, która pokrywa całą stronę. Dzięki Web Platform można to zrobić za pomocą filtrów CSS. Dzięki właściwości CSS filter możesz używać wstępnie zdefiniowanych funkcji filtrów, takich jak blur, contrast, grayscale, hue-rotate i wiele innych. Aby zapewnić jeszcze większą kontrolę, właściwość filter akceptuje też adres URL, który może wskazywać definicję niestandardowego 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 przykładzie powyżej użyto definicji filtra niestandardowego na podstawie tablicy kolorów. Oznacza to, że wartość koloru [Red, Green, Blue, Alpha] każdego piksela jest mnożona przez macierz, 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 oraz piąta wartość dla wartości stałego przesunięcia. Masz 4 wiersze: pierwszy wiersz macierzy jest używany do obliczenia nowej wartości „Red”, drugi wiersz „Zielony”, trzeci wiersz „Niebieski” i ostatni wiersz „Alfa”.

Być może zastanawiasz się, skąd pochodzą dokładne liczby w naszym przykładzie. Co sprawia, że ta macierz kolorów jest dobrym przybliżeniem deuteranopii? Odpowiedź brzmi: nauka! Wartości opierają się na fizjologicznym fizjologicznym modelu symulującym zaburzenia rozpoznawania barw przez Machado, Oliveirę i Fernandesa.

Tak czy inaczej, mamy filtr SVG i możemy zastosować go do dowolnych elementów na stronie za pomocą CSS. Ten sam wzór można powtarzać w przypadku innych wad wzroku. Oto przykład działania tej funkcji:

W takiej sytuacji moglibyśmy utworzyć funkcję Narzędzi deweloperskich w ten sposób: gdy użytkownik emuluje w interfejsie Narzędzi deweloperskich filtr SVG, robimy to w sposób, który wskazuje filtr SVG, a następnie stosujemy styl filtra do elementu głównego. Takie podejście wiąże się jednak z kilkoma problemami:

  • W głównym elemencie strony może już zawierać filtr, który nasz kod może zastąpić.
  • Strona może już zawierać element z atrybutem id="deuteranopia", który jest sprzeczny z definicją filtra.
  • Strona może opierać się na określonej strukturze DOM, a wstawienie elementu <svg> do DOM mogłoby naruszyć te założenia.

Poza innymi, głównym problemem związanym z takim podejściem jest to, że wprowadzamy na stronie dające się automatycznie dostrzegać zmiany. Jeśli użytkownik Narzędzi deweloperskich sprawdzi DOM, może nagle zobaczyć element <svg>, którego nie dodał, lub element CSS filter, którego nie utworzył. To byłoby mylące! Aby wdrożyć tę funkcję w Narzędziach deweloperskich, potrzebujemy rozwiązania bez tych wad.

Zobaczmy, co możemy zrobić, aby ta funkcja była mniej uciążliwa. Aby rozwiązać ten problem, musimy ukryć 2 elementy: 1) styl CSS z właściwością filter oraz 2) definicję filtra SVG, która obecnie jest 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 można uniknąć dodania SVG do DOM? Możesz na przykład przenieść ją 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. Plik SVG wbudowany w kod HTML jest zgodny z regułami analizy kodu HTML. Oznacza to, że w niektórych przypadkach możesz pominąć cudzysłów wokół wartości atrybutów. Jednak plik SVG w osobnych plikach powinien być prawidłowym kodem XML, a analiza XML jest znacznie bardziej rygorystyczna niż kod HTML. Oto jeszcze raz nasz 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 utworzyć ten prawidłowy samodzielny plik SVG (a tym samym format XML), musimy wprowadzić pewne zmiany. Czy zgadniesz, które?

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

Pierwsza zmiana to deklaracja przestrzeni nazw XML na górze. Drugie dodanie to tzw. „solidus” – ukośnik, który wskazuje, że tag <feColorMatrix> otwiera i zamyka element. Ta ostatnia zmiana nie jest konieczna (mogliśmy jedynie zachować jawny tag zamykający </feColorMatrix>), ale ponieważ zarówno XML, jak i SVG-in-HTML obsługują ten skrót />, warto go użyć.

Tak czy inaczej, po wprowadzeniu tych zmian możemy w końcu zapisać ten plik jako prawidłowy plik SVG i wskazać go za pomocą wartości właściwości CSS filter w naszym dokumencie HTML:

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

Nie musimy już dodawać formatu SVG do dokumentu. Już teraz działa dużo lepiej. Ale... teraz korzystamy z osobnego pliku. To nadal zależność. Czy możemy jakoś tego pozbyć?

Okazuje się jednak, że tak naprawdę nie potrzebujemy pliku. Możemy zakodować cały plik w adresie URL za pomocą adresu URL danych. Aby tak się stało, pobieramy dosłownie zawartość wcześniejszego pliku SVG, dodajesz prefiks data: i konfigurujemy odpowiedni typ MIME. Uzyskaliśmy prawidłowy adres URL danych reprezentujący 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>

Korzyścią jest to, że nie musimy już przechowywać pliku w żadnym miejscu ani wczytywać go z dysku lub przez sieć tylko po to, aby wykorzystać go w dokumencie HTML. Dlatego zamiast korzystać z nazwy pliku (tak jak wcześniej), możemy 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 chcesz użyć (tak jak wcześniej). Pamiętaj, że nie musisz kodować dokumentu SVG w adresie URL w standardzie Base64 – spowodowałoby to tylko ograniczenie czytelności i zwiększenie rozmiaru pliku. Na końcu każdego wiersza dodaliśmy ukośnik lewy, aby mieć pewność, że znaki nowego wiersza w adresie URL danych nie kończą literału ciągu CSS.

Na razie mówiliśmy tylko o symulowaniu zaburzeń widzenia przy użyciu technologii internetowej. Co ciekawe, ostateczna implementacja w mechanizmie renderowania Blink jest w rzeczywistości dość podobna. Oto narzędzie pomocnicze C++, które dodaliśmy w celu utworzenia adresu URL danych z określoną definicją filtra, korzystając w tej samej metodzie:

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 go do tworzenia 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 wszystkich możliwości filtrów SVG bez konieczności ponownego implementowania czy wymyślania żadnych kół. Wdrażamy funkcję renderowania mrugania, ale robimy to, korzystając z platformy internetowej.

Dobrze. Dowiesz się więc, jak tworzyć filtry SVG i przekształcać je w adresy URL danych, których można używać w obrębie wartości właściwości CSS filter. Czy przychodzi Ci do głowy jakiś problem z tą techniką? Okazuje się, że w każdym przypadku nie możemy polecać wczytywanym URL-om danych, ponieważ strona docelowa może zawierać tag Content-Security-Policy blokujący adresy URL danych. Ostateczna implementacja na poziomie Blinku uwzględnia te „wewnętrzne” adresy URL danych podczas ładowania strony.

Pomijając skrajne przypadki, robimy całkiem niezłe postępy. Nie korzystamy już z tego, czy funkcja <svg> znajduje się w obrębie tego samego dokumentu, dlatego ograniczyliśmy nasze rozwiązanie do tylko jednej, samodzielnej definicji właściwości CSS filter. Świetnie! A teraz tego pozbądźmy się.

Unikanie zależności CSS w dokumencie

Podsumowując, jesteśmy na tym etapie:

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

Nadal opieramy się na tej właściwości CSS filter, która może zastąpić filter w prawdziwym dokumencie i uszkodzić wszystko. Pojawia się również podczas sprawdzania stylów obliczonych w Narzędziach deweloperskich, co byłoby mylące. Jak możemy uniknąć tych problemów? Musimy znaleźć sposób na dodanie filtra do dokumentu, który nie będzie automatycznie obserwowalny dla programistów.

Wpadłem na pomysł utworzenia nowej, wewnętrznej właściwości CSS w Chrome, która działa 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 upewnić się, że będzie działać tylko w przypadku tego elementu, którego potrzebujemy w przypadku elementu głównego. Jednak to rozwiązanie nie byłoby idealne: powielalibyśmy funkcje, które już istnieją w filter, i nawet jeśli spróbujemy ukryć tę niestandardową usługę, programiści stron internetowych nadal mogli o niej dowiedzieć się o niej i zacząć z niej korzystać, co byłoby niekorzystne dla platformy internetowej. Potrzebujemy innego sposobu stosowania stylu CSS, który nie jest możliwy do obserwowania w DOM. Jakieś pomysły?

Specyfikacja CSS zawiera sekcję opisującą model formatowania wizualnego, który jest w niej używany, a jednym z kluczowych pojęć jest widoczny obszar. Jest to widok, przez który użytkownicy przeglądają stronę internetową. Ściśle powiązane pojęcie to początkowy blok zawierający blok, który przypomina widoczny obszar <div>, który można stylizować tylko na poziomie specyfikacji. Specyfikacja odnosi się do tego pojęcia „widocznego obszaru”. Na przykład wiesz, jak przeglądarka wyświetla paski przewijania, gdy treść nie pasuje? To wszystko jest zdefiniowane w specyfikacji CSS na podstawie tego „widocznego obszaru”.

Ten obiekt viewport jest też dostępny w ramach mechanizmu renderowania Blink jako szczegóły implementacji. Ten kod 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 rozumieć języka C++ ani niuansów mechanizmu stylu Blink, aby widzieć, że kod ten obsługuje (a dokładniej: początkowy element zawierający blok) z-index, display, position i overflow. To wszystkie pojęcia z usług porównywania cen, które być może znasz. Z układem kontekstów wiążą się inne magie, które nie przekładają się bezpośrednio na właściwość CSS, ale ogólnie można wyobrazić sobie obiekt viewport jako coś, co można stylizować za pomocą CSS z poziomu Blink tak jak element DOM. Jedyna różnica jest taka, że nie jest on częścią DOM.

W ten sposób dostaliśmy dokładnie to, czego chcemy. Do obiektu viewport możemy zastosować style filter, co ma wpływ na renderowanie, bez zakłócania w żaden sposób widocznych stylów strony ani DOM.

Podsumowanie

Podsumowując naszą krótką podróż, rozpoczęliśmy od zbudowania prototypu z wykorzystaniem technologii internetowej zamiast języka C++, a następnie rozpoczęliśmy pracę nad przeniesieniem jej części do mechanizmu renderowania Blink.

  • Najpierw uczyniliśmy prototyp bardziej niezależny przez wbudowanie adresów URL danych.
  • Następnie zaprojektowaliśmy te wewnętrzne adresy URL danych tak, by były łatwo dostępne dla CSP, przez zmianę wielkości liter ich wczytywania.
  • Przenosząc style do wewnętrznego interfejsu viewport w Blinku, sprawiliśmy, że nie jest ono zależne od DOM i nie jest dostępne programowo.

Wyjątkową cechą tego wdrożenia jest to, że nasz prototyp HTML/CSS/SVG wpływa na ostateczny projekt techniczny. Znaleźliśmy sposób na korzystanie z platformy sieciowej, nawet z mechanizmem renderowania Blink.

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

Pobieranie kanałów podglądu

Jako domyślnej przeglądarki programistycznej możesz użyć Chrome Canary, Dev lub Beta. Te kanały podglądu dają dostęp do najnowszych funkcji Narzędzi deweloperskich, testują nowoczesne interfejsy API platform internetowych oraz wykrywają problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Użyj tych opcji, aby omówić nowe funkcje i zmiany w poście lub wszelkich innych sprawach związanych z Narzędziami dla programistów.

  • Sugestię lub opinię możesz przesłać na stronie crbug.com.
  • Aby zgłosić problem z Narzędziami deweloperskimi, kliknij Więcej opcji   Więcej   > Pomoc > Zgłoś problemy z Narzędziami deweloperskimi.
  • zatweetować na @ChromeDevTools.
  • Komentarze do filmów o narzędziach dla deweloperów w YouTube lub filmach w YouTube ze wskazówkami dotyczącymi Narzędzi deweloperskich.