En este artículo, se describe por qué y cómo implementamos la simulación de deficiencias en la visión de colores en DevTools y el renderizador Blink.
Fondo: contraste de color deficiente
El texto con contraste bajo es el problema de accesibilidad más común que se puede detectar automáticamente en la Web.
Según el análisis de accesibilidad de WebAIM de los 1 millón de sitios web principales, más del 86% de las páginas principales tienen un contraste bajo. En promedio, cada página principal tiene 36 instancias distintas de texto con contraste bajo.
Cómo usar DevTools para encontrar, comprender y corregir problemas de contraste
Las Herramientas para desarrolladores de Chrome pueden ayudar a los desarrolladores y diseñadores a mejorar el contraste y elegir esquemas de colores más accesibles para las apps web:
- La información sobre la herramienta del modo de inspección que aparece en la parte superior de la página web muestra la proporción de contraste de los elementos de texto.
- El selector de color de DevTools identifica las relaciones de contraste deficientes para los elementos de texto, muestra la línea de contraste recomendada para ayudar a seleccionar mejores colores de forma manual y hasta puede sugerir colores accesibles.
- Tanto el panel Resumen de CSS como el informe de auditoría de accesibilidad de Lighthouse enumeran los elementos de texto con contraste bajo que se encuentran en tu página.
Recientemente, agregamos una nueva herramienta a esta lista, que es un poco diferente de las demás. Las herramientas anteriores se enfocan principalmente en mostrar información sobre la relación de contraste y brindarte opciones para corregirla. Nos dimos cuenta de que a DevTools aún le faltaba una forma para que los desarrolladores comprendieran mejor este espacio de problemas. Para abordar este problema, implementamos la simulación de deficiencia visual en la pestaña Renderización de DevTools.
En Puppeteer, la nueva API de page.emulateVisionDeficiency(type)
te permite habilitar estas simulaciones de forma programática.
Deficiencias en la visión de color
Aproximadamente 1 de cada 20 personas sufre de una deficiencia en la visión de los colores (lo que también se conoce como "daltonismo", un término menos exacto). Esta discapacidad hace que sea más difícil distinguir diferentes colores, lo que puede agravar los problemas de contraste.
Como desarrollador con visión normal, es posible que veas que DevTools muestra una mala relación de contraste para pares de colores que se ven bien. Esto sucede porque las fórmulas de la relación de contraste tienen en cuenta estas deficiencias en la visión de los colores. Es posible que puedas leer texto con contraste bajo en algunos casos, pero las personas con discapacidad visual no tienen ese privilegio.
Permitimos que los diseñadores y desarrolladores simulen el efecto de estas deficiencias visuales en sus propias apps web para proporcionar la pieza faltante: las Herramientas para desarrolladores no solo pueden ayudarte a encontrar y corregir los problemas de contraste, sino que ahora también puedes comprenderlos.
Cómo simular deficiencias en la visión de color con HTML, CSS, SVG y C++
Antes de profundizar en la implementación del renderizador Blink de nuestra función, es útil comprender cómo implementarías una funcionalidad equivalente con tecnología web.
Puedes pensar en cada una de estas simulaciones de deficiencias en la visión de colores como una superposición que cubre toda la página. La plataforma web tiene una forma de hacerlo: los filtros de CSS. Con la propiedad filter
de CSS, puedes usar algunas funciones de filtro predefinidas, como blur
, contrast
, grayscale
, hue-rotate
y muchas más. Para tener aún más control, la propiedad filter
también acepta una URL que puede apuntar a una definición 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>
En el ejemplo anterior, se usa una definición de filtro personalizado basada en una matriz de colores. Conceptualmente, el valor de color [Red, Green, Blue, Alpha]
de cada píxel se multiplica por una matriz para crear un color [R′, G′, B′, A′]
nuevo.
Cada fila de la matriz contiene 5 valores: un multiplicador para (de izquierda a derecha) R, G, B y A, así como un quinto valor para un valor de desplazamiento constante. Hay 4 filas: la primera fila de la matriz se usa para calcular el nuevo valor rojo, la segunda fila verde, la tercera fila azul y la última fila alfa.
Es posible que te preguntes de dónde provienen las cifras exactas de nuestro ejemplo. ¿Qué hace que esta matriz de colores sea una buena aproximación de la deuteranopía? La respuesta es: la ciencia. Los valores se basan en un modelo de simulación de deficiencia de visión de colores fisiológicamente preciso de Machado, Oliveira y Fernandes.
De cualquier manera, tenemos este filtro SVG y ahora podemos aplicarlo a elementos arbitrarios de la página con CSS. Podemos repetir el mismo patrón para otras deficiencias visuales. Esta es una demostración de cómo se ve:
Si quisiéramos, podríamos compilar nuestra función de DevTools de la siguiente manera: cuando el usuario emule una deficiencia visual en la IU de DevTools, insertamos el filtro SVG en el documento inspeccionado y, luego, aplicamos el estilo del filtro en el elemento raíz. Sin embargo, este enfoque tiene varios problemas:
- Es posible que la página ya tenga un filtro en su elemento raíz, que nuestro código podría anular.
- Es posible que la página ya tenga un elemento con
id="deuteranopia"
, lo que genera un conflicto con nuestra definición de filtro. - Es posible que la página dependa de una estructura de DOM determinada y, si insertamos el
<svg>
en el DOM, podríamos incumplir estas suposiciones.
Aparte de los casos extremos, el principal problema con este enfoque es que estaríamos realizando cambios observables de forma programática en la página. Si un usuario de DevTools inspecciona el DOM, es posible que, de repente, vea un elemento <svg>
que nunca agregó o un filter
de CSS que nunca escribió. Eso sería confuso. Para implementar esta funcionalidad en DevTools, necesitamos una solución que no tenga estas desventajas.
Veamos cómo podemos hacer que esto sea menos intrusivo. Esta solución tiene dos partes que debemos ocultar: 1) el estilo CSS con la propiedad filter
y 2) la definición del filtro SVG, que actualmente forma parte del 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>
Evita la dependencia de SVG en el documento
Comencemos con la parte 2: ¿cómo podemos evitar agregar el SVG al DOM? Una idea es moverlo a un archivo SVG independiente. Podemos copiar el <svg>…</svg>
del código HTML anterior y guardarlo como filter.svg
, pero primero debemos hacer algunos cambios. El SVG intercalado en HTML sigue las reglas de análisis de HTML. Eso significa que puedes omitir las comillas alrededor de los valores de los atributos en algunos casos. Sin embargo, se supone que el SVG en archivos separados es un XML válido, y el análisis de XML es mucho más estricto que el de HTML. Este es nuestro fragmento de SVG en 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>
Para que este SVG independiente sea válido (y, por lo tanto, XML), debemos hacer algunos cambios. ¿Puedes adivinar cuál?
<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>
El primer cambio es la declaración del espacio de nombres XML en la parte superior. La segunda adición es el llamado “solidus”, la barra que indica que la etiqueta <feColorMatrix>
abre y cierra el elemento. Este último cambio no es realmente necesario (podríamos usar la etiqueta de cierre </feColorMatrix>
explícita), pero como tanto XML como SVG en HTML admiten esta abreviatura />
, también podríamos usarla.
De cualquier manera, con esos cambios, podemos guardarlo como un archivo SVG válido y dirigirnos a él desde el valor de la propiedad filter
del CSS en nuestro documento HTML:
<style>
:root {
filter: url(filters.svg#deuteranopia);
}
</style>
¡Genial! Ya no tenemos que insertar SVG en el documento. Ya está mucho mejor. Pero… ahora dependemos de un archivo independiente. Eso sigue siendo una dependencia. ¿Podemos deshacernos de ella de alguna manera?
En realidad, no necesitamos un archivo. Podemos codificar todo el archivo dentro de una URL con una URL de datos. Para que esto suceda, literalmente tomamos el contenido del archivo SVG que teníamos antes, agregamos el prefijo data:
, configuramos el tipo MIME adecuado y tenemos una URL de datos válida que representa el mismo archivo 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>
El beneficio es que ahora ya no necesitamos almacenar el archivo en ningún lugar ni cargarlo desde el disco o a través de la red solo para usarlo en nuestro documento HTML. En lugar de hacer referencia al nombre del archivo como lo hicimos antes, ahora podemos dirigirnos a la URL de los datos:
<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>
Al final de la URL, seguimos especificando el ID del filtro que queremos usar, al igual que antes. Ten en cuenta que no es necesario codificar en Base64 el documento SVG en la URL. De lo contrario, solo se perjudicaría la legibilidad y se aumentaría el tamaño del archivo. Agregamos barras diagonales al final de cada línea para garantizar que los caracteres de salto de línea en la URL de datos no finalicen la cadena literal de CSS.
Hasta ahora, solo hablamos de cómo simular deficiencias visuales con tecnología web. Curiosamente, nuestra implementación final en el renderizador Blink es bastante similar. Esta es una utilidad de ayuda de C++ que agregamos para crear una URL de datos con una definición de filtro determinada, según la misma 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;
}
Y así es como lo usamos para crear todos los filtros que necesitamos:
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 "";
}
}
Ten en cuenta que esta técnica nos brinda acceso a toda la potencia de los filtros SVG sin tener que volver a implementar nada ni reinventar la rueda. Estamos implementando una función de Blink Renderer, pero lo hacemos aprovechando la plataforma web.
Bien, ya sabemos cómo construir filtros SVG y convertirlos en URLs de datos que podemos usar en el valor de la propiedad filter
de CSS. ¿Se te ocurre algún problema con esta técnica? Resulta que no podemos confiar en que la URL de datos se cargue en todos los casos, ya que la página de destino puede tener un Content-Security-Policy
que bloquea las URLs de datos. Nuestra implementación final a nivel de Blink se esfuerza especialmente por omitir CSP para estas URLs de datos "internas" durante la carga.
Sin contar los casos extremos, hemos logrado un buen progreso. Como ya no dependemos de que el <svg>
intercalado esté presente en el mismo documento, reducimos nuestra solución a una sola definición de propiedad filter
de CSS independiente. ¡Genial! Ahora, también quitemos eso.
Evita la dependencia de CSS en el documento
En resumen, hasta el momento, hicimos lo siguiente:
<style>
:root {
filter: url('data:…');
}
</style>
Aún dependemos de esta propiedad filter
de CSS, que podría anular un filter
en el documento real y generar errores. También aparecería cuando se inspeccionen los estilos computados en DevTools, lo que sería confuso. ¿Cómo podemos evitar estos problemas? Necesitamos encontrar una forma de agregar un filtro al documento sin que los desarrolladores puedan observarlo de forma programática.
Una idea que surgió fue crear una nueva propiedad CSS interna de Chrome que se comporte como filter
, pero que tenga un nombre diferente, como --internal-devtools-filter
. Luego, podríamos agregar una lógica especial para garantizar que esta propiedad nunca aparezca en DevTools ni en los estilos calculados en el DOM. Incluso podríamos asegurarnos de que solo funcione en el elemento para el que lo necesitamos: el elemento raíz. Sin embargo, esta solución no sería ideal: duplicaríamos la funcionalidad que ya existe con filter
y, aunque intentemos ocultar esta propiedad no estándar, los desarrolladores web podrían descubrirla y comenzar a usarla, lo que sería perjudicial para la plataforma web. Necesitamos otra forma de aplicar un estilo CSS sin que sea observable en el DOM. ¿Cómo puedo hacerlo?
La especificación de CSS tiene una sección en la que se presenta el modelo de formato visual que usa, y uno de los conceptos clave es el viewport. Esta es la vista visual a través de la cual los usuarios consultan la página web. Un concepto estrechamente relacionado es el bloque contenedor inicial, que es como un viewport <div>
que admite diseño y que solo existe a nivel de las especificaciones. La especificación hace referencia a este concepto de "viewport" en todas partes. Por ejemplo, ¿sabes cómo el navegador muestra barras de desplazamiento cuando el contenido no cabe? Todo esto se define en la especificación de CSS, según este "viewport".
Este viewport
también existe dentro del renderizador Blink, como un detalle de implementación. Este es el código que aplica los estilos de viewport predeterminados según las especificaciones:
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;
}
No necesitas comprender C++ ni las complejidades del motor de diseño de Blink para ver que este código controla el z-index
, display
, position
y overflow
del viewport (o, más precisamente, del bloque contenedor inicial). Todos estos son conceptos que quizás conozcas del CSS. Hay otros elementos mágicos relacionados con los contextos de apilamiento, que no se traducen directamente a una propiedad CSS, pero, en general, puedes considerar este objeto viewport
como algo que se puede diseñar con CSS desde Blink, al igual que un elemento DOM, excepto que no forma parte del DOM.
Esto nos da exactamente lo que queremos. Podemos aplicar nuestros estilos filter
al objeto viewport
, que afecta visualmente la renderización, sin interferir en los estilos de página observables ni en el DOM de ninguna manera.
Conclusión
Para recapitular nuestro pequeño recorrido, comenzamos por crear un prototipo con tecnología web en lugar de C++, y luego comenzamos a trabajar en trasladar partes de él al renderizador Blink.
- Primero, incorporamos las URLs de datos para que nuestro prototipo fuera más independiente.
- Luego, hicimos que esas URLs de datos internas sean compatibles con CSP, ya que cargamos con caracteres especiales.
- Hicimos que nuestra implementación sea independiente del DOM y no se pueda observar de forma programática trasladando los estilos al
viewport
interno de Blink.
Lo que hace que esta implementación sea única es que nuestro prototipo de HTML/CSS/SVG terminó influyendo en el diseño técnico final. Encontramos una forma de usar la plataforma web, incluso dentro del renderizador Blink.
Para obtener más información, consulta nuestra propuesta de diseño o el error de seguimiento de Chromium, que hace referencia a todos los parches relacionados.
Descarga los canales de vista previa
Considera usar Chrome Canary, Dev o Beta como tu navegador de desarrollo predeterminado. Estos canales de versión preliminar te brindan acceso a las funciones más recientes de DevTools, te permiten probar las APIs de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que tus usuarios.
Comunícate con el equipo de Chrome DevTools
Usa las siguientes opciones para hablar sobre las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con DevTools.
- Envíanos tus comentarios y solicitudes de funciones a crbug.com.
- Informa un problema de DevTools con Más opciones > Ayuda > Informar un problema de DevTools en DevTools.
- Twittea a @ChromeDevTools.
- Deja comentarios en los videos de YouTube sobre las novedades de DevTools o en los videos de YouTube sobre sugerencias de DevTools.