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 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.

Una lista de problemas comunes de accesibilidad en la Web. El texto con contraste bajo es, de lejos, el problema más común.

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:

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 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 percepción del 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.

Una imagen colorida de crayones derretidos, sin simular deficiencias en la visión del color
Una foto colorida de crayones derretidos, sin simulaciones de deficiencias en la visión de los colores.
ALT_TEXT_HERE
El impacto de simular 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 protanopía 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 tritanopia en una imagen colorida de crayones derretidos.
El impacto de simular la tritanopia en una imagen colorida de crayones derretidos.

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 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 colores sea una buena aproximación de la deuteranopía? 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.

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.

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 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 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>

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. Esto significa que puedes olvidarte de cuestiones como omitir comillas en los valores de 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 que aparece 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 (en su lugar, podríamos atenernos a la etiqueta de cierre </feColorMatrix> explícita), pero como tanto XML como SVG en HTML admiten esta abreviatura />, también deberíamos 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>

¡Genial! Ya no tenemos que insertar SVG en el documento. Eso ya es mucho mejor. Pero… ahora dependemos de un archivo independiente. 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, 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 un disco o por 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 reimplementar nada ni reinventar ninguna 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 podría tener un Content-Security-Policy que bloquea las URLs de datos. Nuestra implementación final a nivel de Blink tiene especial cuidado de omitir el 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 eliminemos eso.

Evita la dependencia de CSS en el documento

En resumen, hasta el momento, hicimos lo siguiente:

<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 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 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 <div> con diseño 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 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 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 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 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 tu navegador de desarrollo predeterminado. Estos canales de vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, te permiten probar API de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que los usuarios.

Comunícate con el equipo de Chrome DevTools

Usa las siguientes opciones para hablar sobre las nuevas funciones, actualizaciones o cualquier otro aspecto relacionado con Herramientas para desarrolladores.