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 deficiencias en la visión de colores en Herramientas para desarrolladores y Blink Renderer.

Fondo: mal contraste de color

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

Una 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 de contraste bajo.

Cómo usar las Herramientas para desarrolladores 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 seleccionar esquemas de colores más accesibles para aplicaciones web:

Hace poco agregamos una herramienta nueva a esta lista, que es un poco diferente a 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 Herramientas para desarrolladores aún le faltaba una forma de que los desarrolladores pudieran comprender mejor este espacio del problema. Para solucionarlo, implementamos la simulación de deficiencia de visión en la pestaña Rendering de Herramientas para desarrolladores.

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

Deficiencias en la visión del color

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

Una colorida imagen de crayones derretidos, sin deficiencias en la visión de los colores simuladas
Una imagen colorida de crayones derretidos, sin simulaciones de deficiencias en la visión de los colores.
ALT_TEXT_HERE
. El impacto de la simulación de acromatopsia en una imagen colorida de crayones derretidos.
El impacto de simular 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.
. Impacto de la simulación de tritanopia en una imagen colorida de crayones derretidos.

Como desarrollador con visión normal, es posible que las Herramientas para desarrolladores muestren una relación de contraste deficiente para los pares de colores que visualmente te parecen bien. Esto sucede porque las fórmulas de la proporció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.

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

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

Antes de profundizar en la implementación de Blink Renderer de nuestra función, resulta útil comprender cómo implementar una funcionalidad equivalente con tecnología web.

Puedes considerar cada una de estas simulaciones de deficiencia de visión del color como una superposición que cubre toda la página. La plataforma web tiene una manera de hacerlo: ¡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 tener 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 por una matriz para crear un nuevo color [R′, G′, B′, A′].

Cada fila de la matriz contiene 5 valores: un multiplicador (de izquierda a derecha) para 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 Rojo, la segunda fila el verde, la tercera fila Azul y la última fila Alfa.

Quizá te preguntes de dónde provienen los números exactos de nuestro ejemplo. ¿Qué hace que esta matriz de color sea una buena aproximación de la deuteranopia? La respuesta es: ¡ciencia! Los valores se basan en un modelo de simulación de deficiencia de visión de color fisiológicamente preciso de Machado, Oliveira y Fernandes.

Como sea, tenemos este filtro SVG, que ahora podemos aplicar a elementos arbitrarios de la página mediante CSS. Podemos repetir el mismo patrón para otras deficiencias de la visión. Aquí te mostramos una demostración de cómo se ve:

Si quisiéramos, podríamos compilar nuestra 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, inyectamos el filtro 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", lo que entra en conflicto con nuestra definición de filtro.
  • Es posible que la página dependa de una determinada estructura del DOM y, si insertas el <svg> en el DOM, podríamos infringir estas suposiciones.

Al margen de los casos extremos, el problema principal de este enfoque es que habríamos haciendo cambios observables de forma programática en la página. 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 estas desventajas.

Veamos cómo podemos hacer que esto sea menos invasivo. 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>

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. SVG intercalado en HTML sigue las reglas de análisis de HTML. Esto significa que puedes olvidarte de cuestiones como omitir comillas en los valores de atributos en algunos casos. Sin embargo, se supone que los SVG en archivos separados son XML válidos, y el análisis de XML es mucho más estricto que el HTML. Este es nuestro fragmento de SVG en HTML de nuevo:

<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 archivo SVG independiente (y, por lo tanto, XML) sea válido, 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 que aparece en la parte superior. La segunda adición es la denominada “solidus”, la barra que indica que la etiqueta <feColorMatrix> abre y cierra el elemento. Este último cambio no es realmente necesario (en su lugar, podríamos adherir a la etiqueta de cierre </feColorMatrix> explícita), pero como tanto XML como SVG en HTML admiten esta abreviatura />, también podemos usarla.

De todos modos, con esos cambios, finalmente podemos guardar esto como un archivo SVG válido y apuntar a él 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 insertar SVG en el documento. Eso ya es mucho mejor. Sin embargo, ahora dependemos de otro archivo. Eso sigue siendo una dependencia. ¿Podemos deshacernos de ella de alguna manera?

Resulta que, en realidad, no necesitamos un archivo. Podemos codificar todo el archivo dentro de una URL si usamos una URL de datos. Para que esto suceda, tomamos literalmente el contenido del archivo SVG que teníamos antes, agregamos el prefijo data:, 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 ya no necesitamos almacenar el archivo en ningún lugar ni cargarlo desde un disco o por la red solo para usarlo en nuestro documento HTML. Entonces, en lugar de referirnos al nombre del archivo como lo hicimos antes, ahora podemos apuntar 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, especificamos el ID del filtro que queremos usar, igual que antes. Ten en cuenta que no es necesario codificar en Base64 el documento SVG en la URL; hacerlo solo afectaría la legibilidad y aumentaría el tamaño del archivo. Agregamos barras inversas al final de cada línea para asegurarnos de que los caracteres de línea nueva en la URL de datos no terminen el literal de la cadena de CSS.

