Skip to content

Instantly share code, notes, and snippets.

@FDelporte
Created July 22, 2024 07:51
Show Gist options
  • Save FDelporte/c74cdf59ecd9ef1b14df86e08faa0c56 to your computer and use it in GitHub Desktop.
Save FDelporte/c74cdf59ecd9ef1b14df86e08faa0c56 to your computer and use it in GitHub Desktop.
Test script to compare performance of many moving Nodes versus drawing on the Canvas
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.openjfx:javafx-controls:22.0.2
//DEPS org.openjfx:javafx-graphics:22.0.2:${os.detected.jfxname}
import javafx.animation.AnimationTimer;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* To execute:
* - Install a Java runtime with JavaFX, eg. from https://www.azul.com/downloads/?package=jdk-fx#zulu
* - Install J'BANG! from https://www.jbang.dev/
* - Start it with `jbang FxNodesVersusCanvas.java`
*/
public class FxNodesVersusCanvas extends Application {
private static int TYPE_OF_TEST = 2; // 1 = Nodes, 2 = Canvas
private static int ADD_BALLS_PER_TICK = 10;
private static int APP_WIDTH = 600;
private static int APP_HEIGHT = 800;
private static int TOP_OFFSET = 80;
private Random r = new Random();
private Scene scene;
private long lastUpdate = 0;
// For the Node demo
private Pane paneBalls = new Pane();
// For the Canvas demo
private List<BallDrawing> ballDrawings = new ArrayList<>();;
private Canvas canvas = new Canvas();
private GraphicsContext context;
private Label lblBallCount = new Label();
@Override
public void start(Stage stage) {
var javaVersion = System.getProperty("java.version");
var javaVendor = System.getProperty("java.vendor");
var javaVendorVersion = System.getProperty("java.vm.version");
var javafxVersion = System.getProperty("javafx.version");
VBox holder = new VBox();
scene = new Scene(holder, APP_WIDTH, APP_HEIGHT);
holder.setFillWidth(true);
holder.setAlignment(Pos.TOP_CENTER);
holder.setSpacing(5);
holder.getChildren().addAll(
new Label("Java: " + javaVersion + ", " + javaVendor + ", " + javaVendorVersion),
new Label("JavaFX: " + javafxVersion),
lblBallCount,
getFramerateLabel(),
getDemo()
);
stage.setTitle("JavaFX Demo, Nodes versus Canvas");
stage.setScene(scene);
stage.show();
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5), t -> onTick()));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
public static void main(String[] args) {
launch();
}
private Node getDemo() {
if (TYPE_OF_TEST == 1) {
// Nodes
paneBalls.setPrefWidth(Double.MAX_VALUE);
paneBalls.setPrefHeight(Double.MAX_VALUE);
paneBalls.setMinHeight(100.0);
return paneBalls;
} else if (TYPE_OF_TEST == 2) {
// Canvas
context = canvas.getGraphicsContext2D();
canvas.widthProperty().bind(scene.widthProperty());
canvas.heightProperty().bind(scene.heightProperty().subtract(TOP_OFFSET));
return canvas;
} else {
return new Label("Set type of test to 1 or 2!");
}
}
private void onTick() {
if (TYPE_OF_TEST == 1) {
// Nodes
// Add ball nodes to the pane
for (var i = 0; i < ADD_BALLS_PER_TICK; i++) {
paneBalls.getChildren().add(new BallNode());
}
// Update the counter
lblBallCount.setText("Number of balls: " + paneBalls.getChildren().size());
// Move all the balls in the pane
for (Node ballNode : paneBalls.getChildren()) {
((BallNode) ballNode).move();
}
} else if (TYPE_OF_TEST == 2) {
// Canvas
// Add balls to the list of balls to be drawn
for (var i = 0; i < ADD_BALLS_PER_TICK; i++) {
ballDrawings.add(new BallDrawing());
}
// Update the counter
lblBallCount.setText("Number of balls: " + ballDrawings.size());
// 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());
}
}
}
private Label getFramerateLabel() {
Label lbl = new Label();
AnimationTimer frameRateMeter = new AnimationTimer() {
@Override
public void handle(long now) {
if (lastUpdate > 0) {
long nanosElapsed = now - lastUpdate;
double frameRate = 1_000_000_000.0 / nanosElapsed;
lbl.setText(String.format("Current frame rate: %d", Math.round(frameRate)));
}
lastUpdate = now;
}
};
frameRateMeter.start();
return lbl;
}
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));
}
}
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));
}
public Color getFill() {
return fill;
}
public int getSize() {
return size;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment