Como simular deficiências de visão de cores no Blink Renderer

Este artigo descreve por que e como implementamos a simulação de deficiência de visão de cores no DevTools e no Blink Renderer.

Plano de fundo: baixo contraste de cor

Texto de baixo contraste é o problema de acessibilidade detectável automaticamente mais comum na Web.

Uma lista de problemas comuns de acessibilidade na Web. Texto de baixo contraste é, de longe, o problema mais comum.

De acordo com a análise de acessibilidade feita pela WebAIM no 1 milhão de principais sites (em inglês), mais de 86% das páginas iniciais têm baixo contraste. Em média, cada página inicial tem 36 instâncias distintas de texto de baixo contraste.

Como usar o DevTools para encontrar, entender e corrigir problemas de contraste

O Chrome DevTools pode ajudar desenvolvedores e designers a melhorar o contraste e escolher esquemas de cores mais acessíveis para apps da Web:

Adicionamos recentemente uma nova ferramenta a essa lista, que é um pouco diferente das outras. As ferramentas acima se concentram principalmente em mostrar informações sobre a taxa de contraste e oferecem opções para corrigir esse problema. Percebemos que o DevTools ainda não tinha uma maneira de os desenvolvedores entenderem melhor a compreensão desse problema. Para resolver isso, implementamos a simulação de deficiência visual na guia "Renderização" do DevTools.

No Puppeteer, a nova API page.emulateVisionDeficiency(type) permite ativar essas simulações de forma programática.

Deficiências na visão de cores

Aproximadamente 1 em cada 20 pessoas tem deficiência na visão de cores, também conhecida como o termo menos preciso "daltonismo". Essas deficiências dificultam a diferenciação de cores, o que pode amplificar problemas de contraste.

Uma imagem colorida de giz de cera derretidos, sem simulação de deficiências visuais de cor
Uma imagem colorida de giz de cera derretidos, sem simulação de deficiências de visão de cores.
.
ALT_TEXT_HERE
O impacto da simulação de acromatopsia em uma imagem colorida de giz de cera derretidos.
.
O impacto da simulação de deuteranopia em uma imagem colorida de giz de cera derretidos.
O impacto da simulação de deuteranopia em uma imagem colorida de giz de cera derretidos.
.
O impacto da simulação de protanopia em uma imagem colorida de giz de cera derretidos.
O impacto da simulação de protanopia em uma imagem colorida de giz de cera derretidos.
.
O impacto da simulação de tritanopia em uma imagem colorida de giz de cera derretidos.
O impacto da simulação de tritanopia em uma imagem colorida de giz de cera derretidos.

Como desenvolvedor com visão regular, talvez o DevTools exiba uma taxa de contraste ruim para pares de cores que parecem aceitáveis para você. Isso acontece porque as fórmulas da taxa de contraste consideram essas deficiências na visão de cores. Você ainda poderá ler textos de baixo contraste em alguns casos, mas pessoas com deficiência visual não têm esse privilégio.

Ao permitir que designers e desenvolvedores simulem o efeito dessas deficiências visuais nos próprios apps da Web, nosso objetivo é oferecer o que faltava: o DevTools pode ajudar você a encontrar e corrigir problemas de contraste, agora você também pode entendê-los.

Simular deficiências visuais de cor com HTML, CSS, SVG e C++

Antes de abordarmos a implementação do Blink Renderer do nosso recurso, é importante entender como você pode implementar uma funcionalidade equivalente usando a tecnologia da Web.

Pense em cada uma dessas simulações de deficiência na visão de cores como uma sobreposição que cobre a página inteira. A plataforma Web tem uma maneira de fazer isso: filtros CSS! Com a propriedade CSS filter, é possível usar algumas funções de filtro predefinidas, como blur, contrast, grayscale, hue-rotate, entre outras. Para ter ainda mais controle, a propriedade filter também aceita um URL que pode apontar para uma definição de filtro SVG personalizada:

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

O exemplo acima usa uma definição de filtro personalizada com base em uma matriz de cores. Conceitualmente, o valor da cor [Red, Green, Blue, Alpha] de cada pixel é multiplicado por matriz para criar uma nova cor [R′, G′, B′, A′].

Cada linha na matriz contém 5 valores: um multiplicador para (da esquerda para a direita) R, G, B e A, bem como um quinto valor para um valor de mudança constante. Há quatro linhas: a primeira linha da matriz é usada para calcular o novo valor de vermelho, a segunda linha é verde, a terceira linha é azul e a última linha é Alfa.

Você pode estar se perguntando de onde vêm os números exatos em nosso exemplo. O que torna essa matriz de cores uma boa aproximação da deuteranopia? A resposta é: ciência! Os valores são baseados em um modelo de simulação de deficiência na visão de cores fisiologicamente preciso de Machado, Oliveira e Fernandes.

