Имитация недостатков цветового зрения в Blink Renderer

В этой статье описывается, почему и как мы реализовали симуляцию дефицита цветового зрения в DevTools и Blink Renderer.

Фон: плохой цветовой контраст.

Низкоконтрастный текст — наиболее распространенная автоматически обнаруживаемая проблема доступности в Интернете.

Список распространенных проблем с доступностью в Интернете. Низкоконтрастный текст — безусловно, самая распространенная проблема.

Согласно анализу доступности 1 миллиона крупнейших веб-сайтов, проведенному WebAIM , более 86% домашних страниц имеют низкую контрастность. В среднем каждая домашняя страница содержит 36 различных экземпляров низкоконтрастного текста.

Использование DevTools для поиска, понимания и устранения проблем с контрастностью

Chrome DevTools может помочь разработчикам и дизайнерам улучшить контрастность и выбрать более доступные цветовые схемы для веб-приложений:

Недавно мы добавили в этот список новый инструмент, и он немного отличается от других. Вышеупомянутые инструменты в основном сосредоточены на отображении информации о коэффициенте контрастности и предоставлении возможностей ее исправления . Мы поняли, что DevTools по-прежнему не дает разработчикам возможности глубже понять эту проблемную область. Чтобы решить эту проблему, мы внедрили симуляцию дефицита зрения на вкладке «Визуализация» DevTools.

В Puppeteer новый API page.emulateVisionDeficiency(type) позволяет программно включать эти симуляции.

Недостатки цветового зрения

Примерно один из 20 человек страдает от дефицита цветового зрения (также известного как менее точный термин «дальтонизм»). Из-за таких нарушений становится сложнее различать разные цвета, что может усугубить проблемы с контрастностью .

Красочное изображение расплавленных мелков, не имитирующее нарушений цветового зрения.
Красочное изображение расплавленных мелков , не имитирующее нарушений цветового зрения.
ALT_TEXT_ЗДЕСЬ
Влияние имитации ахроматопсии на красочную картинку расплавленными мелками.
Влияние имитации дейтеранопии на красочную картинку расплавленными мелками.
Влияние имитации дейтеранопии на красочную картинку расплавленными мелками.
Влияние имитации протанопии на красочную картинку расплавленными мелками.
Влияние имитации протанопии на красочную картинку расплавленными мелками.
Влияние имитации тританопии на красочную картинку расплавленными мелками.
Влияние имитации тританопии на красочную картинку расплавленными мелками.

Как разработчик с обычным зрением, вы можете заметить, что DevTools отображает плохой коэффициент контрастности для пар цветов, которые вам визуально кажутся нормальными. Это происходит потому, что формулы коэффициента контрастности учитывают эти недостатки цветового зрения! В некоторых случаях вы все равно сможете читать текст с низкой контрастностью, но люди с нарушениями зрения не имеют такой привилегии.

Позволяя дизайнерам и разработчикам имитировать эффект этих недостатков зрения в своих собственных веб-приложениях, мы стремимся предоставить недостающую часть: DevTools не только может помочь вам найти и исправить проблемы с контрастностью, но теперь вы также можете понять их!

Имитация нарушений цветового зрения с помощью HTML, CSS, SVG и C++.

Прежде чем мы углубимся в реализацию нашей функции в Blink Renderer, полезно понять, как можно реализовать эквивалентную функциональность с использованием веб-технологий.

Вы можете рассматривать каждую из этих симуляций дефицита цветового зрения как наложение, покрывающее всю страницу. У веб-платформы есть способ сделать это: CSS-фильтры! С помощью свойства filter CSS вы можете использовать некоторые предопределенные функции фильтра, такие как blur , contrast , grayscale , hue-rotate и многие другие. Для еще большего контроля свойство filter также принимает URL-адрес, который может указывать на пользовательское определение фильтра 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>

В приведенном выше примере используется определение пользовательского фильтра на основе цветовой матрицы. Концептуально значение цвета каждого пикселя [Red, Green, Blue, Alpha] умножается на матрицу для создания нового цвета [R′, G′, B′, A′] .

Каждая строка матрицы содержит 5 значений: множитель (слева направо) для R, G, B и A, а также пятое значение для постоянного значения сдвига. Имеется 4 строки: первая строка матрицы используется для вычисления нового значения красного, вторая строка — зеленого, третья строка — синего и последняя строка — альфа.

Вам может быть интересно, откуда взялись точные цифры в нашем примере. Что делает эту цветовую матрицу хорошим приближением дейтеранопии? Ответ: наука! Значения основаны на физиологически точной модели дефицита цветового зрения, разработанной Мачадо, Оливейрой и Фернандесом .

В любом случае, у нас есть этот SVG-фильтр, и теперь мы можем применять его к произвольным элементам на странице с помощью CSS. Мы можем повторить ту же самую схему для других недостатков зрения. Вот демо того, как это выглядит:

