Havia algo que me incomodava depois de ler o artigo de David sobre o invert filtro na semana passada. Era esta frase:

Os valores informados pelo window.getComputedStyle(el) serão os valores CSS originais, entretanto, de modo que não há como obter os valores invertidos verdadeiros de determinadas propriedades.

Mas se eu puder ler o valor original para background-color ou color e posso ler o valor para o filter, então posso computar o valor invertido, o que significa que há uma maneira, afinal.

Bem, vamos ver como fazemos isso.

Inversão total para um fundo sólido

Antes de mais nada, precisamos entender como o invert e começaremos com o caso específico da inversão total – o invert(100%). Para um valor original arbitrário rgb(red, green, blue), onde red, green e blue podem assumir valores entre 0 e 255 aplicando um invert(100%) vai produzir rgb(255 - red, 255 - green, 255 - blue).

Isso é bastante direto e fácil de codificar, especialmente porque os valores obtidos por meio do window.getComputedStyle(el) são rgb() ou rgba() em todos os navegadores que suportam filtros CSS, dependendo do fato de o valor inicial ter um canal alfa menor que 1 ou não.

Observação: Filtros CSS com valores de função de filtro (não apenas url() ) são compatíveis com o Chrome 18+, Safari 6.1+, Opera 15+ e Firefox 34+. O Chrome, o Safari e o Opera precisam do parâmetro -webkit- enquanto o Firefox não precisa e nunca precisou de um prefixo, mas precisa do layout.css.filters.enabled definido como true em about:config. No entanto, o IE ainda não oferece suporte a filtros CSS eles estão listados como “Under Consideration” (em consideração). Se forem implementadas, provavelmente também não precisarão de um prefixo. Portanto, não use -moz- ou -ms- prefixos para filtros!

Portanto, se tivermos um elemento para o qual definimos um background-color e depois aplicamos um invert(100%) sobre ele:

.box {
    background-color: darkorange;
    -webkit-filter: invert(100%);
    filter: invert(100%);
}


Em seguida, podemos ler seus estilos e calcular o valor invertido a partir do original:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    original = styles.backgroundColor, 
    channels = original.match(/\d+/g), 
    inverted_channels = channels.map(function(ch) {
        return 255 - ch;
    }), 
    inverted = 'rgb(' + inverted_channels.join(', ') + ')';


Ele pode ser visto funcionando em esta caneta, onde a primeira caixa tem o original background-color e nenhum filtro aplicado, e a segunda, o mesmo background-color e um invert(100%) enquanto o terceiro tem um fundo que é o valor invertido (conforme computado pelo código JavaScript acima) do filtro original background-color, assumindo um invert(100%) filtro.


Veja a caneta Obtendo o valor invertido para `background-color` via JS #1 por Ana Tudor (@thebabydino) em CodePen.



E quanto aos planos de fundo semitransparentes?

Mas isso é apenas para rgb() e se o nosso background-color não for totalmente opaco, então o valor que lemos com JavaScript será um rgba() valor.

Por exemplo, o backgroundColor lido via JavaScript será "rgb(255, 140, 0)" se, no CSS, definirmos background-color para qualquer uma das seguintes opções:


  • darkorange
  • #ff8c00
  • rgb(255, 140, 0)
  • rgb(100%, 54.9%, 0%)
  • rgba(255, 140, 0, 1)
  • rgba(100%, 54.9%, 0%, 1)
  • hsl(33, 100%, 50%)
  • hsla(33, 100%, 50%, 1)


Mas vai ser "rgba(255, 140, 0, .65)" se definirmos background-color para um dos seguintes valores:


  • rgba(255, 140, 0, .65)
  • rgba(100%, 54.9%, 0%, .65)
  • hsla(33, 100%, 50%, .65)


Desde que o invert deixa o filtro alpha inalterado e modifica apenas o canal red, green e blue isso significa que nosso código JavaScript se torna:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    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 - ch; }), 
    inverted;

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

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


Ele pode ser visto funcionando em esta caneta.



