Voir le flux RSS

bouye

Faire tourner les cartes - partie 1

Noter ce billet
par , 21/12/2014 à 23h42 (2123 Affichages)
Récemment, une personne demandait sur le forum OTN comment faire une transition animée entre deux contrôles : le contrôle 1 se retourne et affiche le contrôle 2 en donnant l'impression qu'il s'agit de deux faces d'une même carte.

Ce genre de transition est assez classique sur plateformes mobiles, bien qu'un peu moins à la mode ces derniers temps, compte tenu du fait que les versions récentes des OS pour téléphones et tablette s'attachent à en retirer toute trace de skeuomorphisme. On peut cependant encore trouver cet effet dans le lecteur média sur les versions d'iOS antérieures à la version 7, où, sur iPhone et iPod Touch, durant la lecture de la musique, on peut faire se retourner la couverture d'un album pour voir la liste des pistes. Évidement, des jeux de cartes (solitaire, réussite, poker, etc.) peuvent utiliser également ce genre d'animations lorsqu'une carte est retournée à l’écran.

Nom : flip_ios.jpg
Affichages : 722
Taille : 28,7 Ko
Lorsqu'on appuie sur le bouton en haut à droite, l'album (ainsi que le bouton lui-même) se retourne pour afficher la liste des morceaux.

