quinta-feira, 28 de junho de 2012

Formas, Transformações e Efeitos em JavaFX 2.1

Brincando com JavaFX, versão 2.1, desenvolvi um jogo de memória para exercitar diversos conceitos do framework. Neste artigo, não descrevo exatamente o desenvolvimento do jogo, mas simplesmente a criação da forma básica, a aplicação de transformações nesta forma para se obter o resultado final e ainda a aplicação de efeito de iluminação para criar a ilusão de volume.

Somente para contextualizar,  jogo em questão é uma tentativa de reproduzir (aqui neste artigo, visualmente pelo menos) aquele jogo eletrônico chamado de Genius, famoso na década de 80. Veja na Wikipedia mais sobre esse jogo. Ainda, em vários outros sites é possível encontrar várias versões online para jogar.

Formas


O jogo tem a forma de uma rosquinha de quarto partes coloridas. Cada parte vou chamar de um quarto de círculo.

Produzir a forma da figura ao lado no JavaFX é muito simples: basta criar dois círculos com raios diferentes e integrar estas duas formas para produzir a forma de uma rosquinha. A partir desta forma, agora, deve-se executar a intersecção com um retângulo cujos lados devem ter valor igual ao maior raio dos círculos criados anteriormente (então estamos falando de um quadrado, na verdade).

O código abaixo traduz tudo o que foi escrito acima. Ler o código é mais fácil que ler qualquer outra explicação.
Circle c1 = new Circle(100);
Circle c2 = new Circle(50);
Shape donut = Shape.subtract(c1, c2);
Rectangle r1 = new Rectangle(100, 100);
Shape quarter = Shape.intersect(donut, r1);

Efeito de Iluminação


Adicionar uma cor à forma criada não bastaria para deixá-la mais interessante visualmente. Além da cor, um efeito de luz é necessário para dar à forma um efeito tridimensional, como mostra a figura ao lado.

Em JavaFX, Light representa um tipo/fonte de luz enquanto Lighting representa o efeito desta luz sobre uma forma.

Abaixo, segue o código, com comentários,  para produzir o efeito apresentadona figura ao lado.




// Cria-se uma luz
Light.Distant light = new Light.Distant();
light.setAzimuth(-135.0);
light.setElevation(60);
// Com esta fonte de luz, cria-se a iluminação
Lighting lighting = new Lighting(light);
lighting.setSurfaceScale(2.0);
// A cor e iluminação é aribuída à forma
quarter.setFill(Color.BLUE);        
quarter.setEffect(lighting);

Rotação & Translação


Para completar o círculo, primeiramente deve-se criar 4 vezes a forma de um quarto de círculo, atribuir uma cor distinta e o mesmo efeito de luz e iluminação às formas. Depois, é necessário rotacionar as formas para obter cada quarto de círculo na devida posição para completar o círculo, ou seja, nas posições noroeste, nordeste, sudoeste e sudeste. Como a primeira forma já ocupa a posição sudeste, para obter as outras posições deve-se rotacionar as demais formas em 90, 180 e 270.

A rotação é suficiente para montar o círculo desejado para o jogo, mas sua exibição ainda não é adequada pois não está centralizada. Para conseguir este resultado, cada forma tem que ser transladada em +/- 50 pixels, de acordo com sua rotação.

O código abaixo mostra o que foi descrito acima. Vale lembrar que as variáveis donut, r1 e lighting já foram instanciadas e descritas anteriormente.

// Quatro formas iguais
Shape quarter1 = Shape.intersect(donut, r1);
Shape quarter2 = Shape.intersect(donut, r1);
Shape quarter3 = Shape.intersect(donut, r1);
Shape quarter4 = Shape.intersect(donut, r1);        
// Posição NOROESTE
quarter1.setEffect(lighting);
quarter1.setFill(Color.BLUE);
quarter1.getTransforms().add(new Rotate(180));
quarter1.getTransforms().add(new Translate(-50,-50));
// Posição NORDESTE
quarter2.setEffect(lighting);
quarter2.setFill(Color.RED);
quarter2.getTransforms().add(new Rotate(270));
quarter2.getTransforms().add(new Translate(-50,50));
// Posição SUDESTE - não precisa rotacionar
quarter3.setEffect(lighting);
quarter3.setFill(Color.YELLOW);
quarter3.getTransforms().add(new Translate(50,50));
// Posição SUDOESTE
quarter4.setEffect(lighting);
quarter4.setFill(Color.GREEN);
quarter4.getTransforms().add(new Rotate(90));
quarter4.getTransforms().add(new Translate(50,-50));

O layout StackPane é bem adequado para exibir as formas criadas, pois exibe cada uma sobreposta e, como a rotação já foi realizada, elas serão serão adequadamente sobrepostas e formarão o círculo esperado.

public void start(Stage primaryStage) {
    /**
     * Todo código anterior ...
     */
    StackPane root = new StackPane();
    root.getChildren().add(quarter1);
    root.getChildren().add(quarter2);
    root.getChildren().add(quarter3);
    root.getChildren().add(quarter4);
    primaryStage.setScene(new Scene(root, 300, 250));
    primaryStage.show();
}

Ao lado, segue a imagem com o resultado final: um círculo formado de quatro partes com um certo volume aparentando uma forma tridimensional.

Ainda falta criar muita coisa para um jogo, mas  o mais importante é também permitir a interação do usuário com cada forma separadamente. Em outras palavras, cada quarto do círculo precisa estar pronto para receber eventos de mouse e executar uma animação. Esta animação viria a ser a sensação de acender rapidamente e apagar mais suavemente cada quarto de círculo.





Interação com Usuário


Cada quarto de círculo de capturar eventos de mouse quando o usuário clicar sobre a forma. Para tanto, precisamos definir uma classe que implemente a interface EventHandler<T extends Event>. note que esta interface pede a definição de um tipo genérico, que neste caso deve ser qualquer classe que implemente a interface Event. Como a intenção é capturar eventos de mouse, então esta classe é a MouseEvent. O código abaixo demonstra a criação e instanciação de uma classe anônima que implementa a interface requerida para a captura de eventos de mouse.

EventHandler<MouseEvent> mouseEvent = new EventHandler<MouseEvent>(){

    @Override
    public void handle(MouseEvent arg0) 
        Paint color = ((Shape)arg0.getSource()).getFill();
        System.out.println("cor: " + color.toString());
    }
};

Observe que, uma vez o evento capturado, a origem do evento é recuperada e, a partir de um conversão explícita para Shape, pega-se a cor da forma. Então é gerada uma saída no console com o valor, em hexadecimal, da cor.

Até aqui, simplesmente foi definida e instanciada uma classe anônima para tratar eventos de mouse. Agora, precisa fazer com que cada forma trate o evento de clique de mouse com esta instância. O código abaixo mostra o que deve ser feito:

quarter1.setOnMouseClicked(mouseEvent);
quarter2.setOnMouseClicked(mouseEvent);
quarter3.setOnMouseClicked(mouseEvent);
quarter4.setOnMouseClicked(mouseEvent);

Neste exemplo, pela interface gráfica o usuário não tem ainda nenhuma percepção que o quarto de círculo recebeu o evento do mouse. O ideal é que seja executada uma animação.

Animação & Mídia


A animação deve ser aquela que dê a sensação para o usuário de que a forma clicada acenda repentinamente e apague gradualmente durante um certo período e tempo. Ainda, a execução de áudio durante a animação torna a interação com o usuário mais interessante.

Veja aqui, neste meu outro artigo chamado "JavaFX Animation and Media Synchronization", como criar uma animação simples e fazer a sincronização com o áudio.

Nenhum comentário:

Postar um comentário