JavaFX 2.x
What I want to do:
PerspectiveTransform#time - not found in JavaFX 2.2)
What I don't want to do:
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();
        }
    }
}