Nous allons voir plusieurs manières de construire une telle animation (ou, tout du moins, quelque chose qui s'en approche) en partant de la mise en place d'une animation 2D toute simple destinée à simuler graphiquement la chose, jusqu’à une vraie animation 3D utilisant des meshes et des textures.

Prérequis

Nous allons avoir besoin de deux images pour afficher une carte, sa face qui affiche sa valeur et son dos. Nous allons prendre une image montrant toutes les valeurs d'un jeu de carte au portrait français et qui est disponible en SVG sur Wikimedia Commons. Nous allons utiliser la version pré rendue disponible a l'IRL http://upload.wikimedia.org/wikipedi...ds-2.0.svg.png. Cette image servira de planche de sprites.

Nous allons donc commencer tout simplement en chargeant l'image dans son intégralité.

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
final Image sourceImage = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Svg-cards-2.0.svg/1280px-Svg-cards-2.0.svg.png");

Et nous allons ensuite utiliser deux instances d'ImageView qui vont se partager cette image pour afficher les parties des cartes qui nous intéressent : l'as de cœur et le dos de la carte.

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
final ImageView frontCard = new ImageView(sourceImage);
frontCard.setViewport(new Rectangle2D(0, 286, 98, 143));
//
final ImageView backCard = new ImageView(sourceImage);
backCard.setViewport(new Rectangle2D(197, 572, 98, 143));

De cette manière, l'image n'est chargée qu'une seule et unique fois en mémoire et nous utilisons des vues pour en afficher que les parties qui nous intéressent.

Voilà, nous avons nos bases. Nous allons désormais travailler avec ces deux éléments pratiquement jusqu’à la toute dernière version de cet exercice. Comme cette version là utilisera des meshes et une texture, le fonctionnement sera un peu différent. Mais ne mettons pas la charrue avant les bœufs...

Faire tourner une carte sur elle-même

Avant de nous lancer, nous allons étudier la rotation de la carte pour essayer de bien comprendre ce qui se passe. Nous pouvons découper l'animation en étapes permettant de mieux comprendre comment elle se déroule. Nous commençons avec notre carte tournée vers nous et nous montrant l'as et nous allons la faire tourner autour de son axe Y :

Nom : card rotate.jpg
Affichages : 621
Taille : 17,5 Ko
Décomposition de l'animation.

  • La carte va tourner de 0° à 180° :
    • La carte a tourné de 0° -> nous voyons l'as de face -> Le dos n'est pas visible.
    • La carte tourné -> l'as est tourné vers la gauche.
    • La carte a tourné de 90° -> elle est vue de coté, sur la tranche -> plus rien n'est visible à l’écran ; on n’aperçoit plus ni l'as, ni le dos.
    • La carte continue de tourner -> le dos de la carte est tourné vers la droite -> l'as n'est pas visible.
    • La carte a tourné de 180° -> nous voyons le dos de face.
  • La carte va tourner de 180° à 360° -> l'animation se déroule comme précédemment mais en intervertissant l'as et le dos.
  • Au delà de 360° -> l'animation boucle -> nous ne faisons que répéter l'animation de 0° à 360° que nous avons fait précédemment.


2D - mise à l’échelle

Pour commencer nous allons faire une animation 2D. Étant donné que nous travaillons en 2D, il ne nous est pas possible d'utiliser une rotation autour de l'axe Y de l’écran. Nous allons donc utiliser la transformation de mise à l’échelle (scale) à la place. Cette méthode très simple ne fourni pas le résultat le plus réaliste, mais elle ne demande pas beaucoup de puissance de calcul ou de mémoire. Elle est donc tout à fait adaptée aux plateformes peu puissantes telles que le PI ou les téléphones bas de gamme.

Ici, nous allons utiliser une animation de mise à l’échelle, ScaleTransition pour simuler la rotation de la carte sur elle-même.

Nom : card scale1.jpg
Affichages : 622
Taille : 17,9 Ko
Une fausse animation en utilisant des mises à l'échelle.

  • La carte va tourner de 0° à 90° -> il faut appliquer une mise à l’échelle sur l'axe X sur la face montrant l'as qui va aller de 1 (pas de mise à l’échelle) à 0 (la face n'est pas visible).
  • La carte va tourner de 90° à 180° -> il faut appliquer une mise à l’échelle sur l'axe X sur la face montrant le dos qui va aller de 0 (la face n'est pas visible) à 1 (pas de mise à l’échelle).
  • La carte va tourner de 180° à 360° -> l'animation se déroule comme précédemment mais en intervertissant l'as et le dos.


Nous allons continuer à parler de rotation de la carte sur elle-même, même si dans cette implémentation nous ne gèrerons aucune rotation. Nous allons donc créer une instance de SequentialTransition qui va contenir deux animations plus simples :
  1. L'animation de 0° à 180°
  2. L'animation de 180° à 360°


Comme il s'agit à chaque fois de la même animation, nous intervertirons tout simplement les deux faces de la carte quand nous passerons d'une animation à l'autre.

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
final SequentialTransition animation = new SequentialTransition(
    flip(frontCard, backCard), // L'animation de 0° à 180°
    flip(backCard, frontCard)); // L'animation de 180° à 360°

Note : il est tout à fait possible de créer une seule et unique Timeline qui contienne tous la gestion de l’intégralité de l'animation au complet. Cependant, ici, j'ai préféré découper les différentes étapes en sous-animation pour rendre le problème plus facile à appréhender.

Il nous faut maintenant implémenter la méthode flip() qui va construire une animation qui va de 0° à 180° :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
private Transition flip(Node front, Node back) {
    // Nous faisons disparaitre la face avant.
    final ScaleTransition scaleOutFront = new ScaleTransition(halfFlipDuration, front);
    scaleOutFront.setFromX(1);
    scaleOutFront.setToX(0);
    // Nous faisons apparaitre la face arrière.
    final ScaleTransition scaleInBack = new ScaleTransition(halfFlipDuration, back);
    scaleInBack.setFromX(0);
    scaleInBack.setToX(1);
    //
    return new SequentialTransition(scaleOutFront, scaleInBack);
}

Ici, halfFlipDuration est une durée qui défini le temps d'une rotation de 0° à 90° :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
private final Duration halfFlipDuration = Duration.seconds(1);

Il nous suffit donc désormais d’implémenter le tout dans une Application avec un bouton pour démarrer ou stopper l'animation et le tour est joué !

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package test;
 
import javafx.animation.ScaleTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.beans.binding.DoubleBinding;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToolBar;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
 
public class Test_D2Scale1 extends Application {
 
    private final Duration halfFlipDuration = Duration.seconds(1);
 
    @Override
    public void start(Stage primaryStage) {
        final Image sourceImage = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Svg-cards-2.0.svg/1280px-Svg-cards-2.0.svg.png");
        //
        final ImageView frontCard = new ImageView(sourceImage);
        frontCard.setViewport(new Rectangle2D(0, 286, 98, 143));
        //
        final ImageView backCard = new ImageView(sourceImage);
        backCard.setViewport(new Rectangle2D(197, 572, 98, 143));
        backCard.setScaleX(0);
        //
        final StackPane stackPane = new StackPane();
        stackPane.getChildren().addAll(frontCard, backCard);
        final ToggleButton playButton = new ToggleButton("Play");
        StackPane.setAlignment(playButton, Pos.TOP_LEFT);
        final Slider timeSlider = new Slider(0, 4 * halfFlipDuration.toMillis(), 0);
        timeSlider.setDisable(true);
        final ToolBar toolBar = new ToolBar();
        toolBar.getItems().addAll(playButton, timeSlider);
        final BorderPane root = new BorderPane();
        root.setTop(toolBar);
        root.setCenter(stackPane);
        final Scene scene = new Scene(root, 300, 250);
        primaryStage.setTitle("2D: scale");
        primaryStage.setScene(scene);
        primaryStage.show();
        //
        final SequentialTransition animation = new SequentialTransition(
                flip(frontCard, backCard), // L'animation de 0° à 180°
                flip(backCard, frontCard)); // L'animation de 180° à 360°
        animation.setCycleCount(SequentialTransition.INDEFINITE);
        playButton.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
            if (newValue) {
                animation.play();
            } else {
                animation.pause();
            }
        });
        timeSlider.valueProperty().bind(new DoubleBinding() {
            {
                bind(animation.currentTimeProperty());
            }
 
            @Override
            public void dispose() {
                super.dispose();
                unbind(animation.currentTimeProperty());
            }
 
            @Override
            protected double computeValue() {
                return animation.getCurrentTime().toMillis();
            }
        });
    }
 
    private Transition flip(Node front, Node back) {
        // Nous faisons disparaitre la face avant.
        final ScaleTransition scaleOutFront = new ScaleTransition(halfFlipDuration, front);
        scaleOutFront.setFromX(1);
        scaleOutFront.setToX(0);
        // Nous faisons apparaitre la face arrière.
        final ScaleTransition scaleInBack = new ScaleTransition(halfFlipDuration, back);
        scaleInBack.setFromX(0);
        scaleInBack.setToX(1);
        //
        return new SequentialTransition(scaleOutFront, scaleInBack);
    }
 
    public static void main(String[] args) {
        launch(args);
    }
}