Veja a caneta Obtendo o valor invertido para `background-color` via JS #2 por Ana Tudor (@thebabydino) em CodePen.




Observe que isso pressupõe que o valor do canal alfa do plano de fundo que definimos no CSS é sempre maior que 0. Se o valor do canal alfa for 0, então o valor retornado por styles.backgroundColor será "transparent" e a inversão realmente não tem mais sentido.

Tudo bem, mas isso foi tudo para um filter valor de invert(100%). O que acontece se quisermos ter um valor menor que 100%?

Caso geral

Bem, para um valor menor que 100%, digamos 65%, o [0, 255] para cada um dos red, green e blue é primeiro esmagado em torno de seu valor central. Isso significa que o limite superior do intervalo vai de 255 para 65% do 255 e seu limite inferior vai de 0 para 100% - 65% = 35% do 255. Em seguida, calculamos o equivalente esmagado do nosso original background-color o que significa simplesmente pegar o valor de cada canal, dimensioná-lo para o novo intervalo reduzido e, em seguida, adicionar o limite inferior. E, finalmente, a última etapa é obter o valor simétrico com relação ao valor central para cada um dos red, green e blue do equivalente reduzido do nosso original background-color.

O seguinte demonstração mostra visualmente tudo isso acontecendo, usando o green como exemplo.


Veja a caneta Esmagando uma faixa de canais por Ana Tudor (@thebabydino) em CodePen.




A faixa original vai de 0 para 255 e o valor original para o green é considerado como sendo 165. Qualquer valor menor que 100% para o argumento do invert reduz o intervalo. Um valor de 50% significa que o intervalo é reduzido a nada, pois o limite superior é igual ao inferior. Isso também significa que o valor final invertido do rgb() final invertido é sempre rgb(128, 128, 128)independentemente do valor inicial do background-color possa ser.

O equivalente esmagado do valor original para o green é aquele que tem um s na frente de seu rótulo. O valor invertido desse equivalente esmagado em relação ao meio do intervalo tem um i na frente de seu rótulo. E podemos ver que esse é o valor do green para o fundo sólido da segunda caixa na parte inferior depois de aplicar um invert sobre ela.

Isso significa que nosso código JavaScript agora se torna:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    invert_perc = (styles.webkitFilter || styles.filter).match(/\d+/), 
    upper = invert_perc/100*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(', ') + ')';


No entanto, há um problema: isso pressupõe que o valor de estilo computado para a propriedade filter seja algo como "invert(65%)". Mas, na maioria das vezes, esse não é o caso. Se definirmos o valor do filtro como invert(65%) no CSS, então o valor que obtemos dessa forma via JavaScript é "invert(0.65)" nos navegadores WebKit e "invert(65%)" no Firefox. Se definirmos o valor do filtro como filter(.65) no CSS, o valor que obtemos via JavaScript é "invert(0.65)". O Firefox o retorna no mesmo formato em que o definimos no CSS, enquanto os navegadores WebKit sempre o retornam como um valor entre 0 e 1. Portanto, precisamos lidar com isso também:

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(', ') + ')';


Como pode ser visto em esta canetaagora funciona corretamente para ambos os formatos, desde que o alpha não seja o canal 0.


Veja a caneta Obtendo o valor invertido para `background-color` via JS #3 por Ana Tudor (@thebabydino) em CodePen.



Palavras finais

Isso ainda lida apenas com o caso muito simples em que o valor do filter é apenas um invert função. Ela não funcionará quando tivermos funções de filtro encadeadas, por exemplo, algo como filter: hue-rotate(90deg) invert(73%).

Ana Tudor

Sobre Ana Tudor

Adora matemática, especialmente geometria. Gosta de brincar com códigos. Apaixonado por fazer experimentos e aprender coisas novas. Fascinado por astrofísica e ciência em geral. Grande fã do avanço tecnológico e de suas aplicações em todos os campos. Demonstra interesse por esportes motorizados, desenho, desenhos animados clássicos, música rock, brinquedos carinhosos e animais com garras afiadas e dentes grandes. Sonha em ter um tigre de verdade.