Alterar a cor do texto com base no brilho da área de fundo coberta?

114

Já pensei no seguinte por um tempo, então agora quero saber suas opiniões, soluções possíveis e assim por diante.

Estou procurando um plugin ou técnica que mude a cor de um texto ou alterne entre imagens / ícones predefinidos dependendo do brilho médio dos pixels cobertos da imagem de fundo ou cor do pai.

Se a área coberta do fundo for bastante escura, deixe o texto branco ou troque os ícones.

Além disso, seria ótimo se o script notasse se o pai não tem cor de fundo definida ou imagem e então continuasse a pesquisar o mais próximo (do elemento pai ao elemento pai ...).

O que você acha, sabe sobre essa ideia? Já existe algo semelhante por aí? exemplos de script?

Saúde, J.

James Cazzetta
fonte
1
Apenas um pensamento em vez de uma resposta. Pode haver uma maneira de definir suas cores usando HSL e, em seguida, observar o valor de luminosidade. Se esse valor estiver acima de um determinado valor, aplique uma regra de css.
Scott Brown
1
você poderia, concebivelmente, analisar a cor de fundo de um elemento em valores R, G, B (e alfa opcional), trabalhando na árvore DOM se o canal alfa estiver definido como zero. No entanto, tentar determinar a cor de uma imagem de fundo é outra questão completamente.
jackwanders de
@Pascal Muito semelhante, e uma boa entrada ... mas não é a resposta exata à minha pergunta.
James Cazzetta

Respostas:

176

Recursos interessantes para isso:

Aqui está o algoritmo W3C (com demonstração JSFiddle também ):

const rgb = [255, 0, 0];

// Randomly change to showcase updates
setInterval(setContrast, 1000);

function setContrast() {
  // Randomly update colours
  rgb[0] = Math.round(Math.random() * 255);
  rgb[1] = Math.round(Math.random() * 255);
  rgb[2] = Math.round(Math.random() * 255);

  // http://www.w3.org/TR/AERT#color-contrast
  const brightness = Math.round(((parseInt(rgb[0]) * 299) +
                      (parseInt(rgb[1]) * 587) +
                      (parseInt(rgb[2]) * 114)) / 1000);
  const textColour = (brightness > 125) ? 'black' : 'white';
  const backgroundColour = 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')';
  $('#bg').css('color', textColour); 
  $('#bg').css('background-color', backgroundColour);
}
#bg {
  width: 200px;
  height: 50px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="bg">Text Example</div>

Alex Ball
fonte
Felix você está certo! estou fora agora, mas com certeza, quando voltar, atualizarei a resposta
Alex Ball
1
Pode ser encurtado para o seguinte, desde que você passe um objeto :::: const setContrast = rgb => (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000> 125? 'preto': 'branco'
Luke Robertson
u rly precisa do jquery apenas para mudar o css?
bluejayke
@bluejayke não, existem outras maneiras ;-)
Alex Ball
63

Este artigo sobre 24 maneiras de calcular o contraste de cores pode ser do seu interesse. Ignore o primeiro conjunto de funções porque elas estão erradas, mas a fórmula YIQ o ajudará a determinar se deve ou não usar uma cor de primeiro plano clara ou escura.

Depois de obter a cor de fundo do elemento (ou ancestral), você pode usar esta função do artigo para determinar uma cor de primeiro plano adequada:

function getContrastYIQ(hexcolor){
    hexcolor = hexcolor.replace("#", "");
    var r = parseInt(hexcolor.substr(0,2),16);
    var g = parseInt(hexcolor.substr(2,2),16);
    var b = parseInt(hexcolor.substr(4,2),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    return (yiq >= 128) ? 'black' : 'white';
}
cyang
fonte
Obrigado, isso é realmente útil .. Isso depende da cor de fundo definida .. Mas você sabe como obter a cor média de uma imagem passando por cada pixel (como em um loop)?
James Cazzetta
4
Em es6, você pode fazer isso com:const getContrastYIQ = hc => { const [r, g, b] = [0, 2, 4].map( p => parseInt( hc.substr( p, 2 ), 16 ) ); return ((r * 299) + (g * 587) + (b * 114)) / 1000 >= 128; }
Centril
Peguei essa função e a expandi um pouco para que você pudesse retornar duas cores personalizadas, em vez de sempre preto e branco. Observe que se as cores forem duas próximas, você ainda pode ter problemas de contraste, mas esta é uma boa alternativa para retornar cores absolutas jsfiddle.net/1905occv/1
Hanna
2
este é legal, eu ajustaria o yiq para> = 160, funcionou melhor para mim.
Arturo
15

Pergunta interessante. Meu pensamento imediato foi inverter a cor do fundo como o texto. Isso envolve simplesmente analisar o plano de fundo e inverter seu valor RGB.

Algo assim: http://jsfiddle.net/2VTnZ/2/

var rgb = $('#test').css('backgroundColor');
var colors = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
var brightness = 1;

var r = colors[1];
var g = colors[2];
var b = colors[3];

var ir = Math.floor((255-r)*brightness);
var ig = Math.floor((255-g)*brightness);
var ib = Math.floor((255-b)*brightness);

$('#test').css('color', 'rgb('+ir+','+ig+','+ib+')');
jeremyharris
fonte
Você provavelmente deseja remover a saturação de sua cor 'invertida' calculando a média dos valores invertidos de R, G, B e definindo-os iguais entre si. No entanto, essa solução está obtendo sua cor base de uma string, e não da propriedade CSS do elemento. Para ser confiável, a solução teria que obter cores de fundo dinamicamente, que normalmente retornam valores rgb () ou rgba (), mas podem variar de acordo com o navegador.
jackwanders
Sim. Para facilitar a análise, usei apenas um valor hexadecimal. Eu atualizei o violino para incluir a captura da cor do elemento do CSS. Eu atualizei o violino e incluí uma espécie de controle de brilho (não sei nada sobre matemática de cores, então provavelmente não é o brilho verdadeiro).
jeremyharris
1
Que tal agora? stackoverflow.com/questions/2541481/…
jeremyharris
2
E se a cor de fundo for #808080!?
Nathan MacInnes
1
@NathanMacInnes ainda vai invertê-lo, simplesmente acontece que inverter algo bem no meio do espectro vai resultar em si mesmo. Este código apenas inverte a cor, o que vem com suas limitações.
jeremyharris
9

mix-blend-mode faz o truque:

header {
  overflow: hidden;
  height: 100vh;
  background: url(https://www.w3schools.com/html/pic_mountain.jpg) 50%/cover;
}

h2 {
  color: white;
  font: 900 35vmin/50vh arial;
  text-align: center;
  mix-blend-mode: difference;
  filter: drop-shadow(0.05em 0.05em orange);
}
<header>
  <h2 contentEditable role='textbox' aria-multiline='true' >Edit me here</h2>
</header>

Adição (março de 2018): A seguir, um bom tutorial explicando todos os diferentes tipos de modos / implementações: https://css-tricks.com/css-techniques-and-effects-for-knockout-text/

James Cazzetta
fonte
5

Achei o script BackgroundCheck muito útil.

Ele detecta o brilho geral do fundo (seja uma imagem de fundo ou uma cor) e aplica uma classe ao elemento de texto atribuído ( background--lightou background--dark), dependendo do brilho do fundo.

Pode ser aplicado a elementos estáticos e em movimento.

( Fonte )

cptstarling
fonte
Obrigado pela contribuição!
James Cazzetta,
Isso funciona para cores de fundo? Li rapidamente o script e não consigo vê-lo utilizando a cor de fundo para verificar o brilho. Apenas imagens.
Jørgen Skår Fischer
1
Olá Jørgen, acho que o script colourBrightness pode servir ao seu propósito: github.com/jamiebrittain/colourBrightness.js
cptstarling
5

Se você estiver usando ES6, converta hex para RGB e use o seguinte:

const hexToRgb = hex => {
    // turn hex val to RGB
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
    return result
        ? {
              r: parseInt(result[1], 16),
              g: parseInt(result[2], 16),
              b: parseInt(result[3], 16)
          }
        : null
}

// calc to work out if it will match on black or white better
const setContrast = rgb =>
    (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000 > 125 ? 'black' : 'white'

const getCorrectColor = setContrast(hexToRgb(#ffffff))
Luke Robertson
fonte
4

Aqui está minha tentativa:

(function ($) {
    $.fn.contrastingText = function () {
        var el = this,
            transparent;
        transparent = function (c) {
            var m = c.match(/[0-9]+/g);
            if (m !== null) {
                return !!m[3];
            }
            else return false;
        };
        while (transparent(el.css('background-color'))) {
            el = el.parent();
        }
        var parts = el.css('background-color').match(/[0-9]+/g);
        this.lightBackground = !!Math.round(
            (
                parseInt(parts[0], 10) + // red
                parseInt(parts[1], 10) + // green
                parseInt(parts[2], 10) // blue
            ) / 765 // 255 * 3, so that we avg, then normalize to 1
        );
        if (this.lightBackground) {
            this.css('color', 'black');
        } else {
            this.css('color', 'white');
        }
        return this;
    };
}(jQuery));

Então, para usá-lo:

var t = $('#my-el');
t.contrastingText();

Isso tornará o texto imediatamente preto ou branco, conforme apropriado. Para fazer os ícones:

if (t.lightBackground) {
    iconSuffix = 'black';
} else {
    iconSuffix = 'white';
}

Então, cada ícone pode ser semelhante a 'save' + iconSuffix + '.jpg'.

Observe que isso não funcionará quando algum contêiner estourar seu pai (por exemplo, se a altura do CSS for 0 e o estouro não estiver oculto). Fazer isso funcionar seria muito mais complexo.

Nathan MacInnes
fonte
1

Ao combinar as respostas [@ alex-ball, @jeremyharris], descobri que esta é a melhor maneira para mim:

        $('.elzahaby-bg').each(function () {
            var rgb = $(this).css('backgroundColor');
            var colors = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);

            var r = colors[1];
            var g = colors[2];
            var b = colors[3];

            var o = Math.round(((parseInt(r) * 299) + (parseInt(g) * 587) + (parseInt(b) * 114)) /1000);

            if(o > 125) {
                $(this).css('color', 'black');
            }else{
                $(this).css('color', 'white');
            }
        });
*{
    padding: 9px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
<div class='elzahaby-bg' style='background-color:#000'>color is white</div>

<div class='elzahaby-bg' style='background-color:#fff'>color is black</div>
<div class='elzahaby-bg' style='background-color:yellow'>color is black</div>
<div class='elzahaby-bg' style='background-color:red'>color is white</div>

A. El-zahaby
fonte