quinta-feira, 14 de janeiro de 2016

Gerando cores aleatórias com Java

Bem-vindos ao blog Preciso Estudar Sempre. Meu nome é João Paulo Maida e minha paixão é estudar.

Como você faria para gerar cores aleatoriamente em Java as quais, seguem as características descritas abaixo ?

  1. Devem ser aleatórias (não podem ser fixas/hard-coded)
  2. Devem estar no formato hexadecimal para HTML
  3. Não podem ser muito claras
  4. Não podem ser muito escuras
  5. Não podem ser preto nem branco
  6. Não podem ser parecidas
  7. Não podem haver cores repetidas
Se você tá com a famosa pressinha (mi mi mi) e não pode esperar, clique aqui ou aqui, baixe o projeto e depois volte aqui para ler tudo. :)

Tá .... vamos começar pelo começo !!

Para que você tenha um total entendimento do assunto abordado aqui, é necessário conhecimento em Java e OO. Caso você não tenha, recomendo algumas aulinhas antes.

Nossa primeira exigência é que as cores devem ser aleatórias. Contudo, como gerar cores através de Java ? Bem, sabemos que o padrão de cores utilizado por componentes eletrônicos é o RGB. O modelo de cores RGB é baseado na teoria de visão colorida tricromática, de Young-Helmholtz, e no triângulo de cores de Hardwell. O uso do modelo RGB como padrão para apresentação de cores na Internet tem suas raízes nos padrões de cores de televisões RCA de 1953 e no uso do padrão RGB nas câmeras Land/Polaroid, pós Edwin Land [Wikipedia]. Este padrão é dividido em três cores, sendo elas:
  • Red = vermelho
  • Green = verde
  • Blue = azul
Uma cor no modelo de cores RGB pode ser descrita pela indicação da quantidade de vermelho, verde e azul que contém. Cada uma pode variar entre o mínimo (completamente escuro - 0) e máximo (completamente intenso - 255). Quando todas as cores estão no mínimo, o resultado é preto. Se todas estão no máximo, o resultado é branco [Wikipedia].

Uma das representações mais usuais para as cores é a utilização da escala de 0 à 255, bastante encontrada na computação pela conveniência de se guardar cada valor de cor em 1 byte (8 bits) [Wikipedia].

Clique aqui e se diverta fazendo suas combinações de cores. Algumas sugestões para você:
  • Branco - RGB (255,255,255);
  • Azul - RGB (0,0,255);
  • Vermelho - RGB (255,0,0);
  • Verde - RGB (0,255,0);
  • Amarelo - RGB (255,255,0);
  • Magenta - RGB (255,0,255);
  • Ciano - RGB (0,255,255);
  • Preto - RGB (0,0,0).

Mãos a massa !!

Método gerarCorAleatoriamente()

1:  /**  
2:   * Método responsável pela geração de cores em hexadecimal  
3:   * @return A cor  
4:   */  
5:  private Color gerarCorAleatoriamente(){  
6:       Random randColor = new Random();   
7:       int r = randColor.nextInt(256);  
8:       int g = randColor.nextInt(256);  
9:       int b = randColor.nextInt(256);  
10:       return new Color(r, g, b);  
11:  }  

Esse método é responsável pela geração de cores aleatórias. Para realizar tal, utiliza o método nextInt(int) o qual, recebe um argumento inteiro que representa o limite do número randômico. O número que será gerado estará no limite 0 - 255. Através da geração aleatória dos números, são definidos as quantidades de R, G e B da cor. A classe Color é utilizada somente para fins de representação.

Método gerarCorHexadecimal(color) e tratarHexString(string)

1:  /**  
2:   * Método que exporta a cor para o formato hexadecimal.   
3:   * @param color Representa a cor.  
4:   * @return Retorna a cor no formato hexadecimal.  
5:   */  
6:  private String gerarCorHexadecimal(Color color){  
7:       return '#'+  
8:            this.tratarHexString(Integer.toHexString(color.getRed()))+  
9:            this.tratarHexString(Integer.toHexString(color.getGreen()))+  
10:            this.tratarHexString(Integer.toHexString(color.getBlue()));  
11:  }  

Este método trabalha em conjunto com o método tratarHexString(string).

