11

I made a small demo to exemplify the problem I am having. A walkthrough on its use:

1 - Click on the screen four times in order to create the grid.

2 - Click on the button at the bottom to give the created grid the perspective effect.

3 - Click on the screen again to draw a circle.

enter image description here

My problem is that I don't know what transformation I should apply on the Circle node in order to have the same visual effect as the grid at the exact location where the user clicked.

The demo at this point only draws a circle with no transformations.

The desired output is as it follows (I cheated here using an Ellipse instead of Circle - this cannot be my solution because the user inputs the four coordinates of the grid):

enter image description here

Here's the code for the demo.

Main.java

package application;

import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(getClass().getResource("View.fxml"));
            Scene scene = new Scene(loader.load(), 600, 600);
            primaryStage.setScene(scene);
            primaryStage.setTitle("Perspective Transformation");
            primaryStage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

View.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>

<VBox fx:id="vBox" maxHeight="600.0" maxWidth="600.0" minHeight="600.0" minWidth="600.0" prefHeight="600.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
   <children>
      <AnchorPane fx:id="anchorPane" maxHeight="575.0" minHeight="575.0" prefHeight="575.0" prefWidth="600.0" />
      <Button fx:id="button" alignment="CENTER" mnemonicParsing="false" prefHeight="25.0" prefWidth="600.0" text="Perspective Transformation" />
   </children>
</VBox>

Controller.java

package application;

import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;

public class Controller {
    @FXML
    VBox vBox;
    @FXML
    AnchorPane anchorPane;
    @FXML
    Button button;

    Line line_1, line_2, line_3, line_4, middle_1, middle_2;

    private int numberOfClicks = 0;
    private int W = 10;
    //Store coordinates of the corresponding clicks
    private double cx1, cy1,
                   cx2, cy2,
                   cx3, cy3,
                   cx4, cy4,
                   cx5, cy5;

    @FXML
    private void initialize() {
        button.setDisable(true);

        button.setOnAction((event) -> {

            //Create a standard grid
            Line standard_1 = new Line(0, 0, 0, 600);
            Line standard_2 = new Line(0, 600, 600, 600);
            Line standard_3 = new Line(600, 600, 600, 0);
            Line standard_4 = new Line(600, 0, 0, 0);
            //Middle ones
            Line standard_5 = new Line(300, 0, 300, 600);
            Line standard_6 = new Line(0, 300, 600, 300);

            standard_1.setStrokeWidth(W);
            standard_2.setStrokeWidth(W);
            standard_3.setStrokeWidth(W);
            standard_4.setStrokeWidth(W);
            standard_5.setStrokeWidth(W);
            standard_6.setStrokeWidth(W);

            anchorPane.getChildren().clear();

            Pane perspectivePane = new Pane();

            perspectivePane.getChildren().addAll(standard_1, standard_2, standard_3,
                                            standard_4, standard_5, standard_6);

            PerspectiveTransform pt = new PerspectiveTransform();

            pt.setUlx(cx1);
            pt.setUly(cy1);
            pt.setUrx(cx2);
            pt.setUry(cy2);
            pt.setLrx(cx3);
            pt.setLry(cy3);
            pt.setLlx(cx4);
            pt.setLly(cy4);

            perspectivePane.setEffect(pt);

            anchorPane.getChildren().add(perspectivePane);

        });

        anchorPane.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent event) {
                numberOfClicks++;

                if(numberOfClicks == 1){
                    cx1 = event.getX();
                    cy1 = event.getY();
                }
                else if(numberOfClicks == 2){
                    cx2 = event.getX();
                    cy2 = event.getY();
                    line_1 = new Line(cx1, cy1, cx2, cy2);
                    line_1.setStrokeWidth(W);
                    anchorPane.getChildren().add(line_1);
                }
                else if(numberOfClicks == 3){
                    cx3 = event.getX();
                    cy3 = event.getY();
                    line_2 = new Line(cx2, cy2, cx3, cy3);
                    line_2.setStrokeWidth(W);
                    anchorPane.getChildren().add(line_2);
                }
                else if(numberOfClicks == 4){
                    cx4 = event.getX();
                    cy4 = event.getY();
                    line_3 = new Line(cx3, cy3, cx4, cy4);
                    line_4 = new Line(cx4, cy4, cx1, cy1);
                    line_3.setStrokeWidth(W);
                    line_4.setStrokeWidth(W);
                    middle_1 = new Line((cx1+cx2)/2, (cy1+cy2)/2, (cx3+cx4)/2, (cy3+cy4)/2);
                    middle_2 = new Line((cx2+cx3)/2, (cy2+cy3)/2, (cx4+cx1)/2, (cy4+cy1)/2);
                    middle_1.setStrokeWidth(W);
                    middle_2.setStrokeWidth(W);
                    anchorPane.getChildren().addAll(line_3, line_4, middle_1, middle_2);
                    button.setDisable(false);
                }
                else if(numberOfClicks == 5){
                    cx5 = event.getX();
                    cy5 = event.getY();
                    Circle circle = new Circle();
                    circle.setCenterX(cx5);
                    circle.setCenterY(cy5);
                    circle.setRadius(30);
                    circle.setFill(Color.TRANSPARENT);
                    circle.setStroke(Color.BLACK);
                    circle.setStrokeWidth(W);

                    //PerspectiveTransform pt = new PerspectiveTransform();

                    //What transformation should I apply to the circle?

                    //circle.setEffect(pt);
                    anchorPane.getChildren().add(circle);
                }
                else {
                    anchorPane.getChildren().clear();
                    numberOfClicks = 0;
                }
            }
        });
    }
}
ihavenoidea
  • 629
  • 1
  • 7
  • 26
  • 1
    Did you try to use the same perspective transform as you did for for the `perspectivePane`? Or did you try to add the circle to the `perspectivePane`? Also a minor hint for the next time you are asking a question: Try to minimize your code example. Also maybe provide your UI as code instead of a ".fxml" so I can just copy and paste your code as a whole without the need of creating multiple files and making sure every file is in the correct folder. – Markus Köbele Feb 21 '18 at 12:54
  • Thanks for the reply and for the tips @MarkusK, I will surely keep them in mind next time! As for your suggestions, if I use the same transformation (on the Circle node) as I did for the perspectivePane, I get [this](https://imgur.com/a/gzXQo) and if I add the Circle node to the perspectivePane I get [this](https://imgur.com/a/yWg9V). The "X" is where I clicked. – ihavenoidea Feb 21 '18 at 17:19
  • I think you have some funky stuff going on in your `Button` handler. – SedJ601 Feb 22 '18 at 22:50

1 Answers1

2

I don't know for sure if this is what you wanted, but I used Group to accomplish this. I followed the code from here. Test this update. I will try explaining it tomorrow if it works.

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Group;
import javafx.scene.control.Button;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;

/**
 *
 * @author blj0011
 */
public class FXMLDocumentController implements Initializable
{

    @FXML
    VBox vBox;
    @FXML
    AnchorPane anchorPane;
    @FXML
    Button button;

    Group group = new Group();
    Group group2 = new Group();

    PerspectiveTransform pt = new PerspectiveTransform();

    Line line_1, line_2, line_3, line_4, middle_1, middle_2;

    private int numberOfClicks = 0;
    private int W = 10;
    //Store coordinates of the corresponding clicks
    private double cx1, cy1,
            cx2, cy2,
            cx3, cy3,
            cx4, cy4,
            cx5, cy5;

    @Override
    public void initialize(URL location, ResourceBundle resources)
    {
        button.setDisable(true);

        anchorPane.getChildren().addAll(group);

        button.setOnAction((event) -> {


            pt.setUlx(cx1);
            pt.setUly(cy1);
            pt.setUrx(cx2);
            pt.setUry(cy2);
            pt.setLrx(cx3);
            pt.setLry(cy3);
            pt.setLlx(cx4);
            pt.setLly(cy4);

            group.setEffect(pt);
        });

        anchorPane.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> {
            numberOfClicks++;

            switch (numberOfClicks) {
                case 1:
                    cx1 = event.getX();
                    cy1 = event.getY();
                    System.out.println(cx1 + " : " + cy1);
                    break;
                case 2:
                    cx2 = event.getX();
                    cy2 = event.getY();
                    line_1 = new Line(cx1, cy1, cx2, cy2);
                    line_1.setStrokeWidth(W);
                    group.getChildren().add(line_1);
                    break;
                case 3:
                    cx3 = event.getX();
                    cy3 = event.getY();
                    line_2 = new Line(cx2, cy2, cx3, cy3);
                    line_2.setStrokeWidth(W);
                    group.getChildren().add(line_2);
                    break;
                case 4:


                    cx4 = event.getX();
                    cy4 = event.getY();



                    line_3 = new Line(cx3, cy3, cx4, cy4);
                    line_4 = new Line(cx4, cy4, cx1, cy1);
                    line_3.setStrokeWidth(W);
                    line_4.setStrokeWidth(W);
                    middle_1 = new Line((cx1 + cx2) / 2, (cy1 + cy2) / 2, (cx3 + cx4) / 2, (cy3 + cy4) / 2);
                    middle_2 = new Line((cx2 + cx3) / 2, (cy2 + cy3) / 2, (cx4 + cx1) / 2, (cy4 + cy1) / 2);
                    middle_1.setStrokeWidth(W);
                    middle_2.setStrokeWidth(W);
                    group.getChildren().addAll(line_3, line_4, middle_1, middle_2);
                    button.setDisable(false);
                    break;
                case 5:
                    List<Double> centerOfTransform = findCenterOfTransForm(pt);

                    cx5 = event.getX();
                    cy5 = event.getY();
                    List<Double> list = new ArrayList();
                    list.add(cx5);
                    list.add(cy5);
                    List<Double> changeInCenterXAndCenterY = findChangeInCenterXAndY(centerOfTransform, list);


                    PerspectiveTransform ptCircle = new PerspectiveTransform();
                    ptCircle.setUlx(cx1 + changeInCenterXAndCenterY.get(0));
                    ptCircle.setUly(cy1 + changeInCenterXAndCenterY.get(1));
                    ptCircle.setUrx(cx2 + changeInCenterXAndCenterY.get(0));
                    ptCircle.setUry(cy2 + changeInCenterXAndCenterY.get(1));
                    ptCircle.setLrx(cx3 + changeInCenterXAndCenterY.get(0));
                    ptCircle.setLry(cy3 + changeInCenterXAndCenterY.get(1));
                    ptCircle.setLlx(cx4 + changeInCenterXAndCenterY.get(0));
                    ptCircle.setLly(cy4 + changeInCenterXAndCenterY.get(1));




                    System.out.println("cx5: " + cx5 + "  cy5: " + cy5);
                    Circle circle = new Circle();

                    circle.setRadius(30);
                    circle.setFill(Color.TRANSPARENT);
                    circle.setStroke(Color.BLACK);
                    circle.setStrokeWidth(W);


                     circle.setCenterX(cx5);
                    circle.setCenterY(cy5);
                    circle.setEffect(ptCircle);
                    anchorPane.getChildren().add(circle);
                    System.out.println("centerx: " + circle.getTranslateX()+ "  centery: " + circle.getTranslateY());
                    break;
                default:
                    group.getChildren().clear();
                    numberOfClicks = 0;
                    break;
            }
        });
    }

     List<Double> findCenterOfTransForm(PerspectiveTransform pt)
    {
        List<Double> tempList = new ArrayList();
        tempList.add((pt.getUlx() + pt.getUrx() + pt.getLrx() + pt.getLlx()) / 4);
        tempList.add((pt.getUly() + pt.getUry() + pt.getLry() + pt.getLly()) / 4);

        return tempList;
    }

     List<Double> findChangeInCenterXAndY(List<Double> originalXAndY, List<Double> newXAndY)
     {
         List<Double> tempList = new ArrayList();
         tempList.add(newXAndY.get(0) - originalXAndY.get(0));
         tempList.add(newXAndY.get(1) - originalXAndY.get(1));

         return tempList;
     }


}
SedJ601
  • 12,173
  • 3
  • 41
  • 59
  • Thanks for the answer! However, the circle is supposed to be created after the application of the effect on the grid. I editted the original question to make more understandable, I'm sorry about the inconvenience. In the comment section of the question I discussed some approaches I attempted. – ihavenoidea Feb 23 '18 at 04:39
  • 1
    This should work as long as no edge line crosses another edge line. – SedJ601 Feb 23 '18 at 06:04
  • Since I'm unfamiliar with your code structure (using implements Initializable and such) I just embedded your initialize() code into mine and some minor changes and your solution here gave [this result](https://media.giphy.com/media/rDOdhNDTs2T5rlmTod/giphy.gif). It's really close to what I need but it looks like the size of the circle is transformed in perspective according to the root pane. Also note that your example changes the grid coordinates after transformation when it shouldn't (although I'm not sure if this would change anything). I'm using your code for some attempts here, thanks! – ihavenoidea Feb 24 '18 at 02:00
  • Added the desired output in the question. – ihavenoidea Feb 24 '18 at 02:29
  • 1
    Prospective transformation is not achievable with mere rotation, shear and translation that affine framework can give you. To do what you need you would have to venture into 3D. Why not render your circle and rectangle into an offscreen texture and then put it on perspective transformed quad? See https://docs.oracle.com/javase/8/javafx/api/javafx/scene/shape/TriangleMesh.html – Andrew Butenko Feb 27 '18 at 02:11
  • @AndrewButenko Thanks for the info, I will take a look on it. Is it possible to get mouse clicking event on a TriangleMesh? In order to draw the circle on the clicked coordinate on the perspective surface. – ihavenoidea Feb 27 '18 at 02:50
  • Sure, MeshView is a regular javafx node and supports mouse events just like any other node. It might be tricky to convert from your view coordinates into texture coordinates of your quad. – Andrew Butenko Feb 27 '18 at 04:07
  • 1
    Maybe, I found [this answer](https://stackoverflow.com/a/48655808/3585576) promising, will give it a try. I think dealing with Perspective Camera that will be a bit challenging, to translate/rotate the Rectangle to the corresponding user input coordinates. – ihavenoidea Feb 27 '18 at 04:53