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

Mathias Bynens
Mathias Bynens

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

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

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

De acordo com a análise de acessibilidade da WebAIM sobre o 1 milhão de sites principais, 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. Ela é um pouco diferente das outras. As ferramentas acima se concentram principalmente nas informações da taxa de contraste de superfície e oferecem opções para corrigi-la. Percebemos que o DevTools ainda não tinha como os desenvolvedores understanding esse espaço problemático. Para resolver isso, implementamos a simulação de deficiência de visão na guia "Renderização do DevTools".

No Puppeteer, a nova API page.emulateVisionDeficiency(type) permite ativar programaticamente essas simulações.

Deficiências de visão de cores

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

Uma imagem colorida de giz de cera derretido, sem deficiência visual simulada
Uma imagem colorida de giz de cera derretido, sem deficiência visual simulada.
ALT_TEXT_HERE
O impacto da simulação da acromatopsia em uma imagem colorida de giz de cera derretido.
O impacto da simulação de deuteranopia em uma imagem colorida de giz de cera derretido.
O 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.
O 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 derretido.
O impacto da simulação de tritanopia em uma imagem colorida de giz de cera derretido.

Como desenvolvedor com visão regular, o DevTools pode mostrar uma taxa de contraste ruim para pares de cores que parecem bons para você. Isso acontece porque as fórmulas de taxa de contraste consideram essas deficiências de visão de cores! Você ainda consegue ler textos de baixo contraste em alguns casos, mas as 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 é encontrar a parte que falta: não só o DevTools pode ajudar você a encontrar e corrigir problemas de contraste, mas também entendê-los.

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

Antes de nos aprofundarmos na implementação do Blink Renderer do nosso recurso, ele ajuda a entender como você implementaria uma funcionalidade equivalente usando a tecnologia da Web.

Pense em cada uma dessas simulações de deficiência visual associada à percepção de cores como uma sobreposição que cobre a página inteira. 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 personalizada de filtro do 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>

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 matrizes para criar uma nova cor de [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 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.

Você pode estar se perguntando de onde vêm os números exatos do 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 fisiologicamente preciso de simulação da deficiência visual associada à percepção de cores 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. Veja uma demonstração:

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

  • A página já pode ter um filtro no elemento raiz que será substituído pelo nosso código.
  • A página pode já ter um elemento com id="deuteranopia", o que não está de acordo com nossa definição de filtro.
  • A página pode usar uma determinada estrutura do DOM e, ao inserir <svg> no DOM, podemos violar essas suposições.

De lado, o principal problema com essa abordagem é que fariamos alterações programaticamente observáveis na página. Se um usuário do DevTools inspecionar o DOM, ele poderá encontrar de repente um elemento <svg> que nunca adicionou ou uma filter de CSS que nunca escreveu. Seria confuso! Para implementar essa funcionalidade no DevTools, precisamos de uma solução que não tenha essas desvantagens.

Vamos conferir como tornar isso menos invasivo. Há duas partes nessa solução que precisamos ocultar: 1) o estilo CSS com a propriedade filter e 2) a definição de 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 do 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 de HTML. Isso significa que, em alguns casos, você pode omitir aspas nos valores dos atributos. No entanto, o SVG em arquivos separados precisa ser um XML válido, e a análise de XML é muito mais rigorosa que a do HTML. Confira nosso snippet 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 autônomo válido (e, portanto, XML), precisamos fazer algumas mudanças. 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 é a chamada "solidus", 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), mas como o XML e o SVG-in-HTML são compatíveis com essa abreviação de />, também é possível usá-la.

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

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

Não precisa mais injetar o 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. É possível codificar todo o arquivo em um URL usando um URL de dados. Para isso, 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 nenhum lugar, nem carregá-lo de disco ou pela rede apenas para usá-lo em nosso documento HTML. Então, em vez de nos referirmos ao nome do arquivo como 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, assim como antes. Não é necessário codificar o documento SVG em Base64 no URL. Isso só prejudicaria a leitura e aumentaria o tamanho do arquivo. Adicionamos barras invertidas ao 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 a tecnologia da Web. Curiosamente, nossa implementação final no Blink Renderer é bem parecida. Veja um utilitário auxiliar C++ que adicionamos para criar um URL de dados com 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 a usamos 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 precisar implementar novamente nada ou inventar novas rodas. Estamos implementando um recurso do Blink Renderer, aproveitando a plataforma Web.

Descobrimos como criar filtros SVG e transformá-los em URLs de dados que podem ser usados no valor da propriedade CSS filter. Você consegue pensar em algum problema com essa técnica? Acontece que não podemos depender do URL de dados 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 toma cuidado especial para ignorar a CSP para esses URLs de dados "internos" durante o carregamento.

Deixando de lado os 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 independente da propriedade filter do CSS. Ótimo. Vamos acabar com isso também.

Como evitar a dependência de CSS no documento

Só para recapitular, estamos aqui até agora:

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

Ainda dependemos dessa propriedade CSS filter, que pode modificar um filter no documento real e causar falhas. Ela também seria mostrada 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 foi criar uma nova propriedade CSS interna do Chrome que se comportasse como filter, mas com um nome diferente, como --internal-devtools-filter. Poderíamos 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 só funcione no elemento de que precisamos: o elemento raiz. No entanto, essa solução não seria ideal: duplicaríamos a funcionalidade que já existe com o filter e, mesmo se pudéssemos ocultar essa propriedade não padrão, os desenvolvedores da Web ainda poderiam descobrir e começar a usá-la, o que seria ruim para a plataforma da Web. Precisamos de outra maneira de aplicar um estilo CSS sem que ele seja observável no DOM. Sugestões?

A especificação CSS tem uma seção que apresenta o modelo de formatação visual usada, e um dos principais conceitos é a janela de visualização. Esta é a visualização visual pela qual os usuários consultam a página da Web. Um conceito intimamente relacionado é o bloco inicial, que é uma espécie de <div> de janela de visualização estilizável que só existe no nível das especificações. A especificação refere-se 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 nas especificações de CSS, com base nessa "janela de visualização".

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

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++ nem as complexidades do mecanismo de estilo do Blink para ver que esse código processa a z-index, display, position e overflow da janela de visualização (ou de forma mais precisa: o bloco que contém). Esses são todos os conceitos que você talvez conheça no CSS. Há outra magia 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 não faz parte do DOM.

Assim temos 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 tecnologia da Web em vez de C++ e depois começamos a trabalhar para mover partes dele para o renderizador Blink.

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

O que é único nessa 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, mesmo dentro do 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 pré-visualização dão acesso aos recursos mais recentes do DevTools, testam as APIs de plataforma da Web modernas e encontram problemas no seu site antes que os usuários o encontrem.

Entrar em contato com a equipe do Chrome DevTools

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

  • Envie uma sugestão ou feedback em crbug.com.
  • Informe um problema do DevTools em Mais opções   Mais   > Ajuda > Informar problemas no DevTools.
  • Publique no Twitter em @ChromeDevTools.
  • Deixe comentários nos vídeos do YouTube sobre o que há de novo ou nos vídeos do YouTube de dicas sobre o DevTools.