1:  /**  
2:   * Método responsável por tratar a string hexadecimal. Caso a string possua tamanho 1, o método adiciona o número 0 na frente da string.  
3:   * @param hexString A string hexadecimal  
4:   * @return A string hexadecimal  
5:   */  
6:  private String tratarHexString(String hexString){  
7:       String hex = null;  
8:       if(hexString.length() == 1){  
9:            hex = '0'+hexString;  
10:       }else{  
11:            hex = hexString;  
12:       }  
13:       return hex;  
14:  }  

Os métodos acima são fácies de entender. O primeiro obtém as quantidades de Rda cor, gerada aleatoriamente na seção acima, e transforma-as em hexadecimal. Contudo, um tratamento precisa ser feito o qual, é realizado no segundo método.

Quando uma quantidade tem seu correspondente hexadecimal gerado somente com um único caractere, torna problemática a exibição de cores pois, o formato de cores em hexadecimal espera três pares de RB. Logo, isso #001a0f é diferente disso #001af. Então, é adicionado o caractere '0' à esquerda da string (linha 9) e o problema é solucionado.

Método isBrilhoCorreto(float) e isSaturacaoCorreta(float)

1:  /**  
2:   * Método que trata o brilho de uma cor.   
3:   * Para que uma cor seja escolhida para o gráfico seu nível de brilho deve estar acima ou igual de 0.5 e abaixo ou igual de 0.9.  
4:   * @param brightness Representa o brilho.  
5:   * @return Retorna true caso a cor esteja de acordo com o ponto necessário (brilho), false caso contrário.  
6:   */  
7:  private boolean isBrilhoCorreto(float brightness){  
8:       if (brightness >= 0.5 && brightness <= 0.9) {  
9:            return true;  
10:       }  
11:       return false;  
12:  }  
13:    
14:  /**  
15:   * Método que trata a saturação de uma cor.   
16:   * Para que uma cor seja escolhida para o gráfico seu nível de saturação deve estar acima ou igual de 0.7.  
17:   * @param saturation Representa a saturação  
18:   * @return Retorna true caso a cor esteja de acordo com o ponto necessário (saturação), false caso contrário.  
19:   */  
20:  private boolean isSaturacaoCorreta(float saturation){  
21:       if (saturation >= 0.7){  
22:            return true;  
23:       }  
24:       return false;  
25:  }  

Para que possamos gerar cores que não sejam muito claras nem muito escuras precisamos controlar seus níveis de brilho (brightness) e saturação (saturation). Como parâmetros para o controle, adotaremos que nossas cores ideais tenham o nível de saturação maior ou igual a 0.7 e que possuam brilho entre 0.5, inclusive, e 0.9, inclusive.

A saturação é um parâmetro que especifica a qualidade de um matiz de cor pelo grau de mesclagem do matiz com a cor branca (padrão HSB) ou, com a cor cinza média (padrão HSL). Então, quanto menos cinza ou branco na cor, mais saturada ela é [Wikipedia]. Entenda por matiz, a cor pura.

Método calcularDistanciaDeCores(color, color), isDistanciaAceitavel(color, color) e isCorParecidaComCorPermitida(list, color)