Если бы мы захотели, мы могли бы построить нашу функцию DevTools следующим образом: когда пользователь эмулирует недостаток зрения в пользовательском интерфейсе DevTools, мы внедряем SVG-фильтр в проверяемый документ, а затем применяем стиль фильтра к корневому элементу. Однако при таком подходе есть несколько проблем:

  • Страница может уже иметь фильтр в корневом элементе, который затем может быть переопределен нашим кодом.
  • Возможно, на странице уже есть элемент с id="deuteranopia" , конфликтующий с нашим определением фильтра.
  • Страница может опираться на определенную структуру DOM, и вставляя <svg> в DOM, мы можем нарушить эти предположения.

Если оставить в стороне крайние случаи, основная проблема этого подхода заключается в том, что мы будем вносить в страницу программно наблюдаемые изменения . Если пользователь DevTools проверяет DOM, он может внезапно увидеть элемент <svg> который он никогда не добавлял, или filter CSS, который он никогда не писал. Это могло бы сбить с толку! Чтобы реализовать эту функциональность в DevTools, нам нужно решение, лишенное этих недостатков.

Давайте посмотрим, как мы можем сделать это менее навязчивым. В этом решении нам нужно скрыть две части: 1) стиль CSS со свойством filter и 2) определение фильтра SVG, которое в настоящее время является частью 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>

Как избежать зависимости SVG внутри документа

Начнем со второй части: как избежать добавления SVG в DOM? Одна из идей — переместить его в отдельный файл SVG. Мы можем скопировать <svg>…</svg> из приведенного выше HTML-кода и сохранить его как filter.svg , но сначала нам нужно внести некоторые изменения! Встроенный SVG в HTML соответствует правилам синтаксического анализа HTML. Это означает, что в некоторых случаях вам могут сойти с рук такие вещи, как отсутствие кавычек вокруг значений атрибутов . Однако предполагается, что SVG в отдельных файлах является допустимым XML, а синтаксический анализ XML гораздо более строгий, чем HTML. Вот еще раз наш фрагмент 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>

Чтобы сделать этот действительный автономный 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>

Первое изменение — это объявление пространства имен XML вверху. Второе дополнение — это так называемый «солидус» — косая черта, обозначающая, что тег <feColorMatrix> одновременно открывает и закрывает элемент. Это последнее изменение на самом деле не является необходимым (вместо этого мы могли бы просто использовать явный закрывающий тег </feColorMatrix> ), но поскольку и XML, и SVG-in-HTML поддерживают это сокращение /> , мы могли бы также использовать его.

В любом случае, с этими изменениями мы наконец можем сохранить это как действительный файл SVG и указать на него из значения свойства filter CSS в нашем HTML-документе:

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

Ура, нам больше не придется внедрять в документ SVG! Это уже намного лучше. Но… теперь мы зависим от отдельного файла. Это все еще зависимость. Можем ли мы как-то от этого избавиться?

Как оказалось, файл нам на самом деле не нужен. Мы можем закодировать весь файл в URL-адресе, используя URL-адрес данных. Чтобы это произошло, мы буквально берем содержимое файла SVG, который у нас был раньше, добавляем префикс data: настраиваем правильный тип MIME, и мы получаем действительный URL-адрес данных, который представляет тот же самый файл 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>

Преимущество заключается в том, что теперь нам больше не нужно где-либо хранить файл или загружать его с диска или по сети только для того, чтобы использовать его в нашем HTML-документе. Таким образом, вместо ссылки на имя файла, как мы делали раньше, теперь мы можем указать URL-адрес данных:

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

В конце URL-адреса мы по-прежнему указываем идентификатор фильтра, который хотим использовать, как и раньше. Обратите внимание: нет необходимости кодировать SVG-документ в URL-адресе с помощью Base64 — это только ухудшит читаемость и увеличит размер файла. Мы добавили обратную косую черту в конце каждой строки, чтобы символы новой строки в URL-адресе данных не завершали строковый литерал CSS.

До сих пор мы говорили только о том, как имитировать недостатки зрения с помощью веб-технологий. Интересно, что наша окончательная реализация в Blink Renderer на самом деле очень похожа. Вот вспомогательная утилита C++, которую мы добавили для создания URL-адреса данных с заданным определением фильтра, используя тот же метод:

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

И вот как мы используем его для создания всех необходимых нам фильтров :

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

Обратите внимание, что этот метод дает нам доступ ко всем возможностям SVG-фильтров без необходимости что-либо повторно реализовывать или изобретать какие-либо колеса. Мы реализуем функцию Blink Renderer, но делаем это за счет использования веб-платформы.

