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.
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:
- A dica do modo de inspeção que aparece na parte de cima da página da Web mostra a taxa de contraste dos elementos de texto.
- O seletor de cores do DevTools indica proporções de contraste inadequadas para elementos de texto, mostra a linha de contraste recomendada para ajudar a selecionar manualmente cores melhores e pode até sugerir cores acessíveis.
- O painel "CSS Overview" e o relatório de auditoria de acessibilidade do Lighthouse listam elementos de texto de baixo contraste encontrados na página.
Recentemente, adicionamos uma nova ferramenta a essa lista, e ela é 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 corrigi-la. Percebemos que o DevTools ainda não tinha uma maneira de os desenvolvedores entenderem melhor esse espaço de problemas. Para resolver esse problema, implementamos a simulação de deficiência visual na guia "Renderização" das Ferramentas do desenvolvedor.
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.
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 pode ajudar a encontrar e corrigir problemas de contraste, além de 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 nosso 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.
- Talvez a página já tenha um elemento com
id="deuteranopia"
, conflitando com a definição do filtro. - A página pode depender de uma determinada estrutura de DOM e, ao inserir o
<svg>
no DOM, podemos violar essas suposições.
Deixando de lado os casos extremos, o principal problema dessa abordagem é que nós faríamos mudanças observáveis de forma programática na página. 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 ver como podemos tornar isso menos intrusivo. 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 pela parte 2: como podemos evitar adicionar o 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 a de HTML. Confira nosso snippet SVG-in-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 de namespace XML na parte de cima. A segunda adição é o chamado "solidus", o caractere 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. 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>
O benefício é que agora não precisamos mais armazenar o arquivo em nenhum lugar nem carregá-lo do disco ou pela rede para usá-lo no 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. Estamos implementando um recurso do Blink Renderer, mas usando a plataforma da 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? Não podemos confiar no URL de dados que 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 o cuidado especial de ignorar o 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 foi criar uma nova propriedade CSS interna do Chrome que se comporta como filter
, mas tem um nome diferente, como --internal-devtools-filter
. Podemos 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 ideal: estaríamos duplicando a funcionalidade que já existe com filter
. Mesmo que tentássemos esconder 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 do CSS tem uma seção que apresenta o modelo de formatação visual que ele usa, e um dos principais conceitos é o viewport. 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;
}
Você não precisa 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 que você já conhece do CSS. Há outras magias relacionadas aos contextos de empilhamento, que não são diretamente traduzidos para uma propriedade CSS, mas, de modo geral, você pode pensar nesse objeto viewport
como algo que pode ser estilizado usando CSS no Blink, assim como um elemento DOM, exceto que ele não faz parte do DOM.
Isso nos dá 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.
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 da Web, mesmo dentro do Blink Renderer.
Para mais informações, consulte 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.
- Envie feedback e solicitações de recursos para crbug.com.
- Informe um problema do DevTools usando a opção Mais opções > Ajuda > Informar um problema do DevTools no DevTools.
- Envie um tweet para @ChromeDevTools.
- Deixe comentários nos vídeos Novidades do DevTools no YouTube ou Dicas do DevTools no YouTube.