De qualquer forma, temos esse filtro SVG e agora podemos aplicá-lo a elementos arbitrários na página usando CSS. Podemos repetir o mesmo padrão para outras deficiências visuais. Aqui está uma demonstração de como isso funciona:

Se quisermos, podemos criar nosso recurso no DevTools da seguinte maneira: quando o usuário emula uma deficiência visual na IU do DevTools, injetamos o filtro SVG no documento inspecionado e, em seguida, aplicamos o estilo do filtro ao elemento raiz. No entanto, essa abordagem tem vários problemas:

  • Talvez a página já tenha um filtro no elemento raiz, que nosso código pode substituir.
  • A página pode já ter um elemento com id="deuteranopia", o que está em conflito com nossa definição de filtro.
  • A página pode depender de uma determinada estrutura do DOM e, ao inserir o <svg> no DOM, podemos violar essas suposições.

Além dos casos extremos, o principal problema com essa abordagem é que fariamos mudanças observáveis na página de modo programático. Se um usuário do DevTools inspecionar o DOM, ele poderá encontrar de repente um elemento <svg> que nunca foi adicionado ou um filter do CSS que nunca gravou. Isso seria confuso. Para implementar essa funcionalidade no DevTools, precisamos de uma solução que não tenha essas desvantagens.

Vamos descobrir como tornar esse processo menos invasivo. Precisamos ocultar duas partes dessa solução: 1) o estilo CSS com a propriedade filter e 2) a definição do filtro SVG, que atualmente faz parte do 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>

Como evitar a dependência SVG no documento

Vamos começar com a parte 2: como podemos evitar a adição do SVG ao DOM? Uma ideia é movê-lo para um arquivo SVG separado. Podemos copiar o <svg>…</svg> do HTML acima e salvá-lo como filter.svg, mas antes precisamos fazer algumas mudanças. O SVG inline em HTML segue as regras de análise HTML. Isso significa que você pode evitar coisas como omitir aspas nos valores dos atributos em alguns casos. No entanto, o SVG em arquivos separados precisa ser um XML válido, e a análise de XML é muito mais rigorosa do que o HTML. Aqui está nosso snippet de SVG em HTML novamente:

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

Para tornar esse SVG independente válido (e, portanto, XML), precisamos fazer algumas alterações. Você consegue adivinhar qual?

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

A primeira mudança é a declaração de namespace XML na parte de cima. A segunda adição é chamada de "sólido", a barra que indica que a tag <feColorMatrix> abre e fecha o elemento. Essa última mudança não é realmente necessária (poderíamos simplesmente usar a tag de fechamento </feColorMatrix> explícita em vez disso), mas como XML e SVG-in-HTML são compatíveis com essa abreviação de />, podemos usá-la.

De qualquer forma, com essas mudanças, podemos finalmente salvar o arquivo como um arquivo SVG válido e apontá-lo pelo valor da propriedade CSS filter no nosso documento HTML:

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

Não precisamos mais injetar SVG no documento. Isso já está muito melhor. Mas agora dependemos de um arquivo separado. Isso ainda é uma dependência. Podemos nos livrar disso de alguma forma?

Na verdade, não precisamos de um arquivo. Podemos codificar todo o arquivo dentro de um URL usando um URL de dados. Para que isso aconteça, literalmente pegamos o conteúdo do arquivo SVG anterior, adicionamos o prefixo data:, configuramos o tipo MIME adequado e temos um URL de dados válido que representa o mesmo arquivo 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>

A vantagem é que agora não precisamos mais armazenar o arquivo em qualquer lugar nem carregá-lo do disco ou da rede apenas para usá-lo em nosso documento HTML. Portanto, em vez de nos referirmos ao nome do arquivo como fizemos anteriormente, agora podemos apontar para o URL dos dados:

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

No final do URL, ainda especificamos o ID do filtro que queremos usar, assim como antes. Não é necessário codificar o documento SVG em Base64 no URL porque isso prejudicaria a legibilidade e aumentaria o tamanho do arquivo. Adicionamos barras invertidas no final de cada linha para garantir que os caracteres de nova linha no URL de dados não encerrem o literal de string CSS.

Até agora, falamos apenas sobre como simular deficiências visuais usando tecnologia da Web. Curiosamente, nossa implementação final no Blink Renderer é bem parecida. Veja o utilitário auxiliar de C++ que adicionamos para criar um URL de dados com uma determinada definição de filtro, com base na mesma técnica:

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

Confira como estamos usando esse recurso para criar todos os filtros necessários:

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

Essa técnica nos dá acesso a toda a capacidade dos filtros SVG sem ter que reimplementar nada ou reinventar nenhuma roda. O recurso Blink Renderer foi implementado usando a plataforma Web.

Certo, então já descobrimos como criar filtros SVG e transformá-los em URLs de dados que podemos usar no valor da propriedade CSS filter. Você consegue pensar em algum problema com essa técnica? Na verdade, não podemos confiar que o URL de dados está sendo carregado em todos os casos, já que a página de destino pode ter um Content-Security-Policy que bloqueia os URLs de dados. Nossa implementação final no nível do Blink toma um cuidado especial para ignorar a CSP para esses URLs de dados "internos" durante o carregamento.