1:  /**  
2:   * Método que calcula a distância entre cores.  
3:   * @param cor1 Representa uma cor.  
4:   * @param cor2 Representa uma cor.  
5:   * @return Retorna a distância.  
6:   */  
7:  private double calcularDistanciaDeCores(Color cor1, Color cor2){  
8:       long meanRed = (cor1.getRed() + cor2.getRed())/2;  
9:       long deltaRed = cor1.getRed() - cor2.getRed();  
10:       long deltaGreen = cor1.getGreen() - cor2.getGreen();  
11:       long deltaBlue = cor1.getBlue() - cor2.getBlue();  
12:       return Math.sqrt((2+meanRed/256)*Math.pow(deltaRed, 2)+4*Math.pow(deltaGreen, 2)+(2+(255-meanRed)/256)*Math.pow(deltaBlue, 2));  
13:  }  
14:    
15:  /**  
16:   * Método que avalia se a distância entre duas é aceitável. Uma distância é dita aceita se é menor que 200.   
17:   * @param cor1 Representa uma cor.  
18:   * @param cor2 Representa uma cor.   
19:   * @return boolean Retorna true caso a distância seja menor que 200, false caso contrário.  
20:   */  
21:  private boolean isDistanciaAceitavel(Color cor1, Color cor2){  
22:       if (this.calcularDistanciaDeCores(cor1, cor2) < 200) {  
23:            return false;  
24:       }  
25:       return true;  
26:  }  
27:    
28:  /**  
29:   * Método que valida se uma cor gerada aleatoriamente possui uma distância aceitável com as cores previamente permitidas.   
30:   * @param coresPermitidas Representa as cores permitidas.  
31:   * @param corAleatoria Representa a cor gerada aleatoriamente.  
32:   * @return Retorna true caso a cor seja parecida, retorna false caso contrário.  
33:   */  
34:  private boolean isCorParecidaComCorPermitida(List<Color> coresPermitidas, Color corAleatoria){  
35:       boolean isCorParecida = false;  
36:       for (Color corPermitida : coresPermitidas) {  
37:            if(!this.isDistanciaAceitavel(corPermitida, corAleatoria)){  
38:                 isCorParecida = true;  
39:                 break;  
40:            }  
41:       }  
42:       return isCorParecida;  
43:  }  

Gerar cores aleatoriamente tem um sério problema, a geração de cores parecidas. A cor azul e azul' não são iguais, mas absurdamente parecidas. Então, como saber se um cor é parecida com a outra? Comparar os níveis de Rda cor não é boa idéia pois, mesmo que as cores sejam muito parecidas, seus níveis são extremamente diferentes. Logo, não temos como estabelecer um parâmetro e voltamos a pergunta. Como identificar uma cor parecida ?

Através de pesquisa, encontrei uma forma para resolver esse problema. Precisamos calcular a distância euclidiana entre as cores, ou seja, a distância entre dois pontos. O cálculo é realizado através do método calcularDistanciaDeCores(color, color) o qual, recebe duas cores como parâmetros. O valor retornado é a distância.

Agora, precisamos estabelecer o que é uma distância aceitável. Através de testes, notei que cores com a distância maior que 200 mantinham uma boa diferença em sua percepção. Então, criei o método isDistanciaAceitavel(color, color). Esse método retorna true, caso as cores estejam em uma distância aceitável ( > 200), caso contrário retorna false. Logo, o problema citado anteriormente foi resolvido. Não precisamos mais nos preocupar com azul e azul'.

A terceira etapa para encerrar esse problema é comparar a cor gerada com as cores já previamente escolhidas. Logo, o método isCorParecidaComCorPermitida(list, color) foi criado. Este método realiza essa comparação utilizando o método citado no parágrafo acima. A lista recebida como parâmetro contém as cores previamente escolhidas e o segundo parâmetro representa a cor gerada recentemente. Caso a cor não seja "parecida" com nenhuma das outras cores já escolhidas, o método retorna true, caso contrário, false.

Método carregarCoresProibidasDefault(list)

1:  /**  
2:   * Método que realiza a carga inicial de cores proibidas.  
3:   * @param coresProibidas Representa a lista de cores proibidas.  
4:   */  
5:  private void carregarCoresProibidasDefault(List<Color> coresProibidas){  
6:       coresProibidas.add(new Color(0,0,0)); //preto  
7:       coresProibidas.add(new Color(255,255,255)); //branco  
8:  }  

Este método carrega cores proibidas as quais são default (padrão). No nosso escopo, são cores proibidas por padrão, a cor preta e branca. Contudo, caso no seu escopo, a cor marrom ou azul-petróleo forem proibidas por padrão, você pode alterar o método e verificação de cores proibidas levará em conta essas, previamente adicionadas.

É possível notar que o método recebe um parâmetro do tipo List. Esse parâmetro é passado para que a lista de cores seja alimentada com as configurações iniciais e posteriormente com as cores proibidas geradas aleatoriamente.

Método gerarCores(int)

