Cómo simular deficiencias de visión de color en el procesador de Blink

En este artículo, se describe por qué y cómo implementamos la simulación de deficiencia de visión del color en Herramientas para desarrolladores y Blink Renderer.

Fondo: Contraste de color deficiente

El texto de contraste bajo es el problema de accesibilidad más común que se puede detectar automáticamente en la Web.

Lista de problemas comunes de accesibilidad en la Web. El texto con contraste bajo es, por mucho, el problema más común.

Según el análisis de accesibilidad de WebAIM sobre el 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.

Uso de las Herramientas para desarrolladores para encontrar, comprender y solucionar 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 aplicaciones web:

Hace poco, agregamos una herramienta nueva a esta lista, que es algo diferente a las demás. Las herramientas anteriores se enfocan principalmente en mostrar la información de la proporción de contraste y brindarte opciones para corregirla. Nos dimos cuenta de que a las Herramientas para desarrolladores aún les faltaba una forma de que los desarrolladores pudieran understanding más este espacio del problema. Para abordar esto, implementamos la simulación de deficiencia de visión en la pestaña Renderización de Herramientas para desarrolladores.

En Puppeteer, la nueva API de page.emulateVisionDeficiency(type) te permite habilitar estas simulaciones de manera programática.

Deficiencias en la visión de los colores

Alrededor de 1 de cada 20 personas sufre de una deficiencia en la visión de los colores (lo que también se conoce como el término menos preciso "daltonismo"). Esta discapacidad hace que sea más difícil distinguir diferentes colores, lo que puede amplificar los problemas de contraste.

Una imagen colorida de crayones derretidos sin deficiencias en la visión del color simulada
Una imagen colorida de crayones derretidos, sin deficiencias en la visión del color simulada.
ALT_TEXT_HERE
El impacto de simular la acromatopsia en una imagen colorida de crayones derretidos.
El impacto de la simulación de deuteranopia en una imagen colorida de crayones derretidos.
El impacto de simular deuteranopia en una imagen colorida de crayones derretidos.
El impacto de simular la protanopia en una imagen colorida de crayones derretidos.
El impacto de simular la protanopia en una imagen colorida de crayones derretidos.
El impacto de simular tritanopia en una imagen colorida de crayones derretidos.
El impacto de simular tritanopia en una imagen colorida de crayones derretidos.

Como desarrollador con visión regular, es posible que veas a Herramientas para desarrolladores una mala relación de contraste para los pares de colores que a ti te ven bien a nivel visual. Esto sucede porque las fórmulas de relación de contraste tienen en cuenta estas deficiencias en la visión del color. En algunos casos, aún podrás leer textos con contraste bajo, pero las personas con discapacidad visual no tienen ese privilegio.

Al permitir que los diseñadores y desarrolladores simulen el efecto de estas deficiencias de visión en sus propias apps web, nuestro objetivo es proporcionar la pieza que falta: Herramientas para desarrolladores no solo puede ayudarte a encontrar y corregir problemas de contraste, sino que ahora también puedes comprenderlos.

Cómo simular deficiencias en la visión de los colores con HTML, CSS, SVG y C++

Antes de que profundicemos en la implementación de nuestra función del Procesador Blink, nos ayudará a comprender cómo implementarías una funcionalidad equivalente usando tecnología web.

Puedes pensar en cada una de estas simulaciones de deficiencias en la visión del color como una superposición que cubre toda la página. La plataforma web tiene una forma de hacerlo: ¡los filtros 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 obtener aún más control, la propiedad filter también acepta una URL que puede apuntar a una definición de filtro SVG personalizado:

<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 en la matriz para crear un nuevo color [R′, G′, B′, A′].

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 cambio constante. Hay 4 filas: la primera fila de la matriz se usa para calcular el nuevo valor de rojo, la segunda fila de verde, la tercera fila de azul y la última fila alfa.

Es posible que te preguntes de dónde provienen los números exactos que aparecen en nuestro ejemplo. ¿Por qué esta matriz de color es una buena aproximación de la deuteranopia? La respuesta es: ¡ciencia! Los valores se basan en un modelo de simulación de deficiencias en la visión del color con precisión fisiológica de Machado, Oliveira y Fernandes.

