JavaFX Canvas.setScaleX/Y(2) not scaling to twice the size

Viewed 82

Maybe I'm misunderstanding something basic, but I'm experimenting with JavaFX and am baffled why scaling a Canvas (using .setScaleX/Y) with value of 2 doesn't result in canvas with two times bigger width/height.

The relevant code is this: (I'm not using any .fxml at this point)

public class Main extends Application {

    @Override
    public void start(Stage primaryStage){
        AnchorPane pane = new AnchorPane();
        Canvas canvas = new Canvas();
        canvas.setWidth(100);
        canvas.setHeight(100);
        canvas.getGraphicsContext2D().setFill(Color.BLUE);
        canvas.getGraphicsContext2D().fillRect(0,0,100,100);
        canvas.setScaleX(2);
        canvas.setScaleY(2);
        pane.getChildren().add(canvas);
        primaryStage.setScene(new Scene(pane, 200, 200));
        primaryStage.show();
    }


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

This results in the canvas taking about 2/3 (in terms of width/heighth, not area) of the window. Using scale of 3 makes it fill the whole window (bar one pixel gap at the edge) and I don't understand why. I looked at the documentation but it seems like scaling factor of 2 should result in two times the dimensions.

EDIT: So per suggestion below, translating the canvas by half it's width/height fixes the issue. But so does centering it in a StackPane, so I don't understand why the AnchorPane won't anchor it by it's top/left edge if it really is 200x200 after scaling, it's as if it was anchoring it by it's 100x100 edges. But if the Canvas is 100x100 then how is it drawing outside of those bounds if simply centered in StackPane?

EDIT2: With some experimenting I think I confirmed that the AnchorPane is positioning the Canvas "as if" it was still 100x100. So by setting the top and left anchors to 50 I can get the same results as translating by 50 or as if centering it in StackPane. But that means that effectively the Canvas is "drawing" outside of it's "edges". Surely this can't be right?

2 Answers

The information added in your edits in the original post give most of the answer to the question.

First note that scaleX and scaleY scale a node around its center. From the documentation: "Defines the factor by which coordinates are scaled about the center of the object along the X axis of this Node."

Second, note that transformations applied to a node are not taken into account in the layout calculations for that node. From the layout documentation (See the section "Visual Bounds vs Layout Bounds"):

So for example ... if a ScaleTransition is used to pulse the size of a button, that pulse animation will not disturb layout around that button. If an application wishes to have the effect, clip, or transform factored into the layout of a node, it should wrap that node in a Group.

The last sentence in that quote from the documentation refers to the fact that the layout bounds of a Group includes any transformations applied to the group's children (but not to the group itself).

To visualize what is happening, start with a 100x100 canvas. The layout ignores the scaling, so you can visualize this as anchoring the canvas to the top left corner of the anchor pane before applying the scaling. Then the canvas is scaled about its center, pushing all four corners 50 pixels away from the center in both dimensions. Consequently the bounds of the canvas in the anchor pane's coordinate system are from (-50, -50) to (150, 150). You can verify this by adding

System.out.println(canvas.getBoundsInParent());

after the stage is shown.

There are two solutions. One is to wrap the canvas in a Group. The layout bounds of the Group will account for the scaling on the canvas:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage){
        AnchorPane pane = new AnchorPane();
        Canvas canvas = new Canvas();
        canvas.setWidth(100);
        canvas.setHeight(100);
        canvas.getGraphicsContext2D().setFill(Color.BLUE);
        canvas.getGraphicsContext2D().fillRect(0,0,100,100);
        canvas.setScaleX(2);
        canvas.setScaleY(2);
        
        Group group = new Group(canvas);
        AnchorPane.setTopAnchor(group, 0.0);
        AnchorPane.setLeftAnchor(group, 0.0);
        
        pane.getChildren().add(group);
        primaryStage.setScene(new Scene(pane, 200, 200));
        primaryStage.show();
    }


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

The other solution is to apply a transformation which scales the canvas about its top-left corner:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage){
        AnchorPane pane = new AnchorPane();
        Canvas canvas = new Canvas();
        canvas.setWidth(100);
        canvas.setHeight(100);
        canvas.getGraphicsContext2D().setFill(Color.BLUE);
        canvas.getGraphicsContext2D().fillRect(0,0,100,100);

        Scale scale = new Scale(2,2);
        scale.setPivotX(0);
        scale.setPivotY(0);
        canvas.getTransforms().add(scale);

        pane.getChildren().add(canvas);
        primaryStage.setScene(new Scene(pane, 200, 200));
        primaryStage.show();
    }


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

You've already added canvas to the pane, try to apply .setScaleX/Y before pane.getChildren().add(canvas).

Related