Algoritmo para determinar el grado del modo oscuro en imágenes invertidas

11 de junio de 2024 • fjbalon

No son pocas las ocasiones en que los sitios web ofrecen, a través de código javascript y cookies, sistemas de switch (o conmutador, en cristiano) del sistema claro u oscuro. Por la salud visual de los lectores, creo importante mantener este elemento fácil. En mi experiencia personal, creo que con un modo claro (en base a blanco) y un moco oscuro (base a negro puro) es más que suficiente. Además, el segundo se supone que aporta un mejor rendimiento en pantallas amoled, y visualmente un negro puro queda mucho más estético, a pesar de la tentación de mantener unos tonos grisáceos que, según otros, mejoran enormemente la lectura; pero ni tengo el conocimiento para defenderlo ni el necesario para atacarlo. El negro puro se constituye como un elemento, en mi uso personal, básico: utilizo un gestor de ventanas en mosaico con fondo negro, el fondo de la terminal st en negro, etc. Quiero entonces que, en cohesión con mi ecosistema, el fondo sea negro. Simple.

¿Cómo lograrlo en un sitio web estático? Simple, CSS trae la solución en sí mismo: la función de medios CSS prefers-color-scheme se utiliza para detectar si un usuario ha solicitado temas de colores claros u oscuros, siendo que indica su preferencia a través de una configuración del sistema operativo o una configuración del agente de usuario que indica el propio navegador. De tal forma que podemos configurar las preferencias para el modo claro o light y oscuro o dark. Tampoco es estrictamente necesario establecer ambos, simplemente con dejar uno, el otro se establecería como defecto negativo.

@media (prefers-color-scheme: light) {
    body {
        color: black;
        background-color: white;
    }
}
     
@media (prefers-color-scheme: dark) {
    body {
        color: white;
        background-color: black;
    }
}

Pero el problema de todo modo switch de modo oscuro son las imágenes. No hablamos de imágenes vectoriales como SVG, que tienen ya el propio soporte en CSS, sino imágenes de mapas de bits que, a menudo, no tienen siempre transparencia y el fondo blanco contrasta con el fondo negro, dejando un aspecto muy poco estético. Yo, que debo ser muy quisquilloso, no puedo consentirlo.

Se me ocurren un par de formas de solucionar esto:

  1. Utilizando un switch con dos imágenes, una para modo claro y otra para modo oscuro. Indicando un CSS:
    .only-on-dark {
        display: none;
    }
        
    @media (prefers-color-scheme: dark) {
        .only-on-dark {
            display: block;
        }
        
        .only-on-light {
            display: none;
        }
    }

    De tal forma que se indique en el HTML ambas imágenes con la clase a la que se deben:

    <img class="only-on-dark" src="dark-image.png" />
    <img class="only-on-light" src="image.png" />

    Quizá sea preferible también embeberlo directamente en el HTML, de tal forma que:

    <picture>
        <!-- Usuario prefiere modo claro -->
        <source srcset="image.png" media="(prefers-color-scheme: light)"/>
        
        <!-- Usuario prefiere modo oscuro -->
        <source srcset="dark-image.png"  media="(prefers-color-scheme: dark)"/>
        
        <!-- Usuario no indica preferencia -->
        <img src="image.png"/>
    </picture>

    De esta forma evitaríamos la descarga de la variante oculta.

  2. Una seguna solución, más eficiente en cuanto a que no necesitaríamos dos versiones de la imagen, sería invertir a través de CSS el color de la imagen original (siempre en el caso de que la inversión no suponga la subversión de la imagen, claro está) en caso de encontrarnos en modo oscuro.

    De tal forma que mantendríamos una clase, sea por ejemplo color-invertible, que identificaría a la imagen:

    <img class="color-invertible" src="image.png"/>

    Para que en el CSS le indicásemos, dentro de prefers-color-scheme: dark:

    @media (prefers-color-scheme: dark) {
        .color-invertible {
            filter: invert(100%);
        }
    }

Pero, ¿y si no estoy usando como creo más conveniente un fondo totalmente negro? ¿y si el fondo que estoy usando es, por ejemplo, (28, 28, 28)?... Creo conveniente realizar antes un breve repaso al funcionamiento técnico del color RGB:

RGB es, entonces, un modelo de color basado en la síntesis aditiva (permite explicar la obtención de un color a partir de la suma de los componentes de color), que permite representar un color mediante la mezcla por adición de los tres colores de luz primarios (rojo, verde y azul - red, green and blue). Si bien el propio modelo de color RGB no define por sí mismo lo que significa exactamente cada uno de los valores primarios, siendo que pueden existir colores notablemente diferentes en distintos dispositivos a pesar de usar el mismo color, esto es algo que no aplica para este caso, pues nos movemos siempre dentro del propio modelo.

