Dans l’entrée précédente, nous avons vu comment simuler la rotation d'une carte sur elle même en utilisant une transformation de mise à l’échelle. Bien que n’étant pas graphiquement très réaliste, cette manière de faire à l'avantage d’être en 2D et de ne pas demander beaucoup de puissance.
Aujourd'hui, nous allons rester en 2D mais nous allons être amené à utiliser une transformation (ou plutôt, dans ce cas, un effet) plus réaliste : une transformation de perspective en utilisant la classe PerspectiveTransform. Cet effet créant une transformation qui n'est pas affine, il est plus gourmand en puissance graphique et de calcul et peut demander également plus de mémoire.
Il s'agit d'un effet graphique qui prend les 4 coins d'un nœud et les déplace aux coordonnées désignées, déformant ainsi le contenu du nœud (dans notre cas une instance d'ImageView).
L'image s’inscrit dans une boite englobante rectangulaire qui contient 4 sommets :
- UL - le coin supérieur gauche (Upper Left).
- UR - le coin supérieur droit (Upper Right).
- LL - le coin inférieur gauche (Lower Left).
- LR - le coin inférieur droit (Lower Right).
Nous allons leur appliquer une modification d'enveloppe : c'est à dire que nous allons déplacer les coordonnées de ces 4 points en spécifiant une quadrilatère défini par les coordonnées destination des 4 sommets. Quand la transformation est appliquée, l'image s'en trouve modifiée et a une apparence déformée. C'est là le principe de l'effet de perspective.
Note : ici nous effectuons la rotation sur des instances d'ImageView. Mais on pourrait tout aussi bien le faire sur des contrôles interactif (ce qui était le but initial du poseur de la question sur OTN). Or, la transformation correcte des coordonnées des événements souris n'est pas garantie lors que l'effet PerspectiveTransformest utilisé. Mieux vaudra donc désactiver toute saisie utilisateur durant l'animation du passage d'un contrôle à un autre pour ensuite les réactiver lorsque l'animation est terminée.
Une fausse animation en utilisant un effet de perspective.
Ici, il ne s'agit pas non-plus d'une vraie rotation de la carte sur elle-même. Nous utilisons simplement un effet graphique qui donne l'impression que la rotation a lieu.
Lors du passage de 0° à 90°, l'enveloppe de la transformation de la face avant est modifiée de manière à ce que UL, UR, LL et LR soient alignés sur une seule et même ligne verticale.
Les déplacement des 4 sommets à 0°, 45° et 90° (face avant).
Lors du passage de 90° à 180°, l'enveloppe de la transformation de la face arrière est modifiée de manière à ce que UL, UR, LL et LR, qui initialement étaient alignés sur une seule et même ligne verticale, reprennent leur positions d'origine pour afficher afficher la carte de face.
Les déplacement des 4 sommets à 90°, 135° et 180° (face arrière).
Le déplacement de chacun des 4 points s'effectue de manière linéaire ; il n'y a pas besoin de s’embêter à essayer de calculer un déplacement en suivant une courbe pour tenter de simuler la perspective : l'animation étant rapide, elle est déjà suffisante en soit pour tromper l’œil.
Tout d'abord, nous allons définir une variable offset qui va contenir le décalage verticale maximal de la perspective et, ici, lui donner une valeur de 10 pixels.
Code Java : | Sélectionner tout |
private final double offset = 10;
Initialisons ensuite nos instances d'ImageView en leur donnant à chacune un effet graphique de type PerspectiveTransform :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | final ImageView frontCard = new ImageView(sourceImage); frontCard.setViewport(new Rectangle2D(0, 286, 98, 143)); final PerspectiveTransform frontPerspective = new PerspectiveTransform(0, 0, 98, 0, 98, 143, 0, 143); frontCard.setEffect(frontPerspective); // final ImageView backCard = new ImageView(sourceImage); backCard.setViewport(new Rectangle2D(197, 572, 98, 143)); final PerspectiveTransform backPerspective = new PerspectiveTransform(98 / 2d, -offset, 98 / 2d, offset, 98 / 2d, 143 - offset, 98 / 2d, 143 + offset); backCard.setEffect(backPerspective); |
Ici, l'enveloppe de la face avant de la carte n'est pas modifiée (on voit donc l'as de face) tandis que tous les points du dos sont alignés sur une même ligne verticale (on ne voit donc pas cette face).
Une fois de plus, pour rendre la chose plus aisément compréhensible, nous allons découper l'animation en plusieurs sous-parties. Ce n'est pas forcement la manière la plus optimisée de faire, mais cela rend le code moins complexe à saisir.
Code Java : | Sélectionner tout |
1 2 3 4 | final SequentialTransition animation = new SequentialTransition( flip(98, 143, frontCard, frontPerspective, backCard, backPerspective), flip(98, 143, backCard, backPerspective, frontCard, frontPerspective)); animation.setCycleCount(SequentialTransition.INDEFINITE); |
Nous implémentons maintenant la méthode flip() pour animer séparément chaque face. Ici, nous allons créer une Timeline qui permettra d'animer chaque point de l'enveloppe. À chaque KeyFrame (étape clé de l’animation) nous avons 4 points avec 2 coordonnées X et Y par point = 8 valeurs à spécifier.
Code Java : | Sélectionner tout |
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 | private Transition flip(double width, double height, Node front, PerspectiveTransform frontPerspective, Node back, PerspectiveTransform backPerspective) { final Timeline perspectiveOutFront = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(frontPerspective.ulxProperty(), 0), new KeyValue(frontPerspective.ulyProperty(), 0), new KeyValue(frontPerspective.urxProperty(), width), new KeyValue(frontPerspective.uryProperty(), 0), new KeyValue(frontPerspective.lrxProperty(), width), new KeyValue(frontPerspective.lryProperty(), height), new KeyValue(frontPerspective.llxProperty(), 0), new KeyValue(frontPerspective.llyProperty(), height) ), new KeyFrame(halfFlipDuration, new KeyValue(frontPerspective.ulxProperty(), width / 2d), new KeyValue(frontPerspective.ulyProperty(), offset), new KeyValue(frontPerspective.urxProperty(), width / 2d), new KeyValue(frontPerspective.uryProperty(), -offset), new KeyValue(frontPerspective.lrxProperty(), width / 2d), new KeyValue(frontPerspective.lryProperty(), height + offset), new KeyValue(frontPerspective.llxProperty(), width / 2d), new KeyValue(frontPerspective.llyProperty(), height - offset) )); // final Timeline perspectiveInBack = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(backPerspective.ulxProperty(), width / 2d), new KeyValue(backPerspective.ulyProperty(), -offset), new KeyValue(backPerspective.urxProperty(), width / 2d), new KeyValue(backPerspective.uryProperty(), offset), new KeyValue(backPerspective.lrxProperty(), width / 2d), new KeyValue(backPerspective.lryProperty(), height - offset), new KeyValue(backPerspective.llxProperty(), width / 2d), new KeyValue(backPerspective.llyProperty(), height + offset) ), new KeyFrame(halfFlipDuration, new KeyValue(backPerspective.ulxProperty(), 0), new KeyValue(backPerspective.ulyProperty(), 0), new KeyValue(backPerspective.urxProperty(), width), new KeyValue(backPerspective.uryProperty(), 0), new KeyValue(backPerspective.lrxProperty(), width), new KeyValue(backPerspective.lryProperty(), height), new KeyValue(backPerspective.llxProperty(), 0), new KeyValue(backPerspective.llyProperty(), height) )); // return new SequentialTransition(perspectiveOutFront, perspectiveInBack); } |
Et nous répétons ces mêmes animations en inversant les faces des cartes pour l'animation de 180° a 360°.
Voici le code complet du test :
Code Java : | Sélectionner tout |
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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | package test; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; 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.PerspectiveTransform; 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_D2Perspective1 extends Application { private final Duration halfFlipDuration = Duration.seconds(1); private final double offset = 10; @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 PerspectiveTransform frontPerspective = new PerspectiveTransform(0, 0, 98, 0, 98, 143, 0, 143); frontCard.setEffect(frontPerspective); // final ImageView backCard = new ImageView(sourceImage); backCard.setViewport(new Rectangle2D(197, 572, 98, 143)); final PerspectiveTransform backPerspective = new PerspectiveTransform(98 / 2d, -offset, 98 / 2d, offset, 98 / 2d, 143 - offset, 98 / 2d, 143 + offset); backCard.setEffect(backPerspective); // 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: perspective"); primaryStage.setScene(scene); primaryStage.show(); // final SequentialTransition animation = new SequentialTransition( flip(98, 143, frontCard, frontPerspective, backCard, backPerspective), flip(98, 143, backCard, backPerspective, frontCard, frontPerspective)); 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(double width, double height, Node front, PerspectiveTransform frontPerspective, Node back, PerspectiveTransform backPerspective) { final Timeline perspectiveOutFront = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(frontPerspective.ulxProperty(), 0), new KeyValue(frontPerspective.ulyProperty(), 0), new KeyValue(frontPerspective.urxProperty(), width), new KeyValue(frontPerspective.uryProperty(), 0), new KeyValue(frontPerspective.lrxProperty(), width), new KeyValue(frontPerspective.lryProperty(), height), new KeyValue(frontPerspective.llxProperty(), 0), new KeyValue(frontPerspective.llyProperty(), height) ), new KeyFrame(halfFlipDuration, new KeyValue(frontPerspective.ulxProperty(), width / 2d), new KeyValue(frontPerspective.ulyProperty(), offset), new KeyValue(frontPerspective.urxProperty(), width / 2d), new KeyValue(frontPerspective.uryProperty(), -offset), new KeyValue(frontPerspective.lrxProperty(), width / 2d), new KeyValue(frontPerspective.lryProperty(), height + offset), new KeyValue(frontPerspective.llxProperty(), width / 2d), new KeyValue(frontPerspective.llyProperty(), height - offset) )); // final Timeline perspectiveInBack = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(backPerspective.ulxProperty(), width / 2d), new KeyValue(backPerspective.ulyProperty(), -offset), new KeyValue(backPerspective.urxProperty(), width / 2d), new KeyValue(backPerspective.uryProperty(), offset), new KeyValue(backPerspective.lrxProperty(), width / 2d), new KeyValue(backPerspective.lryProperty(), height - offset), new KeyValue(backPerspective.llxProperty(), width / 2d), new KeyValue(backPerspective.llyProperty(), height + offset) ), new KeyFrame(halfFlipDuration, new KeyValue(backPerspective.ulxProperty(), 0), new KeyValue(backPerspective.ulyProperty(), 0), new KeyValue(backPerspective.urxProperty(), width), new KeyValue(backPerspective.uryProperty(), 0), new KeyValue(backPerspective.lrxProperty(), width), new KeyValue(backPerspective.lryProperty(), height), new KeyValue(backPerspective.llxProperty(), 0), new KeyValue(backPerspective.llyProperty(), height) )); // return new SequentialTransition(perspectiveOutFront, perspectiveInBack); } public static void main(String[] args) { launch(args); } } |
Une fois de plus, nous pouvons également utiliser un effet de changement de couleur pour simuler l'assombrissement des faces de manière à rendre la fausse rotation plus réaliste :
Code Java : | Sélectionner tout |
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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | package test; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ParallelTransition; 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.effect.PerspectiveTransform; 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_D2Perspective2 extends Application { private final Duration halfFlipDuration = Duration.seconds(1); private final double offset = 10; @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 PerspectiveTransform frontPerspective = new PerspectiveTransform(0, 0, 98, 0, 98, 143, 0, 143); frontCard.setEffect(frontPerspective); final ColorAdjust frontColorAdjust = new ColorAdjust(); frontPerspective.setInput(frontColorAdjust); // final ImageView backCard = new ImageView(sourceImage); backCard.setViewport(new Rectangle2D(197, 572, 98, 143)); final PerspectiveTransform backPerspective = new PerspectiveTransform(98 / 2d, -offset, 98 / 2d, offset, 98 / 2d, 143 - offset, 98 / 2d, 143 + offset); backCard.setEffect(backPerspective); final ColorAdjust backColorAdjust = new ColorAdjust(); backPerspective.setInput(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: perspective + shadow"); primaryStage.setScene(scene); primaryStage.show(); // final SequentialTransition animation = new SequentialTransition( flip(98, 143, frontCard, frontPerspective, frontColorAdjust, backCard, backPerspective, backColorAdjust), flip(98, 143, backCard, backPerspective, backColorAdjust, frontCard, frontPerspective, 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(double width, double height, Node front, PerspectiveTransform frontPerspective, ColorAdjust frontColorAdjust, Node back, PerspectiveTransform backPerspective, ColorAdjust backColorAdjust) { final Timeline perspectiveOutFront = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(frontPerspective.ulxProperty(), 0), new KeyValue(frontPerspective.ulyProperty(), 0), new KeyValue(frontPerspective.urxProperty(), width), new KeyValue(frontPerspective.uryProperty(), 0), new KeyValue(frontPerspective.lrxProperty(), width), new KeyValue(frontPerspective.lryProperty(), height), new KeyValue(frontPerspective.llxProperty(), 0), new KeyValue(frontPerspective.llyProperty(), height) ), new KeyFrame(halfFlipDuration, new KeyValue(frontPerspective.ulxProperty(), width / 2d), new KeyValue(frontPerspective.ulyProperty(), offset), new KeyValue(frontPerspective.urxProperty(), width / 2d), new KeyValue(frontPerspective.uryProperty(), -offset), new KeyValue(frontPerspective.lrxProperty(), width / 2d), new KeyValue(frontPerspective.lryProperty(), height + offset), new KeyValue(frontPerspective.llxProperty(), width / 2d), new KeyValue(frontPerspective.llyProperty(), height - offset) )); 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(perspectiveOutFront, changeBrightnessFront); // final Timeline perspectiveInBack = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(backPerspective.ulxProperty(), width / 2d), new KeyValue(backPerspective.ulyProperty(), -offset), new KeyValue(backPerspective.urxProperty(), width / 2d), new KeyValue(backPerspective.uryProperty(), offset), new KeyValue(backPerspective.lrxProperty(), width / 2d), new KeyValue(backPerspective.lryProperty(), height - offset), new KeyValue(backPerspective.llxProperty(), width / 2d), new KeyValue(backPerspective.llyProperty(), height + offset) ), new KeyFrame(halfFlipDuration, new KeyValue(backPerspective.ulxProperty(), 0), new KeyValue(backPerspective.ulyProperty(), 0), new KeyValue(backPerspective.urxProperty(), width), new KeyValue(backPerspective.uryProperty(), 0), new KeyValue(backPerspective.lrxProperty(), width), new KeyValue(backPerspective.lryProperty(), height), new KeyValue(backPerspective.llxProperty(), 0), new KeyValue(backPerspective.llyProperty(), height) )); 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(perspectiveInBack, changeBrightnessBack); // return new SequentialTransition(flipOutFront, flipInBack); } public static void main(String[] args) { launch(args); } } |
Ici, j'ai créé des instances de Timeline séparées pour gérer la perspective et le changement de couleur mais, bien sûr, il est tout a fait possible de rajouter les instances de KeyFrame dans une seule et unique ligne temporelle.
Voila, c'est tout pour cette fois, lors du prochain billet, nous laisserons de coté la 2D et nous attaquerons enfin la 3D !