[Actualité] Faire tourner les cartes - partie 4
par
, 16/02/2015 à 03h16 (1846 Affichages)
Lorsque nous nous sommes quittés la dernière fois, nous avions positionné des contrôles 2D dans un environnement 3D. Nous avions alors appliqué des rotations sur chacune des faces de notre carte pour la faire se tourner sur elle-même.
Cependant, il semble un peu idiot, maintenant que nous avons accès à un environnement 3D, de devoir encore manipuler chacune des faces séparément alors que nous pourrions effecteur une seule rotation de 360° sur un objet représentant la carte entière au lieu d'effectuer plusieurs petites rotations sur chacune des faces.
Commençons par charger nos images sur chacune de nos faces avec le même code que d'habitude :
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 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));
Et maintenant, groupons les ensemble :
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part final Group card = new Group(frontCard, backCard);
Si nous affichons cet objet, nous voyons bien sûr que la face représentant le dos. En effet, cet ImageView est sur le sommet du groupe et donc cache l'autre face. Il nous faut donc cacher, soit en modifiant la propriété contrôlant le fait qu'elle soit visible, soit en modifiant l'ordre des objets dans le groupe (mais dans ce cas, cela demande un peu plus de travail lors de l'affichage puisqu'elle est déclarée visible) :
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 backCard.setVisible(false); //backCard.toBack();
Nous ajoutons ensuite cet objet dans une sous-scène permettant d’afficher la 3D avec une caméra de perspective comme nous l'avons fait précédemment. Désormais, il ne nous reste plus qu'à écrire la rotation :
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 final RotateTransition rotateCard = new RotateTransition(halfFlipDuration.multiply(4), card); rotateCard.setFromAngle(0); rotateCard.setToAngle(360); rotateCard.setInterpolator(Interpolator.LINEAR); final Timeline animation = rotateCard; animation.setCycleCount(RotateTransition.INDEFINITE);
Ce qui nous donne :
Erreur...
Et... et quelque chose cloche... Nous avons en effet notre carte qui tourne bien sur elle-même de 360° ; mais elle nous présente toujours la même face : l'as...
Donc, nous allons devoir modifier la rotation pour qu'à certaines étapes clés, la face montrant le dos devienne visible tandis que celle montrant l'as devienne invisible et inversement lors de la seconde partie de la rotation.
On peut aussi résoudre le problème en faisant que la face montrant le dos passe par dessus celle montrant l'as et inversement lors de la seconde partie (idem cela demande plus de travail lors de l'affichage car alors les deux faces sont visibles).
Commençons par réécrire notre rotation en utilisant cette fois-ci une instance de la classe Timeline plutôt que de la classe RotateTransition :
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 final Timeline rotateCard = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(card.rotateProperty(), 0)), new KeyFrame(halfFlipDuration.multiply(4), new KeyValue(card.rotateProperty(), 360))); final Timeline animation = rotateCard; animation.setCycleCount(SequentialTransition.INDEFINITE);
Ici nous avons inséré deux étapes clés dans l'animation :
- À zero secondes, la carte subit une rotation de 0°.
- À la fin de l'animation, la carte subit une rotation de 360°.
Les valeurs intermédiaires seront interpolées entre ces deux étapes clés et pour le moment, à l'affichage, le résultat est identique à celui précédemment obtenu.
Nous allons maintenant insérer deux nouvelles étapes clés au ¼ et ¾ de la rotation pour modifier la visibilité de chacune des faces :
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 final Timeline rotateCard = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(card.rotateProperty(), 0)), new KeyFrame(halfFlipDuration, actionEvent -> { frontCard.setVisible(false); backCard.setVisible(true); // backCard.toFront(); }), new KeyFrame(halfFlipDuration.multiply(3), actionEvent -> { frontCard.setVisible(true); backCard.setVisible(false); // frontCard.toFront(); }), new KeyFrame(halfFlipDuration.multiply(4), new KeyValue(card.rotateProperty(), 360)));
Ici, nous n'utilisons pas des propriétés mais des déclenchements d’évènements ; il n'y aura donc aucune interpolation de valeur : uniquement une action qui sera effectuée au moment opportun.
Lors de l’exécution, tout semble être correct désormais : notre face as disparait pour afficher la face dos puis l'inverse se produit et notre carte semble tourner correctement sur elle-même...
Enfin un résultat correct ?
Mais est-ce vraiment le cas ? Notre l'image qui représente le dos de la carte est symétrique suivant l'axe vertical donc nous ne pouvons pas vraiment nous rendre compte d'un défaut... cependant si nous utilisons une image qui n'offre pas une telle symétrie ou qui contient du texte, un nouveau problème apparait rapidement :
Non, ce n'est toujours pas bon...
Eh oui... lorsqu'il est visible, le dos de la carte subit un effet miroir et donc apparait inversé suivant l'axe Y. Et cela se voit particulièrement lorsqu'on y affiche du texte. Ce problème était d'ailleurs déjà apparent lorsque nous ne voyions que la face as de la carte durant l'animation ; vous vous en rendrez compte si vous remontez quelques images plus haut.
En y réfléchissant, ce problème est tout à fait normal. N'oubliez pas : à l'origine le dos de notre carte est orienté vers nous, tout comme l'as. Ainsi, lorsque le groupe qui définit la carte tourne de 180°, la face visible (que ce soit l'as ou le dos) a également tourné de 180° et donc nous voyons son miroir. Nous pouvons facilement corriger ceci en rajoutant lors de la création de la face dos :
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 backCard.setRotationAxis(Rotate.Y_AXIS); backCard.setRotate(180);
Désormais, le dos de la carte est affiché en miroir lors de l'initialisation du groupe. Donc, lorsque le groupe aura tourné de 180°, le dos, qui aura lui tourné de 360°, s'affichera dans une orientation correcte et le texte sera lisible dans le bons sens :
Bingo !
Voici donc le code final de cette transformation :
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 package test; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.SequentialTransition; import javafx.animation.Timeline; import javafx.application.Application; import javafx.beans.binding.DoubleBinding; import javafx.geometry.Pos; import javafx.geometry.Rectangle2D; import javafx.scene.Group; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; import javafx.scene.SceneAntialiasing; import javafx.scene.SubScene; 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.scene.transform.Rotate; import javafx.stage.Stage; import javafx.util.Duration; public class Test_D3Goup1 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.setRotationAxis(Rotate.Y_AXIS); backCard.setRotate(180); // final Group card = new Group(frontCard, backCard); backCard.setVisible(false); // backCard.toBack(); card.setRotationAxis(Rotate.Y_AXIS); // final StackPane stackPane = new StackPane(); stackPane.getChildren().addAll(card); final SubScene subScene = new SubScene(stackPane, 300, 250, false, SceneAntialiasing.BALANCED); subScene.setCamera(new PerspectiveCamera()); 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(subScene); final Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("3D: Group"); primaryStage.setScene(scene); primaryStage.show(); // final Timeline rotateCard = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(card.rotateProperty(), 0)), new KeyFrame(halfFlipDuration, actionEvent -> { frontCard.setVisible(false); backCard.setVisible(true); // backCard.toFront(); }), new KeyFrame(halfFlipDuration.multiply(3), actionEvent -> { frontCard.setVisible(true); backCard.setVisible(false); // frontCard.toFront(); }), new KeyFrame(halfFlipDuration.multiply(4), new KeyValue(card.rotateProperty(), 360))); final Timeline animation = rotateCard; 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(); } }); } public static void main(String[] args) { launch(args); } }
Tout comme précédemment, aucun éclairage ne s'applique sur notre carte et donc nous devons une nouvelle fois nous reposer sur des effets 2D pour modifier l'apparence de la carte. Contrairement aux animations précédentes pour lesquelles nous avions traité chaque face séparément, ici, il est plus facile, tout comme pour la rotation, de placer l'effet ColorAdjust sur le Group représentant la carte elle-même. Nous créons alors une Timeline séparée pour gérer la variation de couleur et les deux animations seront combinées dans une ParallelTransition . Ici encore, je fais cela pour rendre les choses plus faciles à appréhender ; il est tout à fait possible de rajouter toutes les étapes clés des deux animations dans une seule et unique ligne temporelle.
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
116
117
118
119
120 package test; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ParallelTransition; import javafx.animation.SequentialTransition; import javafx.animation.Timeline; import javafx.application.Application; import javafx.beans.binding.DoubleBinding; import javafx.geometry.Pos; import javafx.geometry.Rectangle2D; import javafx.scene.Group; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; import javafx.scene.SceneAntialiasing; import javafx.scene.SubScene; 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.scene.transform.Rotate; import javafx.stage.Stage; import javafx.util.Duration; public class Test_D3Goup2 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.setRotationAxis(Rotate.Y_AXIS); backCard.setRotate(-180); // final Group card = new Group(frontCard, backCard); backCard.setVisible(false); // backCard.toBack(); card.setRotationAxis(Rotate.Y_AXIS); final ColorAdjust cardColorAdjust = new ColorAdjust(); card.setEffect(cardColorAdjust); // final StackPane stackPane = new StackPane(); stackPane.getChildren().addAll(card); final SubScene subScene = new SubScene(stackPane, 300, 250, false, SceneAntialiasing.BALANCED); subScene.setCamera(new PerspectiveCamera()); 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(subScene); final Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("3D: Group + shadow"); primaryStage.setScene(scene); primaryStage.show(); // final Timeline rotateCard = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(card.rotateProperty(), 0)), new KeyFrame(halfFlipDuration, actionEvent -> { frontCard.setVisible(false); backCard.setVisible(true); // backCard.toFront(); }), new KeyFrame(halfFlipDuration.multiply(3), actionEvent -> { frontCard.setVisible(true); backCard.setVisible(false); // frontCard.toFront(); }), new KeyFrame(halfFlipDuration.multiply(4), new KeyValue(card.rotateProperty(), 360))); final Timeline changeBrightness = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(cardColorAdjust.brightnessProperty(), 0)), new KeyFrame(halfFlipDuration, new KeyValue(cardColorAdjust.brightnessProperty(), -1)), new KeyFrame(halfFlipDuration.multiply(2), new KeyValue(cardColorAdjust.brightnessProperty(), 0)), new KeyFrame(halfFlipDuration.multiply(3), new KeyValue(cardColorAdjust.brightnessProperty(), -1)), new KeyFrame(halfFlipDuration.multiply(4), new KeyValue(cardColorAdjust.brightnessProperty(), 0)) ); final ParallelTransition animation = new ParallelTransition(rotateCard, changeBrightness); 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(); } }); } public static void main(String[] args) { launch(args); } }
Voilà, nous en avons fini avec les formes 2D. Lors de la prochaine étape, nous aborderons des sujets tels que les vertices, les coordonnées de textures, les meshes, les normales… une bonne prise de tête en perspective