Your Task
invokes drawTriangle()
in the background to update a Canvas
. The associated GraphicsContext
requires that “Once a Canvas
node is attached to a scene, it must be modified on the JavaFX Application Thread.” Your deeply recursive call blocks the JavaFX Application Thread, preventing a timely screen update. In contrast, your platform’s implementation of System.out.println()
may allow it to report in a timely way. The timing disparity is seen even without a Task
at all.
Happily for Canvas
, “If it is not attached to any scene, then it can be modified by any thread, as long as it is only used from one thread at a time.” One approach might be suggested in A Task Which Returns Partial Results. Create a notional Task<Image>
that updates a detached Canvas
in the background. Periodically, perhaps at each level of recursion, copy the Canvas
and publish a snapshot via updateValue()
. The enclosing Pane
can listen to the task’s value
property and update an enclosed Canvas
via drawImage()
without blocking the JavaFX Application Thread.
Sadly, snapshot “Throws IllegalStateException
if this method is called on a thread other than the JavaFX Application Thread.”
In the alternative shown below, CanvasTask
extends Task<Canvas>
and publishes a new Canvas
on each iteration of a loop. The enclosing CanvasTaskTest
listens to the value
property and replaces the previous Canvas
each time a new one arrives. The example below displays a series of fractal trees of increasing depth and the time needed to compose each. Note that in a GraphicsContext
, “Each call pushes the necessary parameters onto the buffer where they will be later rendered onto the image of the Canvas
node by the rendering thread at the end of a pulse.” This allows JavaFX to leverage a platform’s rendering pipeline, but it may impose an additional overhead for a large number of strokes. In practice, tens of thousands of strokes slow rendering imperceptibly, while millions of overlapping strokes may be superfluous.
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* @see https://stackoverflow.com/a/44056730/230513
*/
public class CanvasTaskTest extends Application {
private static final int W = 800;
private static final int H = 600;
@Override
public void start(Stage stage) {
stage.setTitle("CanvasTaskTest");
StackPane root = new StackPane();
Canvas canvas = new Canvas(W, H);
root.getChildren().add(canvas);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
CanvasTask task = new CanvasTask();
task.valueProperty().addListener((ObservableValue<? extends Canvas> observable, Canvas oldValue, Canvas newValue) -> {
root.getChildren().remove(oldValue);
root.getChildren().add(newValue);
});
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
private static class CanvasTask extends Task<Canvas> {
private int strokeCount;
@Override
protected Canvas call() throws Exception {
Canvas canvas = null;
for (int i = 1; i < 15; i++) {
canvas = new Canvas(W, H);
GraphicsContext gc = canvas.getGraphicsContext2D();
strokeCount = 0;
long start = System.nanoTime();
drawTree(gc, W / 2, H - 50, -Math.PI / 2, i);
double dt = (System.nanoTime() - start) / 1_000d;
gc.fillText("Depth: " + i
+ "; Strokes: " + strokeCount
+ "; Time : " + String.format("%1$07.1f", dt) + " µs", 8, H - 8);
Thread.sleep(200); // simulate rendering latency
updateValue(canvas);
}
return canvas;
}
private void drawTree(GraphicsContext gc, int x1, int y1, double angle, int depth) {
if (depth == 0) {
return;
}
int x2 = x1 + (int) (Math.cos(angle) * depth * 5);
int y2 = y1 + (int) (Math.sin(angle) * depth * 5);
gc.strokeLine(x1, y1, x2, y2);
strokeCount++;
drawTree(gc, x2, y2, angle - Math.PI / 8, depth - 1);
drawTree(gc, x2, y2, angle + Math.PI / 8, depth - 1);
}
}
public static void main(String[] args) {
launch(args);
}
}