En computación, para indicar con qué proporción es mezclado cada color, se asigna un valor a cada uno de estos tres colores, de forma que el valor 0 indica que no interviene en la mezcla y, a medida que el valor aumenta, se entiende que aporta intensidad a ésta. Aunque el intervalo de valores podría ser cualquiera, es frecuente que cada color primario se codifique con un byte (8 bits). Así, de manera prácticamente estándar, la intensidad de cada uno de los componentes se mide según una escala del 0 al 255.

De tal forma que el rojo se obtiene con (255, 0, 0), el verde con (0, 255, 0) y el azul con (0, 0, 255), obteniendo en cada caso un resultado monocromático. La ausencia de color -o negro- se obtiene cuando los tres componentes son 0: (0, 0, 0). Amarillo sería (255, 255, 0), cian (0, 255, 255) y magenta (255, 0, 255). El color blanco de forma mezclando en el nivel máximo los colores primarios, o sea, (255, 255, 255). En el caso de la presente web, contamos con un gris propio de (28, 28, 28). El lector puede usar un color picker para experimentar y jugar con estos colores.

En términos más técnicos del tema objeto del artículo, invertir el color en RGB significa restar cada componente del color al valor máximo posible. Así:

255 - x = i

Donde x es el valor actual, e i el resultado invertido. Por ejemplo:

De blanco a negro:

(255 255 255) 255 - 255 = 0 0 0 0

De negro a blanco:

(0 0 0) 255 - 0 = 255 255 255 255

Por lo que si necesito que la inversión del color de fondo corresponda con el RGB (28, 28, 28):

255 - x = 28 x = 227

La teoría de inversión de color RGB la tenemos clara. Sin embargo, no podemos indicar directamente que requerimos el valor (277, 277, 277) al fondo de una imagen para que se corresponda con éste en modo oscuro. Ya que lo requerimos es el valor o porcentaje de inversión necesaria para que se produzca el cálculo anterior transparentemente.

Si el caso fuera invertir de blanco (255, 255, 255) a negro (0, 0, 0) y viceversa en todas las ocasiones, es decir, si mantenemos un fondo blanco, y negro en modo oscuro. Simplemente valdría con usar la unidad filter: invert(1); ó el porcentaje total filter: invert(100%);.

Pero si requerimos de una inversión parcial, la fórmula sería:

r = x ( 1 - p ) + ( 255 - x ) p

Donde r es el resultado deseado (o sea, 28), x el valor original (del blanco, 255) y p el porcentaje deseado. Desarrollamos:

28 = 255 ( 1 - p ) + ( 255 - 255 ) p 28 = 255 * p 0 p 28 = 255 * p p = 28 255 p = 0.109803921569

Finalmente, el valor p lo convertimos a porcentaje y restamos del total para obtener el valor deseado:

0.109803921569 * 100 = 10.9803921569 100 - 10.9803921569 = 89.0196078431

Obtniendo así el filtro de inversión filter: invert(89.0196078431%);. He desarrollado un pequeño código que automatiza este cálculo:

#include <stdio.h>
#include <stdlib.h>

double inversion(int x);

int main(int argc, char *argv[]) {
    if (argc != 4) {
        printf("Uso: %s r g b\n", argv[0]);
        return 1;
    }

    int r = atoi(argv[1]);
    int g = atoi(argv[2]);
    int b = atoi(argv[3]);

    printf("Valor RGB objetivo: (%d, %d, %d)\n", r, g, b);

    inversion(r);
    inversion(g);
    inversion(b);

    return 0;
}

double inversion(int x) {
    double r = (double)x /255.0;
    double p = 100.0 - (r * 100.0);
    printf("%f\n", p);
    return p;
}

Obtniendo así el valor de inversión parcial que permite filtrar la imagen y que se mimetice con el fondo de una forma plenamente estética y armoniosa. He aquí un ejemplo de la imagen invertida para con el fondo actual (28, 28, 28) con un valor de filtro del 89.0196078431%:

En computación, principalmente en algunos lenguajes como HTML, CSS o Javascript, se emplea la notación hexadecimal del color. Este sistema combina los tres códigos numéricos en dos dígitos hexadecimales para expresar las intensisdades de los colores primarios RGB. Es decir, es un apantallamiento del RGB.

Así, como codificamos en hexadecimal, la base correspnde a la numeración del 0 al 9 y las seis letras a, b, c, d, e, y f. No corresponde a este artículo desarrollar la numeración hexadecimal. Entonces, la intensidad máxima es ff, que se corresponde con (15×16)+15=255 en decimal. La nula, evidentemente es 00. Así, por ejemplo el valor RGB (113, 97, 228) corresponde al valor hexadecimal #7161E4.