JavaFX Nodes versus Canvas

Last week I was working on a blog post about Azul Zulu with JavaFX support for ARM systems, like the Raspberry Pi. As you can see in this video, I found out my little test application with a lot of “bouncing balls” started losing performance on the Raspberry Pi with more than 1000 of those balls.

Of course, having that high number of visual components in a typical JavaFX user interface would be a badly designed application. Imagine a long registration form with that number of input fields and labels… it would drive your users crazy. But I still wanted to try out the same with the Canvas approach, so I extended my test application and made a second version where you can easily switch between Nodes and drawing on a Canvas to compare the differences.

Node versus Canvas

In JavaFX, both Node and Canvas are part of the scene graph but they have different use cases. The choice between the two often depends on the specific needs of your application. You use Nodes for static content like input forms, data tables, dashboards with graphs,… This is usually more convenient and efficient. The Canvas gives you more flexibility when you need to generate dynamic or custom content.

JavaFX Node

javafx.scene.Node is the base class and all visual JavaFX components extend it. That goes several “layers” deep. For instance, for a Button > ButtonBase > Labeled > Control > Region > Parent > Node.

Summarized:

These are a few typical components that extend from Node:

Label label = new Label("Hello World!");
Button button = new Button("Click Me!");

JavaFX Canvas

javafx.scene.canvas also extends Node, with special functionality. You can draw your own content on the Canvas using a set of graphics commands provided by a GraphicsContext.

Summarized:

This is an example that draws a rectangle:

Canvas canvas = new Canvas(400, 300);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.BLUE);
gc.fillRect(50, 50, 100, 70);

Demo Code

The demo application can be found in this GitHub Gist. The value at the beginning of the code defines which approach is used:

private static int TYPE_OF_TEST = 1; // 1 = Nodes, 2 = Canvas

Using Nodes

When you use Nodes, a Pane is added to the screen in which balls gets added. Each ball is a Circle Node with a move method:

class BallNode extends Circle {
    private final Color randomColor = Color.color(Math.random(), Math.random(), Math.random());
    private final int size = r.nextInt(1, 10);
    private double dx = r.nextInt(1, 5);
    private double dy = r.nextInt(1, 5);

    public BallNode() {
        this.setRadius(size / 2);
        this.setFill(randomColor);
        relocate(r.nextInt(380), r.nextInt(620));
    }

    public void move() {
        if (hitRightOrLeftEdge()) {
            dx *= -1; // Ball hit right or left wall, so reverse direction
        }
        if (hitTopOrBottom()) {
            dy *= -1; // Ball hit top or bottom, so reverse direction
        }
        setLayoutX(getLayoutX() + dx);
        setLayoutY(getLayoutY() + dy);
    }

    private boolean hitRightOrLeftEdge() {
        return (getLayoutX() < (scene.getX() + getRadius())) ||
            (getLayoutX() > (scene.getWidth() - getRadius()));
    }

    private boolean hitTopOrBottom() {
        return (getLayoutY() < (scene.getY() - getRadius())) ||
            (getLayoutY() > (scene.getHeight() - getRadius() - 60));
    }
}

Using Canvas

When you use the Canvas, each Ball is a data object, and all balls get drawn on the Canvas at every tick:

class BallDrawing {
    private final Color fill = Color.color(Math.random(), Math.random(), Math.random());
    private final int size = r.nextInt(1, 10);
    private double x = r.nextInt(APP_WIDTH);
    private double y = r.nextInt(APP_HEIGHT - TOP_OFFSET);
    private double dx = r.nextInt(1, 5);
    private double dy = r.nextInt(1, 5);

    public void move() {
        if (hitRightOrLeftEdge()) {
            dx *= -1; // Ball hit right or left wall, so reverse direction
        }
        if (hitTopOrBottom()) {
            dy *= -1; // Ball hit top or bottom, so reverse direction
        }
        x += dx;
        y += dy;
    }

    private boolean hitRightOrLeftEdge() {
        return (x < (scene.getX() + size)) ||
            (x > (scene.getWidth() - size));
    }

    private boolean hitTopOrBottom() {
        return (y < (scene.getY() - size)) ||
            (y > (scene.getHeight() - size - 60));
    }

    // Getters
}

Moving the Objects

The application uses a Timeline to add more objects, and move them, every five milliseconds:

Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5), t -> onTick()));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();

private void onTick() {
    if (TYPE_OF_TEST == 1) {
        // Add ball nodes to the pane
        for (var i = 0; i < ADD_BALLS_PER_TICK; i++) {
            paneBalls.getChildren().add(new BallNode());
        }

        // Move all the balls in the pane
        for (Node ballNode : paneBalls.getChildren()) {
            ((BallNode) ballNode).move();
        }
    } else if (TYPE_OF_TEST == 2) {
        // Add balls to the list of balls to be drawn
        for (var i = 0; i < ADD_BALLS_PER_TICK; i++) {
            ballDrawings.add(new BallDrawing());
        }
        
        // Clear the canvas (remove all the previously balls that were drawn)
        context.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight());

        // Move all the balls in the list, and draw them on the Canvas
        for (BallDrawing ballDrawing : ballDrawings) {
            ballDrawing.move();
            context.setFill(ballDrawing.getFill());
            context.fillOval(ballDrawing.getX(), ballDrawing.getY(), ballDrawing.getSize(),  ballDrawing.getSize());
        }
    } 
}

Executing the Applications

I used the following approach to run the application:

Conclusion

As you can see in the video, with this example, you can add roughly 10 times more objects to the Canvas before the framerate drops, compared to the number of Nodes. This is not a “scientific result” at all, but it gives a good impression of what can be achieved by using Canvas.