Итак, мы выяснили, как создавать фильтры SVG и превращать их в URL-адреса данных, которые мы можем использовать в значении свойства нашего filter CSS. Можете ли вы придумать проблему с этой техникой? Оказывается, мы не можем полагаться на загрузку URL-адреса данных во всех случаях, поскольку целевая страница может иметь Content-Security-Policy , которая блокирует URL-адреса данных. В нашей окончательной реализации на уровне Blink особое внимание уделяется обходу CSP для этих «внутренних» URL-адресов данных во время загрузки.

Если оставить в стороне крайние случаи, мы добились хорошего прогресса. Поскольку мы больше не зависим от присутствия встроенного <svg> в том же документе, мы фактически сократили наше решение до одного автономного определения свойства filter CSS. Большой! Теперь давайте избавимся и от этого.

Как избежать зависимости CSS в документе

Подведем итог: вот где мы находимся на данный момент:

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

Мы по-прежнему зависим от этого свойства filter CSS, которое может переопределить filter в реальном документе и что-то сломать. Это также будет отображаться при проверке вычисленных стилей в DevTools, что может сбить с толку. Как мы можем избежать этих проблем? Нам нужно найти способ добавить фильтр в документ так, чтобы он не был виден программно разработчикам.

Одна из идей заключалась в создании нового внутреннего CSS-свойства Chrome, которое ведет себя как filter , но имеет другое имя, например --internal-devtools-filter . Затем мы могли бы добавить специальную логику, чтобы это свойство никогда не отображалось в DevTools или в вычисляемых стилях в DOM. Мы могли бы даже убедиться, что он работает только с одним элементом, для которого он нам нужен: корневым элементом. Однако это решение не было бы идеальным: мы бы дублировали уже существующую функциональность с помощью filter , и даже если бы мы очень старались скрыть это нестандартное свойство, веб-разработчики все равно могли бы узнать о нем и начать его использовать, что было бы плохо для веб-платформы. Нам нужен какой-то другой способ применения стиля CSS, чтобы он не был виден в DOM. Есть идеи?

В спецификации CSS есть раздел, описывающий используемую модель визуального форматирования , и одним из ключевых понятий является область просмотра . Это визуальное представление, с помощью которого пользователи просматривают веб-страницу. Близкая концепция — это начальный содержащий блок , который похож на стилизуемую область просмотра <div> , которая существует только на уровне спецификации. В спецификации повсюду упоминается концепция «окна просмотра». Например, вы знаете, как браузер показывает полосы прокрутки, когда контент не помещается? Все это определено в спецификации CSS на основе этого «окна просмотра».

Этот viewport также существует в Blink Renderer как деталь реализации. Вот код , который применяет стили области просмотра по умолчанию в соответствии со спецификацией:

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

Вам не нужно разбираться в C++ или тонкостях движка Style Blink, чтобы понять, что этот код обрабатывает z-index , display , position и overflow области просмотра (или, точнее, исходного содержащего блока). Это все концепции, с которыми вы, возможно, знакомы по CSS! Есть и другая магия, связанная с наложением контекстов, которая не переводится напрямую в свойство CSS, но в целом вы можете думать об этом объекте viewport как о чем-то, что можно стилизовать с помощью CSS из Blink, точно так же, как элемент DOM, но это не так. часть ДОМ.

Это дает нам именно то, что мы хотим! Мы можем применить наши стили filter к объекту viewport , что визуально влияет на рендеринг, никоим образом не мешая наблюдаемым стилям страницы или DOM.

Заключение

Подводя итог нашему небольшому путешествию, мы начали с создания прототипа с использованием веб-технологий вместо C++, а затем начали работать над переносом его частей в Blink Renderer.

  • Сначала мы сделали наш прототип более автономным, встроив URL-адреса данных.
  • Затем мы сделали эти URL-адреса внутренних данных дружественными к CSP, задав для их загрузки специальный регистр.
  • Мы сделали нашу реализацию DOM-агностической и программно ненаблюдаемой, переместив стили во внутреннюю viewport Blink.

Уникальность этой реализации заключается в том, что наш прототип HTML/CSS/SVG в конечном итоге повлиял на окончательный технический проект. Мы нашли способ использовать веб-платформу даже в Blink Renderer!

Для получения дополнительной информации ознакомьтесь с нашим предложением по дизайну или с ошибкой отслеживания Chromium , которая ссылается на все соответствующие исправления.

Загрузите предварительный просмотр каналов

Рассмотрите возможность использования Chrome Canary , Dev или Beta в качестве браузера для разработки по умолчанию. Эти каналы предварительного просмотра предоставляют вам доступ к новейшим функциям DevTools, позволяют тестировать передовые API-интерфейсы веб-платформы и помогают находить проблемы на вашем сайте раньше, чем это сделают ваши пользователи!

Свяжитесь с командой Chrome DevTools

Используйте следующие параметры, чтобы обсудить новые функции, обновления или что-либо еще, связанное с DevTools.