De todos modos, tenemos este filtro SVG y ahora podemos aplicarlo a elementos arbitrarios en la página usando CSS. Podemos repetir el mismo patrón para otras deficiencias de la visión. Esta es una demostración de cómo se ve:

Si quisiéramos, podríamos compilar la función de Herramientas para desarrolladores de la siguiente manera: cuando el usuario emula una deficiencia de visión en la IU de Herramientas para desarrolladores, insertamos el filtro de SVG en el documento inspeccionado y, luego, aplicamos el estilo de 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", que no coincida con nuestra definición de filtro.
  • Es posible que la página dependa de una estructura de DOM determinada y, si se inserta <svg> en el DOM, podríamos infringir estas suposiciones.

Dejando de lado los casos extremos, el problema principal de este enfoque es que realizaríamos cambios observables en la página de forma programática. Si un usuario de Herramientas para desarrolladores inspecciona el DOM, es posible que, de repente, vea un elemento <svg> que nunca agregó o un filter de CSS que nunca escribió. ¡Sería confuso! Para implementar esta funcionalidad en Herramientas para desarrolladores, necesitamos una solución que no tenga estos inconvenientes.

Veamos cómo podemos hacer esto menos invasivo. Esta solución tiene dos partes que debemos ocultar: 1) el estilo de CSS con la propiedad filter, y 2) la definición del filtro SVG, que actualmente es 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>

Cómo evitar 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 otro archivo SVG. Podemos copiar el <svg>…</svg> del HTML anterior y guardarlo como filter.svg, pero primero debemos hacer algunos cambios. Los archivos SVG intercalados en HTML siguen las reglas de análisis de HTML. Eso significa que, en algunos casos, puedes dejar de usar acciones como omitir las comillas en los valores de atributos. Sin embargo, se supone que los archivos SVG en archivos separados son XML válidos, y el análisis de XML es mucho más estricto que HTML. Este es nuestro fragmento de SVG en HTML una vez más:

<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 hacer que este SVG independiente sea válido (y, por lo tanto, XML), debemos realizar 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 la llamada "solidus": la barra que indica la etiqueta <feColorMatrix> abre y cierra el elemento. Este último cambio no es realmente necesario (en su lugar, podemos limitarnos a la etiqueta de cierre explícita </feColorMatrix>), pero como XML y SVG en HTML admiten esta abreviatura />, también podemos usarla.

De todos modos, con esos cambios, podemos guardar esto como un archivo SVG válido y señalarlo desde el valor de la propiedad filter de CSS en nuestro documento HTML:

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

¡Hurra, ya no tenemos que inyectar SVG en el documento! Eso ya es mucho mejor. Pero... ahora dependemos de un archivo separado. Eso sigue siendo una dependencia. ¿Podemos deshacernos de él de alguna manera?

Resulta que no necesitamos un archivo. Podemos codificar todo el archivo dentro de una URL usando una URL de datos. Para que esto suceda, literalmente tomamos el contenido del archivo SVG que teníamos antes, agregamos el prefijo data: y configuramos el tipo de MIME adecuado, y obtuvimos 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 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. Entonces, en lugar de hacer referencia al nombre de archivo como lo hicimos antes, ahora podemos apuntar a la URL de 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, ya que esto solo perjudicaría la legibilidad y aumentará el tamaño del archivo. Agregamos barras inversas al final de cada línea para garantizar que los caracteres de salto de línea en la URL de datos no terminen el literal de cadena de CSS.

Hasta ahora, solo hemos hablado de cómo simular las deficiencias de visión con la tecnología web. Curiosamente, nuestra implementación final en el procesador Blink es bastante similar. Esta es una utilidad auxiliar de C++ que agregamos para crear una URL de datos con una definición de filtro determinada y basada en 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 utilizamos 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 volver a inventar ruedas. Estamos implementando una función Blink Renderer, pero lo hacemos aprovechando la plataforma web.

Bien, descubrimos cómo construir filtros SVG y convertirlos en URLs de datos que podamos usar dentro del 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 podría tener un Content-Security-Policy que bloquee las URLs de datos. Nuestra implementación final a nivel de Blink tiene especial cuidado de omitir la CSP para estas URLs de datos "internas" durante la carga.