Notez qu'il faut songer à rendre le dos invisible au départ en mettant son échelle sur l'axe des X à 0 pour éviter qu'elle s'affiche.

Nous avons déjà une animation qui fonctionne, cependant, elle manque de réalisme. Il existe pourtant un moyen très simple de donner a l’œil (enfin, au cerveau plutôt) l'impression que la carte tourne : nous allons faire varier l’intensité de son éclairage !

Nom : card scale2.jpg
Affichages : 617
Taille : 16,9 Ko
Un simple changement de luminosité peut rendre l'animation plus agréable à l’œil.

Reprenons les étapes clés de notre animation.
  • La carte va tourner de 0° à 90° -> la face qui montre l'as va progressivement s'assombrir jusqu’à devenir sombre ou noire.
  • La carte va tourner de 90° à 180° -> la face qui montre le dos est initialement sombre ou noire. et va progressivement s’éclaircir jusqu’à atteindre une luminosité normale.


Nous pouvons faire cela avec l'effet graphique ColorAdjust. Initialement, nous ne touchons pas à la propriete brightness des effets donc les cartes s'affichent à l'identique.

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
final ColorAdjust frontColorAdjust = new ColorAdjust();
frontCard.setEffect(frontColorAdjust);
//
final ColorAdjust backColorAdjust = new ColorAdjust();
backCard.setEffect(backColorAdjust);

Nous allons légèrement modifier la construction de notre animation principale :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
final SequentialTransition animation = new SequentialTransition(
        flip(frontCard, frontColorAdjust, backCard, backColorAdjust),
        flip(backCard, backColorAdjust, frontCard, frontColorAdjust));

Et nous allons créer une nouvelle version de la méthode flip() de manière à gérer en plus des animation de la luminosité de l'effet graphique :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Transition flip(Node front, ColorAdjust frontColorAdjust, Node back, ColorAdjust backColorAdjust) {
    // Nous faisons disparaitre la face avant.
    final ScaleTransition scaleOutFront = new ScaleTransition(halfFlipDuration, front);
    scaleOutFront.setFromX(1);
    scaleOutFront.setToX(0);
    final Timeline changeBrightnessFront = new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(frontColorAdjust.brightnessProperty(), 0)),
            new KeyFrame(halfFlipDuration, new KeyValue(frontColorAdjust.brightnessProperty(), -1)));
    final ParallelTransition flipOutFront = new ParallelTransition(scaleOutFront, changeBrightnessFront);
    // Nous faisons apparaitre la face arrière.
    final ScaleTransition scaleInBack = new ScaleTransition(halfFlipDuration, back);
    scaleInBack.setFromX(0);
    scaleInBack.setToX(1);
    final Timeline changeBrightnessBack = new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(backColorAdjust.brightnessProperty(), -1)),
            new KeyFrame(halfFlipDuration, new KeyValue(backColorAdjust.brightnessProperty(), 0)));
    final ParallelTransition flipInBack = new ParallelTransition(scaleInBack, changeBrightnessBack);
    //
    return new SequentialTransition(flipOutFront, flipInBack);
}