Hasta ahora, solo hemos hablado sobre cómo simular deficiencias en la visión con la tecnología web. Resulta interesante que nuestra implementación final en el renderizador Blink sea bastante similar. Esta es una utilidad auxiliar de C++ que agregamos para crear una URL de datos con una definición de filtro determinada, 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 aquí le mostramos cómo 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 reimplementar nada ni reinventar ninguna rueda. Estamos implementando una función del procesador Blink, pero lo hacemos aprovechando la plataforma web.

Ya vimos la manera de construir filtros SVG y convertirlos en URLs de datos que podamos usar dentro del valor de nuestra propiedad filter de CSS. ¿Se les ocurre algún problema con esta técnica? Resulta que, en realidad, no podemos confiar en la URL de datos que se carga 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 para evitar la CSP para estas URLs de datos "internas" durante la carga.

Dejando de lado los casos extremos, logramos avanzar bastante. Debido a que ya no dependemos de que <svg> intercalado esté presente en el mismo documento, redujimos de forma efectiva nuestra solución a una sola definición de propiedad filter de CSS independiente. ¡Genial! Ahora también eliminemos eso.

Cómo evitar la dependencia de CSS en el documento

En resumen, este es el punto 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 generar fallas. También aparecería cuando se inspeccionen los estilos calculados en Herramientas para desarrolladores, lo que sería confuso. ¿Cómo podemos evitar estos problemas? Debemos encontrar una manera de agregar un filtro al documento sin que los desarrolladores lo puedan observar de manera programática.

Se surgió una idea para crear una nueva propiedad de CSS interna de Chrome que se comporte como filter, pero que tiene 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 computarizados del DOM. Incluso podríamos asegurarnos de que solo funcione en el único 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 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 alguna otra manera de aplicar un estilo de CSS sin que sea observable en el DOM. ¿Cómo puedo hacerlo?

Las especificaciones de CSS tienen una sección que presenta el modelo de formato visual que usa, y uno de los conceptos clave es la vista del puerto. 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 con estilo <div> que solo existe a nivel de las especificaciones. La especificación hace referencia a este concepto de "vista del puerto" en muchos lugares. Por ejemplo, ¿sabes cómo muestra el navegador las barras de desplazamiento cuando el contenido no entra? Todo esto se define en las especificaciones de CSS, en función de esta “vista del puerto”.

Este viewport también existe dentro del 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 necesitas comprender C++ ni las complejidades del motor de estilos de Blink para comprobar que este código procesa los z-index, display, position y overflow del viewport (o, con mayor precisión, del bloque inicial que lo contiene). Esos son todos los conceptos que quizás conozcas de CSS. Hay otra magia relacionada con el apilamiento de contextos, que no se traduce directamente en una propiedad de CSS, pero, en general, podrías pensar en este objeto viewport como algo a lo que se le puede dar estilo con CSS desde Blink, como un elemento del DOM, excepto que no forma parte del DOM.

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

Conclusión

Para resumir nuestro pequeño recorrido, comenzamos por crear un prototipo con tecnología web en lugar de C++ y, luego, comenzamos a trabajar en partes de él en el procesador Blink.

  • Primero, hicimos que nuestro prototipo fuera más independiente integrando URLs de datos.
  • Luego, hicimos que esas URLs de datos internos fueran compatibles con CSP, a través de un uso especial de mayúsculas en su carga.
  • Hicimos que nuestra implementación fuera agnóstica del DOM y no se pudiera observar de manera programática. Para ello, trasladamos estilos a viewport de Blink-internal.

Lo que hace única a esta implementación 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 de Blink Renderer.

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 navegadores de desarrollo predeterminados. Estos canales de vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, prueban API de plataforma web de vanguardia y detectan problemas en tu sitio antes que los usuarios.

Comunicarse 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 otra cosa relacionada con Herramientas para desarrolladores.

  • Para enviarnos sugerencias o comentarios, accede a crbug.com.
  • Informa un problema en Herramientas para desarrolladores con Más opciones   Más > Ayuda > Informa problemas de Herramientas para desarrolladores en Herramientas para desarrolladores.
  • Twittea a @ChromeDevTools.
  • Deja comentarios en nuestros videos de YouTube de Herramientas para desarrolladores o en videos de YouTube de las Sugerencias de las Herramientas para desarrolladores.