Dejando de lado los casos extremos, logramos grandes avances. Como ya no dependemos de que <svg> intercalado esté presente en el mismo documento, redujimos de manera efectiva nuestra solución a una sola definición de propiedad filter de CSS autónoma. ¡Genial! Ahora desháganos de eso también.

Cómo evitar la dependencia de CSS en el documento

En resumen, esto es lo que hemos llegado hasta ahora:

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

Seguimos dependiendo de esta propiedad filter de CSS, que podría anular un filter en el documento real y provocar errores. También se muestra cuando se inspeccionan los estilos calculados en Herramientas para desarrolladores, lo que sería confuso. ¿Cómo podemos evitar estos problemas? Necesitamos encontrar una manera de agregar un filtro al documento sin que los desarrolladores puedan observarlo de manera programática.

Una idea fue crear una nueva propiedad de 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 Herramientas para desarrolladores ni en los estilos calculados en el DOM. Incluso podemos asegurarnos de que solo funcione en el único elemento para el que lo necesitemos: el elemento raíz. Sin embargo, esta solución no sería ideal: duplicaríamos la funcionalidad que ya existe con filter. Incluso si nos esforzamos por ocultar esta propiedad no estándar, los desarrolladores web aún podrían descubrirla y comenzar a usarla, lo que sería malo para la plataforma web. Necesitamos otra forma de aplicar un estilo de CSS sin que sea observable en el DOM. ¿Cómo puedo hacerlo?

La especificación de CSS tiene una sección que 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 similar a un viewport <div> con estilo que solo existe a nivel de las especificaciones. La especificación hace referencia a este concepto de "vista del puerto" en todos lados. Por ejemplo, ¿sabes que el navegador muestra barras de desplazamiento cuando el contenido no cabe? Todo esto se define en la especificación CSS, según este “viewport”.

Este viewport también existe en el procesador Blink, como un detalle de implementación. Este es el código que aplica los estilos de viewport predeterminados según la especificación:

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 es necesario que comprendas C++ ni las particularidades del motor Style de Blink para ver que este código controla los elementos z-index, display, position y overflow del viewport (o con mayor exactitud: el bloque inicial que lo contiene). Esos son todos los conceptos que quizás conozcas de CSS. Hay otra magia relacionada con el apilado de contextos, que no se traduce directamente en una propiedad de CSS, pero, en general, puedes pensar en este objeto viewport como algo que se puede diseñar con CSS desde Blink, al igual que un elemento DOM, excepto que no sea parte del DOM.

Esto nos da exactamente lo que queremos. Podemos aplicar nuestros estilos de filter al objeto viewport, que afecta visualmente la renderización, sin interferir con los estilos de página observables ni con el DOM de ninguna manera.

Conclusión

Para recapitular nuestro pequeño viaje aquí, comenzamos por compilar un prototipo con tecnología web en lugar de C++ y, luego, comenzamos a trabajar en el movimiento de partes de él al procesador Blink.

  • Primero, hicimos nuestro prototipo más independiente al intercalar las URLs de datos.
  • Luego, logramos que esas URLs de datos internos fueran compatibles con CSP mediante la carga de mayúsculas y minúsculas.
  • Movimos los estilos al viewport interno de Blink para que nuestra implementación sea independiente del DOM y no sea observable de manera programática.

Lo que hace que esta implementación sea única es que nuestro prototipo HTML/CSS/SVG terminó influyendo en el diseño técnico final. Encontramos una manera de usar la plataforma web, incluso dentro del procesador 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 vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, prueban las API de vanguardia de la plataforma web y te permiten encontrar problemas en tu sitio antes que los usuarios.

Cómo comunicarte con el equipo de Herramientas para desarrolladores de Chrome

Usa las siguientes opciones para hablar sobre las nuevas funciones y los cambios en la publicación, o cualquier otro aspecto relacionado con Herramientas para desarrolladores.

  • Envíanos una sugerencia o un comentario a través de crbug.com.
  • Informa un problema en Herramientas para desarrolladores con Más opciones   Más   > Ayuda > Informar problemas de Herramientas para desarrolladores en esta herramienta.
  • Envía un tweet a @ChromeDevTools.
  • Deje comentarios en las Novedades de los videos de YouTube de Herramientas para desarrolladores o en las sugerencias de Herramientas para desarrolladores los videos de YouTube.