Deixando os casos extremos, fizemos um bom progresso. Como não dependemos mais de <svg> inline estar presente no mesmo documento, reduzimos nossa solução a apenas uma definição de propriedade filter de CSS independente. Ótimo! Agora vamos nos livrar disso também.

Como evitar a dependência de CSS no documento

Apenas para recapitular, é aqui que estamos até agora:

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

Ainda dependemos dessa propriedade CSS filter, que pode substituir um filter no documento real e causar erros. Ele também apareceria ao inspecionar os estilos calculados no DevTools, o que seria confuso. Como podemos evitar esses problemas? Precisamos encontrar uma maneira de adicionar um filtro ao documento sem que ele seja programaticamente observável para os desenvolvedores.

Uma ideia que surgiu é criar uma nova propriedade CSS interna do Chrome que se comporta como filter, mas tem um nome diferente, como --internal-devtools-filter. Podemos então adicionar uma lógica especial para garantir que essa propriedade nunca apareça no DevTools ou nos estilos computados no DOM. Podemos até mesmo garantir que ela funcione apenas no elemento necessário: o elemento raiz. No entanto, essa solução não seria a ideal: copiaríamos funcionalidades que já existem com o filter e, mesmo se tentarmos ocultar essa propriedade fora do padrão, os desenvolvedores Web ainda poderiam descobrir e começar a usá-la, o que seria ruim para a plataforma Web. Precisamos de outra maneira de aplicar um estilo CSS sem que ele seja observável no DOM. Sugestões?

A especificação do CSS tem uma seção que apresenta o modelo de formatação visual usado, e um dos principais conceitos é a janela de visualização. Essa é a visualização visual pela qual os usuários consultam a página da Web. Um conceito intimamente relacionado é o bloco que contém inicial, que é como uma janela de visualização <div> estilizada que só existe no nível da especificação. A especificação se refere a esse conceito de "janela de visualização" o tempo todo. Por exemplo, você sabe como o navegador mostra barras de rolagem quando o conteúdo não se encaixa? Tudo isso é definido na especificação do CSS, com base nessa "janela de visualização".

Esse viewport também existe no renderizador Blink, como um detalhe de implementação. Este é o código que aplica os estilos padrão da janela de visualização de acordo com a especificação:

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

Você não precisa entender C++ ou as complexidades do mecanismo de estilo do Blink para ver que esse código processa z-index, display, position e overflow da janela de visualização (ou, mais precisamente, do bloco que contém inicial). Esses são conceitos com os quais você deve estar familiarizado com o CSS. Há outra mágica relacionada ao empilhamento de contextos que não se traduz diretamente em uma propriedade CSS. Mas, no geral, você pode pensar nesse objeto viewport como algo que pode ser estilizado usando CSS no Blink, assim como um elemento DOM, mas ele não faz parte do DOM.

Isso é exatamente o que queremos. Podemos aplicar nossos estilos filter ao objeto viewport, o que afeta visualmente a renderização, sem interferir nos estilos de página observáveis ou no DOM de forma alguma.

Conclusão

Para recapitular nossa pequena jornada aqui, começamos criando um protótipo usando a tecnologia da Web em vez de C++ e, em seguida, começamos a trabalhar em partes móveis dele para o Blink Renderer.

  • Primeiro, tornamos nosso protótipo mais autossuficiente com a linha de URLs de dados.
  • Em seguida, tornamos esses URLs de dados internos compatíveis com CSP, aplicando maiúsculas e minúsculas especiais no carregamento.
  • Movemos os estilos para o viewport interno do Blink para tornar nossa implementação independente e programaticamente não observável.

O diferencial dessa implementação é que nosso protótipo de HTML/CSS/SVG acabou influenciando o design técnico final. Encontramos uma maneira de usar a plataforma Web, até mesmo no Blink Renderer!

Para mais informações, confira nossa proposta de design ou o bug de rastreamento do Chromium, que faz referência a todos os patches relacionados.

Fazer o download dos canais de visualização

Use o Chrome Canary, Dev ou Beta como seu navegador de desenvolvimento padrão. Esses canais de pré-lançamento dão acesso aos recursos mais recentes do DevTools, testam APIs modernas da plataforma Web e encontram problemas no seu site antes que os usuários o façam!

Entrar em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos e mudanças na postagem ou qualquer outro assunto relacionado ao DevTools.

  • Envie uma sugestão ou feedback pelo site crbug.com.
  • Informe um problema do DevTools usando Mais opções   Mais > Ajuda > Relate problemas no DevTools no DevTools.
  • Tuíte em @ChromeDevTools.
  • Deixe comentários nos vídeos do YouTube sobre as novidades do DevTools ou nos vídeos do YouTube com dicas sobre o DevTools.