JavaFX: Deformation

Proceeding with my article series investigating the use of JavaFX for OpenPatrician, I have come to the deformation part. In this first part I want to investigate JavaFX potential to deform a rectangle into a quadrilateral. This is a non-affine transformation as the parallelism is not maintained. Usually this comes into place if you have a perspective view and have to map a rectangle.

Fortunately JavaFX provides an effect for exactly that: The PerspectiveTransform.

JavaFX_PerspectiveTransformationRectangle

The red square defines the rectangle that is to be deformed and is basically defined by the lower right corner (origin), the lower left corner for the width and the upper left corner for the height. The violet area is the rectangle after the applied transformation. This is a special case where one corner of the original rectangle matches with the resulting position, but it can easily be reasoned, that any rectangle can be translated to match this case.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package ch.sahits.javafx.test.stretch;

import ch.sahits.javafx.test.ResizeableCanvas;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.effect.PerspectiveTransformBuilder;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.RectangleBuilder;
import javafx.stage.Stage;

public class PerpectiveTransformEffectTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            Group root = new Group();
            Image img = new Image(getClass().getResource("PaperScroll.png").openStream());
            final double width = img.getWidth();
            final double height = img.getHeight();
            Scene scene = new Scene(root, width, height);
            ImageView imageView = new ImageView(img);

            RectangleBuilder rectBuilder = RectangleBuilder.create()
                    .translateX(366)
                    .translateY(29)
                    .opacity(0.5)
                    .fill(Color.RED)
                    .width(559)
                    .height(502);
            Rectangle untransformed = rectBuilder.build();
            Rectangle transformed = rectBuilder.build();
            transformed.setFill(Color.BLUEVIOLET);

            PerspectiveTransform perspectiveTransform = PerspectiveTransformBuilder.create()
                    .ulx(200).uly(0)
                    .urx(544).ury(19)
                    .llx(0).lly(403)
                    .lrx(559).lry(502)
                    .build();

            transformed.setEffect(perspectiveTransform);

            root.getChildren().addAll(imageView, untransformed, transformed);     
            primaryStage.setTitle("Perspective Transform");
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (IOException ex) {
            Logger.getLogger(PerpectiveTransformEffectTest.class.getName()).log(Level.SEVERE, null, ex);
        }    }

    public static void main(String[] args) {
        launch(args);
    }
}

Some comments on the code:

  • The root element is a Group compared to the StackPane from previous examples. This is to ensure absolute positioning of the children in the root node. If a Stack pane would have been used, the elements not filling the whole scene (untransformed and transformed) would be centered and then translated.
  • There are cases where it makes a difference where the effect is applied on the node itself or a parent group.

As you might imagine, this transformation of rectangle is only the fore play to the real thing, as I intend to write on the paper scroll. And here we run into some drawbacks:

  • The PerspectiveTransform only transforms the visual representation. This means that if I have a link placed in the top left of the red rectangle, so that it is not overlapping with the violet shape, I cannot click on the transformed link, as it’s bound are still on the old place.
  • Playing around with texts produces funny results:

As it seems this means that the easiest way to transform text would be to:

This is basis wJavaFX_two_lines_not_deformedith two lines of text without the perspective transformation applied. Both labels have font size 24px and are added together with a transparent rectangle to a group, which is then added to the root:

 

            Label line1 = LabelBuilder.create()
                    .text("First Line")
                    .style("-fx-font-size: 24px")
                    .translateX(10)
                    .translateY(20)
                    //.effect(perspectiveTransform)
                    .build();

            Label line2 = LabelBuilder.create()
                    .text("Second Line")
                    .style("-fx-font-size: 24px")
                    .translateX(10)
                    .translateY(50)
                    //.effect(perspectiveTransform)
                    .build();

            Rectangle transparent = rectBuilder.build();
            transparent.setOpacity(0);

            Group text = GroupBuilder.create()
                    .translateX(366)
                    .translateY(29)
                    .children(transparent, line1, line2)
                    //.effect(perspectiveTransform)
                    .build();

            transformed.setEffect(perspectiveTransform);

            root.getChildren().addAll(imageView, untransformed, transformed, text);

JavaFX_two_line_deformedWhen applying the transformation by uncommenting the effect line in the Group builder, you get this. The size of the resulting font is obviously not correct.
JavaFX_oneline_deformed_without_groupIf the first line is directly added to the group with applied transformation. Pay attention that the translation must also add the one that was previously applied to the group.

 

 

 

           Label line1 = LabelBuilder.create()
                    .text("First Line")
                    .style("-fx-font-size: 24px")
                    .translateX(366+10)
                    .translateY(29+20)
                    .effect(perspectiveTransform)
                    .build();

            transformed.setEffect(perspectiveTransform);

            root.getChildren().addAll(imageView, untransformed, transformed, line1);

Even uglier when doing the second line in the same way.JavaFX_twolines_deformed_without_group

 

 

 

 

JavaFX_twolines_deformed_without_transparent_rectangleThe sole way to get a bit a better result is back to the first solution and removing the transparent rectangle.

 

 

 

 

 

            
Label line1 = LabelBuilder.create()
                    .text("First Line")
                    .style("-fx-font-size: 24px")
                    .translateX(10)
                    .translateY(20)
                    //.effect(perspectiveTransform)
                    .build();

            Label line2 = LabelBuilder.create()
                    .text("Second Line")
                    .style("-fx-font-size: 24px")
                    .translateX(10)
                    .translateY(50)
                    //.effect(perspectiveTransform)
                    .build();

            Group text = GroupBuilder.create()
                    .translateX(366)
                    .translateY(29)
                    .children(line1, line2)
                    .effect(perspectiveTransform)
                    .build();

            transformed.setEffect(perspectiveTransform);

            root.getChildren().addAll(imageView, untransformed, transformed, text);

It seems as the size of the group is defined by the elements in the group and the transformation is applied only on that. That is the reason, why the rectangle is present.

 

  • Create a scene of the size of the rectangle off screen
  • Write in the text that is needed
  • Create an Image out of the scene using its snapshot functionality
  • Place the image in the original scene and deform it

The more troublesome issue is that I need a way to apply the Transformation on a polygon and then use the transformed values as the clickable areas.

 

 

2 Gedanken zu „JavaFX: Deformation“

Schreibe einen Kommentar