JavaFX Flip Node with PerspectiveTransform


Question

JavaFX 2.x

What I want to do:

  • This script translated into Java source code. I tried that myself, but some of that stuff is deprecated (e.g. PerspectiveTransform#time - not found in JavaFX 2.2)
  • Flipping like this and like that.

    What I don't want to do:

  • Use RotateTransition because it depends on the PerspectiveCamera. Since I'll have many flippable tiles next to each other, the front/back replacement halfway through the animation won't go well.

    What I have so far:

    import javafx.animation.KeyFrame;
    import javafx.animation.KeyValue;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Group;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.effect.PerspectiveTransform;
    import javafx.scene.effect.PerspectiveTransformBuilder;
    import javafx.scene.image.ImageView;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    /**
     * 
     * @author ggrec
     *
     */
    public class FX_Tester extends Application 
    {
    
        @Override
        public void start(final Stage stage) throws Exception 
        {
            final StackPane stackPane = new StackPane();
    
            final ImageView img1 = new ImageView("http://img3.wikia.nocookie.net/__cb20120816162009/mario/images/thumb/1/15/MarioNSMB2.png/200px-MarioNSMB2.png");
            final ImageView img2 = new ImageView("http://img2.wikia.nocookie.net/__cb20120518002849/mario/images/thumb/7/78/Tanooki_Mario_Artwork_-_Super_Mario_Bros._3.png/180px-Tanooki_Mario_Artwork_-_Super_Mario_Bros._3.png");
    
            final FlipView flipPane = new FlipView(img1, img2);
    
            stackPane.setOnMouseClicked(new EventHandler<MouseEvent>() {
    
                @Override public void handle(final MouseEvent arg0) 
                {
                    flipPane.doFlip();
                }
            });
    
            stackPane.getChildren().setAll(flipPane);
    
            stage.setScene(new Scene(stackPane));
            stage.show();
        }
    
        public static void main(final String[] args)
        {
            launch();
        }
    
        private class FlipView extends Group
        {
            private Node frontNode;
            private Node backNode;
    
            private boolean isFlipped = false;
    
            private SimpleDoubleProperty time = new SimpleDoubleProperty(Math.PI / 2);
    
            private Timeline anim = new Timeline(
    
                    new KeyFrame(Duration.ZERO, new KeyValue(time, Math.PI / 2)),
                    new KeyFrame(Duration.ONE,  new KeyValue(time, - Math.PI / 2)),
                    new KeyFrame(Duration.ONE,  new EventHandler<ActionEvent>() {
    
                        @Override public void handle(final ActionEvent arg0)
                        {
                            isFlipped = !isFlipped;
                        }
                    })
                    );
    
            private FlipView(final Node frontNode, final Node backNode)
            {
                this.frontNode = frontNode;
                this.backNode = backNode;
    
                getChildren().setAll(frontNode, backNode);
    
                frontNode.setEffect(getPT(time.doubleValue()));
                backNode.setEffect(getPT(time.doubleValue()));
    
                frontNode.visibleProperty().bind(time.greaterThan(0));
                backNode.visibleProperty().bind(time.lessThan(0));
            }
    
            private PerspectiveTransform getPT(final double t)
            {
                final double width = 200;
                final double height = 200;
                final double radius = width / 2;
                final double back = height / 10;
    
                return PerspectiveTransformBuilder.create()
                        .ulx(radius - Math.sin(t)*radius)
                        .uly(0 - Math.cos(t)*back)
                        .urx(radius + Math.sin(t)*radius)
                        .ury(0 + Math.cos(t)*back)
                        .lrx(radius + Math.sin(t)*radius)
                        .lry(height - Math.cos(t)*back)
                        .llx(radius - Math.sin(t)*radius)
                        .lly(height + Math.cos(t)*back)
                        .build();
            }
    
            public void doFlip() 
            {
                if (isFlipped)
                {
                    anim.setRate(1.0);
                    anim.setDelay(Duration.ZERO);
                }
                else
                {
                    anim.setRate(-1.0);
                    anim.setDelay(Duration.ONE);
                }
    
                anim.play();
            }
        }
    
    }
    
  • 1
    2
    2/19/2016 2:35:08 PM

    Accepted Answer

    After heavy R&D, I've managed to implement the flip functionality without PerspectiveCamera, using only PerspectiveTransform.

    If you're too lazy to run this SSCCE, then go here to see a demo on how the below code works.

    Q: But George, how is this different from the other methods???
    A: Well, first of: since you're not using PerspectiveCamera, the user's perspective won't be affected if say you have 100 flipping tiles on the screen. Second and last: The back node is ALREADY flipped. So it's not mirrored, it's not rotate, it's not scaled. It's "normal". Ain't that great?

    Cheers.


    import javafx.animation.Interpolator;
    import javafx.animation.KeyFrame;
    import javafx.animation.KeyValue;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.effect.PerspectiveTransform;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    /**
     * 
     * @author ggrec
     *
     */
    public class DFXFlipPaneTester extends Application
    {
    
        // ==================== 1. Static Fields ========================
    
        /*
         * Mmm... pie.
         */
        private static final Double PIE = Math.PI;
    
        private static final Double HALF_PIE = Math.PI / 2;
    
        private static final double ANIMATION_DURATION = 10000;
    
        private static final double ANIMATION_RATE = 10;
    
    
        // ====================== 2. Instance Fields =============================
    
        private Timeline animation;
    
        private StackPane flipPane;
    
        private SimpleDoubleProperty angle = new SimpleDoubleProperty(HALF_PIE);
    
        private PerspectiveTransform transform = new PerspectiveTransform();
    
        private SimpleBooleanProperty flippedProperty = new SimpleBooleanProperty(true);
    
    
        // ==================== 3. Static Methods ====================
    
        public static void main(final String[] args)
        {
            Application.launch(args);
        }
    
    
        // ==================== 5. Creators ====================
    
        @Override
        public void start(final Stage primaryStage) throws Exception
        {
            primaryStage.setScene(new Scene(createFlipPane()));
            primaryStage.show();
        }
    
        private StackPane createFlipPane()
        {
            angle = createAngleProperty();
    
            flipPane = new StackPane();
            flipPane.setPadding(new Insets(30));
    
            flipPane.setMinHeight(500);
            flipPane.setMinWidth(500);
    
            flipPane.getChildren().setAll(createBackNode(), createFrontNode());
    
            flipPane.widthProperty().addListener(new ChangeListener<Number>() {
    
                @Override public void changed(final ObservableValue<? extends Number> arg0, final Number arg1, final Number arg2)
                {
                    recalculateTransformation(angle.doubleValue());
                }
            });
    
            flipPane.heightProperty().addListener(new ChangeListener<Number>() {
    
                @Override public void changed(final ObservableValue<? extends Number> arg0, final Number arg1, final Number arg2)
                {
                    recalculateTransformation(angle.doubleValue());
                }
            });
    
            return flipPane;
        }
    
        private StackPane createFrontNode()
        {
            final StackPane node = new StackPane();
    
            node.setEffect(transform);
            node.visibleProperty().bind(flippedProperty);
    
            node.getChildren().setAll(createButton("Front Button")); //$NON-NLS-1$
    
            return node;
        }
    
        private StackPane createBackNode()
        {
            final StackPane node = new StackPane();
    
            node.setEffect(transform);
            node.visibleProperty().bind(flippedProperty.not());
    
            node.getChildren().setAll(createButton("Back Button")); //$NON-NLS-1$
    
            return node;
        }
    
        private Button createButton(final String text)
        {
            final Button button = new Button(text);
            button.setMaxHeight(Double.MAX_VALUE);
            button.setMaxWidth(Double.MAX_VALUE);
    
            button.setOnAction(new EventHandler<ActionEvent>() {
    
                @Override public void handle(final ActionEvent arg0)
                {
                    flip();
                }
            });
    
            return button;
        }
    
        private SimpleDoubleProperty createAngleProperty()
        {
            // --------------------- <Angle> -----------------------
    
            final SimpleDoubleProperty angle = new SimpleDoubleProperty(HALF_PIE);
    
            angle.addListener(new ChangeListener<Number>() {
    
                @Override public void changed(final ObservableValue<? extends Number> obsValue, final Number oldValue, final Number newValue)
                {
                    recalculateTransformation(newValue.doubleValue());
                }
            });
    
            return angle;
        }
    
        private Timeline createAnimation()
        {
            return new Timeline(
    
                    new KeyFrame(Duration.millis(0),    new KeyValue(angle, HALF_PIE)),
    
                    new KeyFrame(Duration.millis(ANIMATION_DURATION / 2),  new KeyValue(angle, 0, Interpolator.EASE_IN)),
    
                    new KeyFrame(Duration.millis(ANIMATION_DURATION / 2),  new EventHandler<ActionEvent>() {
    
                        @Override public void handle(final ActionEvent arg0)
                        {
                            // TODO -- Do they another way or API to do this?
                            flippedProperty.set( flippedProperty.not().get() );
                        }
                    }),
    
                    new KeyFrame(Duration.millis(ANIMATION_DURATION / 2),  new KeyValue(angle, PIE)),
    
                    new KeyFrame(Duration.millis(ANIMATION_DURATION), new KeyValue(angle, HALF_PIE, Interpolator.EASE_OUT))
    
                    );
        }
    
    
        // ==================== 6. Action Methods ====================
    
        private void flip()
        {
            if (animation == null)
                animation = createAnimation();
    
            animation.setRate( flippedProperty.get() ? ANIMATION_RATE : -ANIMATION_RATE );
    
            animation.play();
        }
    
    
        // ==================== 8. Business Methods ====================
    
        private void recalculateTransformation(final double angle)
        {
            final double insetsTop = flipPane.getInsets().getTop() * 2;
            final double insetsLeft = flipPane.getInsets().getLeft() * 2;
    
            final double radius = flipPane.widthProperty().subtract(insetsLeft).divide(2).doubleValue();
            final double height = flipPane.heightProperty().subtract(insetsTop).doubleValue();
            final double back = height / 10;
    
            /*
             * Compute transform.
             * 
             * Don't bother understanding these unless you're a math passionate.
             * 
             * You may Google "Affine Transformation - Rotation"
             */
            transform.setUlx(radius - Math.sin(angle) * radius);
            transform.setUly(0 - Math.cos(angle) * back);
            transform.setUrx(radius + Math.sin(angle) * radius);
            transform.setUry(0 + Math.cos(angle) * back);
            transform.setLrx(radius + Math.sin(angle) * radius);
            transform.setLry(height - Math.cos(angle) * back);
            transform.setLlx(radius - Math.sin(angle) * radius);
            transform.setLly(height + Math.cos(angle) * back);
        }
    
    }
    
    7
    3/20/2014 6:00:06 PM

    Oracle created a sample called DisplayShelf. It is similar to the PhotoFlip application you linked, but is implemented for Java 2+. The Oracle sample code is in the Ensemble Sample Application. You can review the DisplayShelf source in the JavaFX open source repository.

    display shelf

    The DisplayShelf is a Cover Flow style implementation of PerspectiveTransform animations, so its not exactly the same as a full image flip. But many of the principles are the same, so you should be able to study the DisplayShelf example, then develop the code which you need to fit your requirement.

    Related image flipping question for JavaFX => Flip a card animation.


    Licensed under: CC-BY-SA with attribution
    Not affiliated with: Stack Overflow
    Icon