Ce qui nous donne :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package test;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ParallelTransition;
import javafx.animation.ScaleTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.beans.binding.DoubleBinding;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToolBar;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Test_D2Scale2 extends Application {

    private final Duration halfFlipDuration = Duration.seconds(1);

    @Override
    public void start(Stage primaryStage) {
        final Image sourceImage = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Svg-cards-2.0.svg/1280px-Svg-cards-2.0.svg.png");
        //
        final ImageView frontCard = new ImageView(sourceImage);
        frontCard.setViewport(new Rectangle2D(0, 286, 98, 143));
        final ColorAdjust frontColorAdjust = new ColorAdjust();
        frontCard.setEffect(frontColorAdjust);
        //
        final ImageView backCard = new ImageView(sourceImage);
        backCard.setViewport(new Rectangle2D(197, 572, 98, 143));
        backCard.setScaleX(0);
        final ColorAdjust backColorAdjust = new ColorAdjust();
        backCard.setEffect(backColorAdjust);
        //
        final StackPane stackPane = new StackPane();
        stackPane.getChildren().addAll(frontCard, backCard);
        final ToggleButton playButton = new ToggleButton("Play");
        StackPane.setAlignment(playButton, Pos.TOP_LEFT);
        final Slider timeSlider = new Slider(0, 4 * halfFlipDuration.toMillis(), 0);
        timeSlider.setDisable(true);
        final ToolBar toolBar = new ToolBar();
        toolBar.getItems().addAll(playButton, timeSlider);
        final BorderPane root = new BorderPane();
        root.setTop(toolBar);
        root.setCenter(stackPane);
        final Scene scene = new Scene(root, 300, 250);
        primaryStage.setTitle("2D: scale + shadow");
        primaryStage.setScene(scene);
        primaryStage.show();
        //
        final SequentialTransition animation = new SequentialTransition(
                flip(frontCard, frontColorAdjust, backCard, backColorAdjust),
                flip(backCard, backColorAdjust, frontCard, frontColorAdjust));
        animation.setCycleCount(SequentialTransition.INDEFINITE);
        playButton.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
            if (newValue) {
                animation.play();
            } else {
                animation.pause();
            }
        });
        timeSlider.valueProperty().bind(new DoubleBinding() {
            {
                bind(animation.currentTimeProperty());
            }

            @Override
            public void dispose() {
                super.dispose();
                unbind(animation.currentTimeProperty());
            }

            @Override
            protected double computeValue() {
                return animation.getCurrentTime().toMillis();
            }
        });
    }

    private Transition flip(Node front, ColorAdjust frontColorAdjust, Node back, ColorAdjust backColorAdjust) {
        // Nous faisons disparaitre la face avant.
        final ScaleTransition scaleOutFront = new ScaleTransition(halfFlipDuration, front);
        scaleOutFront.setFromX(1);
        scaleOutFront.setToX(0);
        final Timeline changeBrightnessFront = new Timeline(
                new KeyFrame(Duration.ZERO, new KeyValue(frontColorAdjust.brightnessProperty(), 0)),
                new KeyFrame(halfFlipDuration, new KeyValue(frontColorAdjust.brightnessProperty(), -1)));
        final ParallelTransition flipOutFront = new ParallelTransition(scaleOutFront, changeBrightnessFront);
        // Nous faisons apparaitre la face arrière.
        final ScaleTransition scaleInBack = new ScaleTransition(halfFlipDuration, back);
        scaleInBack.setFromX(0);
        scaleInBack.setToX(1);
        final Timeline changeBrightnessBack = new Timeline(
                new KeyFrame(Duration.ZERO, new KeyValue(backColorAdjust.brightnessProperty(), -1)),
                new KeyFrame(halfFlipDuration, new KeyValue(backColorAdjust.brightnessProperty(), 0)));
        final ParallelTransition flipInBack = new ParallelTransition(scaleInBack, changeBrightnessBack);
        //
        return new SequentialTransition(flipOutFront, flipInBack);
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Désormais, au cours de l'animation, la luminosité de chaque face va varier au cours du temps ce qui rend l'effet plus réaliste.

Prochaine étape : un peu de perspective !

Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog Viadeo Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog Twitter Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog Google Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog Facebook Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog Digg Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog Delicious Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog MySpace Envoyer le billet « Faire tourner les cartes - partie 1 » dans le blog Yahoo

Mis à jour 20/01/2015 à 00h17 par bouye

Tags: carte
Catégories
Java , Java , JavaFX

Commentaires