Emular el filtro Invert con Sass

Después de encontrar la manera de obtener el resultado del filtro invertido para un fondo sólido, la siguiente idea que me vino a la mente, naturalmente, era emular esto mismo con Sass con el fin de reproducir el efecto del filtro para todos los navegadores que no soportan filtros. Sass ya cuenta con una función de inversión, pero reproduce el efecto del filtro: invert(100%). El objetivo es implementar uno que funcione para cualquier porcentaje.

Si recuerdas, la solución JavaScript que implementamos la última vez, fue algo como esto:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    filter = (styles.webkitFilter || styles.filter),
    invert_arg = filter.match(/(0.d+)|d+/)[0], 
    upper = ((filter.indexOf('%') > -1)?(invert_arg/100):invert_arg)*255, 
    lower = 255 - upper, 
    original = styles.backgroundColor.split('('), 
    channels = original[1].match(/(0.d+)|d+/g), 
    alpha = (channels.length > 3)?(1*channels.splice(3, 1)[0]):1, 
    inverted_channels = channels.map(function(ch) {
      return 255 - Math.round(lower + ch*(upper - lower)/255);
    }), 
    inverted;

if(alpha !== 1) {
  inverted_channels.splice(3, 0, alpha);
}

inverted = original[0] + '(' + inverted_channels.join(', ') + ')';

Por lo que el primer paso, dados los canales rojo, verde y azul (que deben estar entre 0 y 255) y el argumento de la función de inversión, que debe tener un valor entre 0 y 1, o un porcentaje entre 0% y 100% (los valores fuera del rango AR están permitidos, pero tienen un límite), calculamos los límites del rango squished y los valores invertidos por cada canal.

Debería quedar algo como esto:

$red: 255;
$green: 165;
$blue: 0;
$percentage: 65%;

$original: rgb($red, $green, $blue);

$upper: ($percentage/100%)*255;
$lower: 255 - $upper;

$inverted-red: 255 - round($lower + $red*($upper - $lower)/255);
$inverted-green: 255 - round($lower + $red*($upper - $lower)/255);
$inverted-blue: 255 - round($lower + $red*($upper - $lower)/255);

$inverted: rgb($inverted-red, $inverted-green, $inverted-blue);

Por lo tanto, tenemos la misma fórmula para los canales invertidos, así que tiene sentido poner esto en una función:

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $red*($upper - $lower)/255);
}

Ahora lo podemos reemplazar con nuestro valor invertido final.

$inverted: rgb(invert-channel($red, $upper, $lower), 
               invert-channel(green, $upper, $lower), 
               invert-channel(blue, $upper, $lower));

Pero, ¿realmente necesitamos exponer todas esas llamadas para invert-channel? Bueno, realmente no, por lo que vamos a crear otra función que maneje el cálculo de los límites y las llamadas a invert-channel:

@function _invert($red, $green, $blue, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel($red, $upper, $lower), 
                invert-channel($green, $upper, $lower), 
                invert-channel($blue, $upper, $lower));
}

Muy bien, pero tal vez no siempre queramos que nuestro fondo original para sea un valor rgb(). Tal vez queramos que sea una palabra clave o un valor hsl(). Por supuesto que podríamos utilizar alguna herramienta para hacer la conversión antes de utilizarlo. Pero , ¿hay alguna manera de conseguir los canales sin importar el formato con el que partimos? Bueno, ahí está! Sass tiene funciones de color rojo, verde y azul precisamente para esto.

Esto significa que podemos simplificar nuestra función _invert:

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel(red($original), $upper, $lower), 
                invert-channel(green($original), $upper, $lower), 
                invert-channel(blue($original), $upper, $lower));
}

Ahora nuestro código original quedaría así:

$original: orange;
$percentage: 65%;

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $red*($upper - $lower)/255);
}

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel(red($original), $upper, $lower), 
                invert-channel(green($original), $upper, $lower), 
                invert-channel(blue($original), $upper, $lower));
}

$inverted: _invert($original, $percentage);

Esto ya tiene mejor pinta. Sin embargo, podemos limpiar el código más a fondo llamando dinámicamente a invert_channel. Esto significa que podemos reescribir nuestra función _invert de esta manera:

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    $inverted-channels: ();

    @each $channel-name in 'red' 'green' 'blue' {
        $channel: call($channel-name, $original);
        $inverted-channel: invert-channel($channel, $upper, $lower);
        $inverted-channels: append($inverted-channels, $inverted-channel);
    }

    @return rgb($inverted-channels...);
}

Esto es mucho más elegante, ya que hemos eliminado todas las repeticiones. Sólo hay una cosa más que necesitamos controlar: la posibilidad de que nuestro valor original sea semitransparente. Esto en realidad es bastante fácil, ya que podemos extraer su canal alfa utilizando la función alpha, y después podemos agregar ese valor a la lista de canales invertidos. De esta manera, nuestro código final será:

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $channel*($upper - $lower)/255);
}

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;
    $alpha: alpha($original);

    $inverted-channels: ();

    @each $channel-name in 'red' 'green' 'blue' {
        $channel: call($channel-name, $original);
        $inverted-channel: invert-channel($channel, $upper, $lower);
        $inverted-channels: append($inverted-channels, $inverted-channel);
    }

    $inverted-channels: append($inverted-channels, $alpha);

    @return rgba($inverted-channels...);
}

$inverted: _invert($original, $percentage);

Lo que mola de la función rgba es que va a retornar un valor RGBA() en el CSS resultante sólo si tu alfa es estrictamente menor que 1.

Puedes ver el funcionamiento y jugar con él en este Codepen:

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP