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: contraste de cores ruim

Texto com baixo contraste é o problema de acessibilidade mais comum que pode ser detectado automaticamente na Web.

Uma lista de problemas comuns de acessibilidade na Web. Textos de baixo contraste são, de longe, o problema mais comum.

De acordo com a análise de acessibilidade da WebAIM sobre os 1 milhão de sites mais acessados, 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:

Recentemente, adicionamos uma nova ferramenta a essa lista, que é um pouco diferente das outras. As ferramentas acima se concentram principalmente em exibir informações sobre a proporção de contraste e oferecer opções para corrigir. Percebemos que o DevTools ainda não tinha uma maneira de os desenvolvedores entenderem melhor esse espaço de problemas. 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 das cores

Cerca de 1 em cada 20 pessoas (link em inglês) apresenta deficiência na percepção de cores, também conhecida pelo termo "daltonismo". Isso dificulta a diferenciação das cores, o que pode aumentar os problemas de contraste.

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

Como desenvolvedor com visão normal, você pode notar que o DevTools mostra uma taxa de contraste ruim para pares de cores que parecem estar bem para você. Isso acontece porque as fórmulas de taxa de contraste levam em conta essas deficiências de visão de cores. Você talvez ainda consiga ler textos de baixo contraste em alguns casos, mas pessoas com deficiências visuais 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 é fornecer a peça que faltava: o DevTools não apenas ajuda a encontrar e corrigir problemas de contraste, mas também a entendê-los.

Como simular deficiências na visão de cores com HTML, CSS, SVG e C++

Antes de mergulharmos na implementação do Blink Renderer do nosso recurso, é útil entender como você implementaria uma funcionalidade equivalente usando a tecnologia da Web.

Você pode pensar em cada uma dessas simulações de deficiência de visão de cores como uma sobreposição que cobre toda a página. A plataforma da 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 e muito mais. 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 de 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 cinco valores: um multiplicador para (da esquerda para a direita) R, G, B e A, além de um quinto valor para um valor de deslocamento constante. Há quatro linhas: a primeira linha da matriz é usada para calcular o novo valor vermelho, a segunda linha verde, a terceira linha azul e a última linha alfa.

Talvez você esteja se perguntando de onde vêm os números exatos do nosso exemplo. O que faz com que essa matriz de cores seja uma boa aproximação da deuteranopia? A resposta é: ciência! Os valores são baseados em um modelo de simulação de deficiência de 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. Confira uma demonstração:

Se quiséssemos, poderíamos criar o recurso do DevTools da seguinte maneira: quando o usuário emular uma deficiência visual na interface do DevTools, injetamos o filtro SVG no documento inspecionado e aplicamos o estilo do filtro no elemento raiz. No entanto, essa abordagem tem vários problemas:

  • A página pode já ter um filtro no elemento raiz, que nosso código pode substituir.
  • A página pode já ter um elemento com id="deuteranopia", conflitando com a definição do filtro.
  • A página pode se basear em 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 alterações observáveis na página de modo programático. Se um usuário das DevTools inspecionar o DOM, ele poderá encontrar um elemento <svg> que nunca foi adicionado ou um filter do CSS que nunca foi escrito. 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. Há duas partes dessa solução que precisamos ocultar: 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 de 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 primeiro precisamos fazer algumas mudanças. O SVG inline no HTML segue as regras de análise do HTML. Isso significa que você pode omitir aspas 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 criar um SVG independente válido (e, portanto, XML), precisamos fazer algumas mudanças. Sabe 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 do 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 usar a tag de fechamento </feColorMatrix> explícita), mas como o XML e o SVG-in-HTML oferecem suporte a essa abreviação />, podemos usá-la.

De qualquer forma, com essas mudanças, podemos finalmente salvar isso como um arquivo SVG válido e apontar para ele a partir do valor da propriedade CSS filter no documento HTML:

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

Ufa, 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 o arquivo inteiro em um URL usando um URL de dados. Para isso, pegamos o conteúdo do arquivo SVG que tínhamos antes, 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. Em vez de se referir ao nome do arquivo como fizemos antes, agora podemos apontar para o URL de 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, como antes. Não é necessário codificar o documento SVG em Base64 no URL. Isso só 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 do CSS.

Até agora, só falamos sobre como simular deficiências visuais usando tecnologia da Web. Curiosamente, nossa implementação final no Blink Renderer é bastante semelhante. Confira um utilitário auxiliar em 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 o 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 ao poder total dos filtros SVG sem precisar reimplementar nada ou reinventar a roda. O recurso Blink Renderer foi implementado usando a plataforma Web.

Agora sabemos como criar filtros SVG e transformá-los em URLs de dados que podem ser usados no valor da propriedade filter do CSS. Você consegue pensar em um 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 URLs de dados. Nossa implementação final no nível do Blink tem um cuidado especial para ignorar a CSP para esses URLs de dados "internos" durante o carregamento.

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

Como evitar a dependência de CSS no documento

Para recapitular, este é o que temos até agora:

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

Ainda dependemos dessa propriedade filter do CSS, que pode substituir um filter no documento real e causar problemas. 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 observável de forma programática 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é garantir que ele 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 que os usuários consultam na página da Web. Um conceito intimamente relacionado é o bloco de contenção inicial, que é como uma viewport <div> estilizável que existe apenas no nível de especificação. A especificação se refere a esse conceito de "janela de visualização" em todos os lugares. Por exemplo, você sabe como o navegador mostra barras de rolagem quando o conteúdo não cabe? Tudo isso é definido na especificação CSS com base nessa "janela de visualização".

Esse viewport também existe no Blink Renderer como um detalhe de implementação. Este é o código que aplica os estilos de viewport padrã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;
}

Não é necessário entender C++ ou as complexidades do mecanismo de estilo do Blink para saber que esse código processa o z-index, display, position e overflow da viewport (ou, mais precisamente, do bloco de contenção inicial). Esses são conceitos com os quais você deve estar familiarizado com o CSS. Há outra magia relacionada ao empilhamento de contextos que não se traduz diretamente em uma propriedade CSS. Porém, 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 nos dá exatamente o que queremos! Podemos aplicar nossos estilos filter ao objeto viewport, que afeta visualmente a renderização, sem interferir nos estilos de página observáveis ou no DOM.

Conclusão

Para recapitular nossa pequena jornada, começamos criando um protótipo usando tecnologia da Web em vez de C++, e depois começamos a mover partes dele para o renderizador Blink.

  • Primeiro, fizemos nosso protótipo mais independente, inserindo URLs de dados.
  • Em seguida, fizemos esses URLs de dados internos compatíveis com o CSP, carregando-os de forma especial.
  • Tornamos nossa implementação independente do DOM e não observável de forma programática, movendo estilos para o viewport interno do Blink.

O que é único nessa implementação é que nosso protótipo 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 navegador de desenvolvimento padrão. Esses canais de visualização dão acesso aos recursos mais recentes do DevTools, permitem testar APIs de plataforma da Web de última geração e ajudam a encontrar problemas no seu site antes que os usuários.

Entre em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos, atualizações ou qualquer outra coisa relacionada ao DevTools.