1:  /**  
2:   * Método responsável por gerar cores aleatoriamente no formato RGB seguindo os padrões de qualidade especificados.  
3:   * @param qtdDeCores Representa a quantidade de cores que deseja-se gerar.  
4:   * @return List<Color> Representa a lista de cores permitidas em formato RGB.  
5:   */  
6:  public List<Color> gerarCores(int qtdDeCores){  
7:       List<Color> coresPermitidas = new ArrayList<Color>();            
8:       List<Color> coresProibidas = new ArrayList<Color>();  
9:       this.carregarCoresProibidasDefault(coresProibidas);  
10:         
11:       Color corAleatoria = null;  
12:       boolean isCorProibida = false;  
13:       for(int i=0; i<qtdDeCores; i++){  
14:            while(true){  
15:                 corAleatoria = this.gerarCorAleatoriamente();  
16:                 float[] hsb = Color.RGBtoHSB(corAleatoria.getRed(), corAleatoria.getGreen(), corAleatoria.getBlue(), null);  
17:                 float saturation = hsb[1];  
18:                 float brightness = hsb[2];  
19:                 for(Color corProibida : coresProibidas){  
20:                      isCorProibida = corProibida.equals(corAleatoria)   
21:                                          || !this.isBrilhoCorreto(brightness)   
22:                                          || !this.isSaturacaoCorreta(saturation)  
23:                                          || !this.isDistanciaAceitavel(corAleatoria, corProibida)   
24:                                          || this.isCorParecidaComCorPermitida(coresPermitidas, corAleatoria);  
25:                      if(isCorProibida){  
26:                           coresProibidas.add(corProibida);  
27:                           break;  
28:                      }  
29:                 }  
30:                 if(isCorProibida){  
31:                      isCorProibida = false;  
32:                      continue;  
33:                 }  
34:                 break;  
35:            }  
36:            coresProibidas.clear();  
37:            coresPermitidas.add(corAleatoria);  
38:       }  
39:                   
40:       return coresPermitidas;  
41:  }  

Esse método é o grande controlador de toda a inteligência da geração de cores. É ele quem aplica todas as regras que especificamos acima mas, existem alguns pontos os quais, merecem uma atenção maior. Na linha 16, é possível notar uma transformação de RGB para HSB (HueSaturationBrightness). Essa transformação é necessária para que, possamos acessar essas propriedades das cores.

O loop infinito é necessário para que seja gerada uma cor aleatoriamente até que ela seja aceita segundo nossas regras de qualidade. Como não sabemos quando essa suposta cor será gerada, então é necessário um loop infinito acompanhada de uma condicional com break.

A limpeza da lista de cores proibidas é necessária pois seu tamanho cresce absurdamente. Caso esse tratamento não fosse realizado uma OutOfMemoryError aconteceria e aplicação seria encerrada.

Pronto !! Terminamos !!



Abaixo você pode encontrar as referências usadas e um link para um site muito legal sobre cores, vale a pena dar uma conferida.

Caso você faça o download do projeto, encontrará a classe que desenvolvemos aqui e um Jar. O arquivo Jar contém nossa classe compilada para que, você possa adicioná-la ao seu projeto e gerar suas cores.

Baixe o projeto
Dropbox: https://www.dropbox.com/s/dc4b021zse7akfp/GeradorCoresAleatorias.rar?dl=0
GitHub: https://github.com/PrecisoEstudarSempre/GeradorCoresAleatorias.git

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários, na nossa página do facebook ou mande um email.

Facebook: https://www.facebook.com/precisoestudarsempre/
E-mail: precisoestudarsempre@gmail.com

Referências:
Colour metric - http://www.compuphase.com/cmetric.htm
RGB - https://pt.wikipedia.org/wiki/RGB
HTML Color Picker - http://www.w3schools.com/tags/ref_colorpicker.asp
Saturação - https://pt.wikipedia.org/wiki/Satura%C3%A7%C3%A3o
Matiz (cor) - https://pt.wikipedia.org/wiki/Matiz_(cor)
Color Hex Color Codes - http://www.color-hex.com/

3 comentários:

Unknown disse...

Cara, maravilhoso. Precisava exatamente deste algoritmo para gerar cores automaticamente para gráficos. Parabéns pelo trabalho!

Preciso estudar sempre disse...

Muito obrigado Matheus Sousa. Agradeço a consideração e carinho pelo trabalho. Fique a vontade para conhecer o blog. Você sempre será bem-vindo.

Unknown disse...

Material excelente, além de gerar cores aleatórias...estou precisando gerar cores através de combinações pré-definidas...tipo cor x combinada com a cor y gera a cor z....