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.

JavaFX Animation and Media Synchronization

I've been experiencing JavaFX 2.1 lately and I've created pieces of code to work some concepts out. One of those is how to synchronize animation and media (audio, more precisely). My fisrt shot was to create instances of a Timeline, a MediaPlayer and then invoke the play() method of both instances sequentially. That solution seems to work rather nice since you have just one anination and one audio to play.  Although, what if you have a sequence of anination and audio to play?

An instance of SequentialTransition fits perfectly if you want to play a row of aninations, one right after the other. And an instance of ParallelTransition plays animations at the same time, or at least concurrently. To get things more interesting, you can create instances of ParallelTransition and add them to an instance of SequentialTransition, or the other way around. Both types accepts instances of any type that extends Animation, which is an abstract class of JavaFX. Timeline, SequentialTransition and ParallelTransition themselves, and other transitions all extend Animation, but MediaPlayer don't! So I had to figure out how to synchronize animation and media simultaneously and then play them in a sequence of animations and medias.

Animation

First, let's create a simple animation using Timeline that changes colors of a certain shape. The execution  of such animation takes 5 seconds and color changes smoothly. The piece of code below just creates a rectangle and other instances to represent each color using the RGB color system (Red, Green and Blue). An anonimous class is also created and instanciated which implements the ChangeListener interface; here is where the color of the shape changed. Then, the listener instance is added to each color variable instance.

final Rectangle anyShape = new Rectangle(250, 200);
               
final DoubleProperty redColor = new SimpleDoubleProperty();
final DoubleProperty greenColor = new SimpleDoubleProperty();
final DoubleProperty blueColor = new SimpleDoubleProperty();
        
ChangeListener colorListener = new ChangeListener() {

    @Override
    public void changed(ObservableValue arg0, Object arg1, Object arg2) {
        anyShape.setFill(Color.color(redColor.doubleValue(), greenColor.doubleValue(), blueColor.doubleValue()));  
    }
};
        
redColor.addListener(colorListener);
greenColor.addListener(colorListener);
blueColor.addListener(colorListener);

Now, the color variable instances have to change and that's the animation we're talking about. Here, the Timeline comes into play.

Two colors is picked up to make the transition. Then, an instance of Timeline is created and a sequence of KeyFrame instances is added to it. Pay attention to the pairs of KeyFrame instances: each pair handle a color within a period of time, i.e., Duration instances. Besides Duration instance, the KeyFrame contructor demands a KeyValue instance as well. That one takes in a color variable instance and a start color. So, one KeyFrame instance defines that a variable is set to a certain value at a certain period of time and along another KeyFrame instance, such value is interpolated to another one until another period of time.

Color startColor = Color.BLUEVIOLET;
Color endColor = Color.YELLOWGREEN;

Timeline animation = new Timeline();
animation.getKeyFrames().addAll(new KeyFrame(new Duration(0.0), new KeyValue(redColor, startColor.getRed())),
                                new KeyFrame(new Duration(5000.0), new KeyValue(redColor, endColor.getRed())),
                                new KeyFrame(new Duration(0.0), new KeyValue(greenColor, startColor.getGreen())),
                                new KeyFrame(new Duration(5000.0), new KeyValue(greenColor, endColor.getGreen())),
                                new KeyFrame(new Duration(0.0), new KeyValue(blueColor, startColor.getBlue())),
                                new KeyFrame(new Duration(5000.0), new KeyValue(blueColor, endColor.getBlue())));

animation.play();

Audio

Time to load an audio file and make it play too. The piece of code below does so. The audio file here is inside the JAR file. First, the URL to the audio file is retrieved and then the toExternalForm() method is invoked to get the real path, otherwise, it will not work when the application is executed by the JAR file itself (it will work only within the development IDE).

URL soundURL = this.getClass().getResource("/resource/audio.wav");
final MediaPlayer mediaPlayer = new MediaPlayer(new Media(soundURL.toExternalForm()));
mediaPlayer.play();

Synchronizing Animation and Audio

Since ParallelTransition does not accept a MediaPlayer instance (which makes sense), animation and media synchronization has to have its own way. Actually, Timeline works it out by itself, along KeyFrame and EventHandler instances. The KeyFrame constructor, besides a Duration instance, also accepts an instance of EventHandler. So, at start time, an EventHandler instance must be set, as well as at stop time. The piece of code below shows how to create instances of EventHandler as anonymous classes. Next, KeyFrame instances are created which take in the EventHandler instances at start and stop durations and are right away added to the animation, i.e., the Timeline instance.

EventHandler<ActionEvent> startAudio = new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent arg0) {
        mediaPlayer.play();
    }
};

EventHandler<ActionEvent> stopAudio = new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent arg0) {
        mediaPlayer.stop();
    }
};

animation.getKeyFrames().add(new KeyFrame(new Duration(0.0), startAudio));
animation.getKeyFrames().add(new KeyFrame(new Duration(5000.0), stopAudio));

animation.play();

Conclusion

The main intention of this post is to show how to synchronize animation and media in JavaFX 2.1, using Timeline, KeyFrame and EventHandler classes. Alongside, a simple animation and the load of an audio file are also described and used as basis to the main subject.