© Stephen Chin, Johan Vos and James Weaver 2019
S. Chin et al.The Definitive Guide to Modern Java Clients with JavaFXhttps://doi.org/10.1007/978-1-4842-4926-0_6

6. High-Performance Graphics

Stephen Chin1 , Johan Vos2 and James Weaver3
(1)
Belmont, CA, USA
(2)
Leuven, Belgium
(3)
Marion, IN, USA
 

Written by William Antônio Siqueira

JavaFX is a complete platform for creating rich user interfaces. It has a complete set of controls for use and allows developers to style their applications using CSS. Among all the controls provided by JavaFX, we have the powerful Canvas. Using Canvas, we can create visually impressive graphics applications that make use of JavaFX hardware acceleration graphics. In this chapter, we will explore Canvas capabilities to create dynamic graphics applications using known algorithms and techniques.

Imagine that you were given the task of creating a JavaFX game. You may achieve it using the standard control APIs, but controls are not suitable for it. The same applies if you have to build a simulation or some other kind of application that requires continuous update of the screen. For such cases, we usually use Canvas.

The Canvas from JavaFX API resembles ones from other platforms and programming languages, and since it is Java, we can port it to mobile and embedded devices and make use of JavaFX hardware acceleration. Another great advantage of being part of a Java library is that we could use the infinite number of APIs available to retrieve information that later could be displayed in a canvas, for example, to access a remote service or a database to retrieve data that can be displayed in a unique way using Canvas.

Just like a Button or a Label, javafx.scene.canvas.Canvas is a subclass of Node, which means that it can be added to the JavaFX scene graph and have transformation, event listeners, and effects applied to it. To use Canvas, however, we need another class, GraphicsContext, where all the magic happens. From GraphicsContext, we have access to all the methods to draw on canvas to build our application. Currently, JavaFX only supports a 2D graphics context, but that’s enough to create high-performance graphics.

Using Canvas

To get started with canvas, let’s draw a few simple geometric forms and a text. In Listing 6-1, you can see a small application that makes use of GraphicsContext to draw simple forms and a text.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
public class HelloCanvas extends Application {
        private static final String MSG = "JavaFX Rocks!";
        private static final int WIDTH = 800;
        private static final int HEIGHT = 600;
        public static void main(String[] args) {
                launch();
        }
        @Override
        public void start(Stage stage) throws Exception {
                Canvas canvas = new Canvas(800, 600);
                GraphicsContext gc = canvas.getGraphicsContext2D();
                gc.setFill(Color.WHITESMOKE);
                gc.fillRect(0, 0, WIDTH, HEIGHT);
                gc.setFill(Color.DARKBLUE);
                gc.fillRoundRect(100, 200, WIDTH - 200, 180, 90, 90);
                gc.setTextAlign(TextAlignment.CENTER);
                gc.setFont(Font.font(60));
                gc.setFill(Color.LIGHTBLUE);
                gc.fillText(MSG, WIDTH / 2, HEIGHT / 2);
                gc.setStroke(Color.BLUE);
                gc.strokeText(MSG, WIDTH / 2, HEIGHT / 2);
                stage.setScene(new Scene(new StackPane(canvas), WIDTH, HEIGHT));
                stage.setTitle("Hello Canvas");
                stage.show();
        }
}
Listing 6-1

Hello Canvas application

As already mentioned, the javafx.scene.canvas.GraphicsContext class is used to instruct what will be drawn in the canvas, and with it we can fill a geometric form using, for example, fillRect and fillOval. To select the color that will be used to fill the geometric forms, we use the method setFill, which accepts objects of type Paint. Color is a subclass of Paint, and it has built-in colors for our use, so we don’t have to select the actual color red, green, and blue values. We can pick some of the available colors. Like setFill, we also have the possibility to stroke geometric forms and text using methods like strokeRect and strokeOval, and to set the stroke color, we can use setStroke. Changing the stroke and the fill is like using a brush with a color palette, where you have to first paint the brush with the desired color before making the actual drawing or painting. The result of this application is shown in Figure 6-1.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig1_HTML.jpg
Figure 6-1

A simple Canvas application that draws a rectangle and a text

When we draw something, we must also provide its x and y position, which is similar to what is done when we have to trace functions in a Cartesian coordinate system. It is important to get familiar with how canvas sees x and y positions to be able to correctly write forms, and it basically starts considering y from the upper-left corner. For x, it is the same; however, a higher y means that the element you are drawing will be close to the bottom of the application. With the code from Listing 6-2, we can generate the application from Figure 6-2 that shows various x,y coordinates in a canvas. Inside a nested for loop, we stroke rectangles and also paint small ovals with texts to display each x, y point. Notice how we have to change the fill to white before drawing the text and then we select the color red to draw the oval.
Canvas canvas = new Canvas(WIDTH, HEIGHT);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFont(Font.font(12));
gc.setFill(Color.BLACK);
gc.fillRect(0, 0, WIDTH, HEIGHT);
gc.setStroke(Color.LIGHTGRAY);
for (int i = 0; i < WIDTH; i+=RECT_S) {
        for (int j = 0; j < HEIGHT; j+=RECT_S) {
                gc.strokeRect(i, j, RECT_S, RECT_S);
                gc.setFill(Color.WHITE);
                gc.fillText("x=" + i + ",y=" + j, i + 2, j + 12);
                gc.setFill(Color.RED);
                gc.fillOval(i - 4, j - 4, 8, 8);
        }
}
Listing 6-2

Drawing x,y coordinates

../images/468104_1_En_6_Chapter/468104_1_En_6_Fig2_HTML.jpg
Figure 6-2

This application shows how x and y positions work in a JavaFX canvas

Using event handling and Canvas draw capabilities, we can alter the way that graphics are created. For example, allow the user to freely draw in a canvas as you can see in Listing 6-3, where we register a listener for mouse pressed and we start drawing a path. Then on onMouseDragged, we continuously add lines to the path. If the user stops dragging and presses the mouse button again, a new path is created. When the user clicks the canvas with the secondary mouse button, we draw the background over everything in the canvas, cleaning it. The methods for creating paths allow you to interactively build geometric shapes; in this case, we just used it to make the drawing more precise (we could draw small points instead, creating the path), but there are many other applications for this part of the API. The result is a simple paint application that you can see in Figure 6-3.
public void start(Stage stage) throws Exception {
        Canvas canvas = new Canvas(800, 600);
        GraphicsContext ctx = canvas.getGraphicsContext2D();
        ctx.setLineWidth(10);
        canvas.setOnMousePressed(e -> ctx.beginPath());
        canvas.setOnMouseDragged(e -> {
                ctx.lineTo(e.getX(), e.getY());
                ctx.stroke();
        });
        canvas.setOnMouseClicked(e -> {
                if(e.getButton() == MouseButton.SECONDARY) {
                        clear(ctx);
                }
        });
        stage.setTitle("Drawing on Canvas");
        stage.setScene(new Scene(new StackPane(canvas), WIDTH, HEIGHT));
        stage.show();
        clear(ctx);
}
public void clear(GraphicsContext ctx) {
        ctx.setFill(Color.DARKBLUE);
        ctx.fillRect(0, 0, WIDTH, HEIGHT);
        ctx.setStroke(Color.ALICEBLUE);
}
Listing 6-3

Drawing on a canvas

../images/468104_1_En_6_Chapter/468104_1_En_6_Fig3_HTML.jpg
Figure 6-3

A small JavaFX drawing application

So far, we just explored high-level GraphicsContext methods to create shapes and texts. If we want to build more complex graphics, we may need to handle the pixels directly, one by one. Hopefully this can be easily achieved using the PixelWriter that can be accessed from GraphicsContext. Using pixel writer, we can set the color of each individual pixel in the canvas. The number of pixels depends on the Canvas size, for example, if it has size 800 × 600, then it has 480000 pixels which can be accessed individually using x and y points. In another words, we can go through each pixel of a canvas by iterating from x = 0 until x = Canvas.getWidth, and inside this iteration we can have another one from y = 0 until y = canvas.getHeight. Translating it to code, we have what you can see in Listing 6-4, which results in a canvas with random pixels as you can see in Figure 6-4.
Canvas canvas = new Canvas(WIDTH, HEIGHT);
GraphicsContext gc = canvas.getGraphicsContext2D();
for (int i = 0; i < canvas.getWidth(); i++) {
        for (int j = 0; j < canvas.getHeight(); j++) {
                gc.getPixelWriter().setColor(i, j, Color.color(Math.random(), Math.random(), Math.random()));
        }
}
Listing 6-4

Writing random colors to each pixel of a canvas

../images/468104_1_En_6_Chapter/468104_1_En_6_Fig4_HTML.jpg
Figure 6-4

A canvas with random pixels

The GraphicsContext class also allows you to draw complex paths, other geometric forms, and images and configure how the content will be displayed. To explore all the Canvas and GraphicsContext possibilities, we recommend you read the Javadocs, where you will find all available methods with information on how to use them.

Giving Life to a Canvas Application

To create the kind of applications we described at the beginning of this chapter, we need to constantly update the canvas to create animations or simulations. There are many different ways to achieve this; however, to keep it simple, we will inspire on the Processing programming language and create a method draw that is invoked repeatedly and setup that will be invoked only one time on an abstract class GraphicApp. In this chapter, we will use GraphicApp to explore a few known algorithms because it has some code that would be repeated in all examples. Using this abstract class, we can focus on setup and draw, and we won’t have to repeat ourselves in each example. Let’s understand what it does by checking its source in Listing 6-5.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import javafx.util.Duration;
public abstract class GraphicApp extends Application {
        protected int width = 800;
        protected int height = 600;
        protected GraphicsContext graphicContext;
        private Paint backgroundColor = Color.BLACK;
        private Timeline timeline = new Timeline();
        private int frames = 30;
        private BorderPane root;
        private Stage stage;
        @Override
        public void start(Stage stage) throws Exception {
                this.stage = stage;
                Canvas canvas = new Canvas(width, height);
                graphicContext = canvas.getGraphicsContext2D();
                canvas.requestFocus();
                root = new BorderPane(canvas);
                stage.setScene(new Scene(root));
                setup();
                canvas.setWidth(width);
                canvas.setHeight(height);
                startDrawing();
                stage.show();
                internalDraw();
        }
        public abstract void setup();
        public abstract void draw();
        public void title(String title) {
                stage.setTitle(title);
        }
        public void background(Paint color) {
                backgroundColor = color;
        }
        public void frames(int frames) {
                this.frames = frames;
                startDrawing();
        }
        public void setBottom(Node node) {
                root.setBottom(node);
        }
        private void internalDraw() {
                graphicContext.setFill(backgroundColor);
                graphicContext.fillRect(0, 0, width, height);
                draw();
        }
        private void startDrawing() {
                timeline.stop();
                if (frames > 0) {
                        timeline.getKeyFrames().clear();
                        KeyFrame frame = new KeyFrame(Duration.millis(1000 / frames), e -> internalDraw());
                        timeline.getKeyFrames().add(frame);
                        timeline.setCycleCount(Timeline.INDEFINITE);
                        timeline.play();
                }
        }
        public double map(double value, double start1, double stop1, double start2, double stop2) {
                return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
        }
}
Listing 6-5

The GraphicApp abstract class provides a skeleton for creating animated graphics using canvas

Notice that the methods draw and setup are abstract. To create applications, we must extend GraphicApp and implement these methods. The method draw call frequency is controlled by a Timeline class as you can see in method startDrawing, where a unique frame duration is controlled by a frame int parameter, which represents the number of frames per second. On the method draw, it is possible to access the parameter grahicsContext, which is of type GraphicsContext, and then start creating your application. Using grahicsContext, you can also access the canvas to register listeners, so you can respond to user input. The method map is a utility to convert values of a range to another range. Finally, you can add custom controls to the bottom using the setBottom method .

Using GraphicsApp, we can focus on our visual effects. For example, let’s create a bouncing balls application. This application simply draws a few ovals that change direction when they reach the application boundaries. You can see in Listing 6-6 that we focus on our idea, which is a model element that represents a ball using the class Ball, and then generate random values for it; and for each iteration in draw, we update the ball position and draw it in the screen.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
public class BouncingBalls extends GraphicApp {
        private static final int TOTAL_BALLS = 20;
        List<Ball> balls = new ArrayList<>();
        public static void main(String[] args) {
                launch(args);
        }
        @Override
        public void setup() {
                Random random = new Random();
                for (int i = 0; i < TOTAL_BALLS; i++) {
                        Ball ball = new Ball();
                        ball.circ = random.nextInt(100) + 10;
                        ball.x = random.nextInt(width - ball.circ);
                        ball.y = random.nextInt(height - ball.circ);
                        ball.xDir = random.nextBoolean() ? 1: -1;
                        ball.yDir = random.nextBoolean() ? 1: -1;
                        ball.color = Color.color(Math.random(), Math.random(), Math.random());
                        balls.add(ball);
                }
                background(Color.DARKCYAN);
        }
        @Override
        public void draw() {
                for (Ball ball : balls) {
                        ball.update();
                        ball.draw(gc);
                }
        }
        public class Ball {
                int x, y, xDir = 1, yDir = 1, circ;
                Color color;
                public void update() {
                        if (x + circ > width || x < 0) {
                                xDir *= -1;
                        }
                        if (y + circ > height || y < 0) {
                                yDir *= -1;
                        }
                        x += 5 * xDir;
                        y += 5 * yDir;
                }
                public void draw(GraphicsContext gc) {
                        gc.setLineWidth(10);
                        gc.setFill(color);
                        gc.fillOval(x, y, circ, circ);
                        gc.setStroke(Color.BLACK);
                        gc.strokeOval(x, y, circ, circ);
                }
        }
}
Listing 6-6

The bouncing balls example

When you run this application, you will see balls on the canvas, moving to all sides, as you can see in Figure 6-5. You can improve it by adding intersection detection, physics, event handling, or any other effect that would make it useful or cool.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig5_HTML.jpg
Figure 6-5

Bouncing balls example

Having said that, let’s explore some known algorithms using our GraphicsApp.

Particle Systems

Particle systems were introduced by William Reeves in the paper “Particle Systems: A Technique for Modeling a Class of Fuzzy Objects” where he defined them as “a collection of many many minute particles that together represent a fuzzy object.” You can think of it by having two main components: emitter and particle. An emitter keeps creating particles that will eventually die. The applications for particle systems include the following:
  • Games effects: Explosions, collision

  • Animations: Fire, cloud, wave hitting a stone

  • Simulations: Space, reproduction of living beings

It is possible to create a very simple particle system with a few lines of code, but this type of system can be considerably complex depending on what we want to achieve. For simple and advanced particle systems, we will basically need two classes: Particle and Emitter. The Particle class depends on Emitter, and an Emitter can have one or an infinite number of particles.

Using these classes, we can build an application with a single emitter which generates particles that move to random directions. See Figure 6-6 followed by the code to generate it in Listing 6-7.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig6_HTML.jpg
Figure 6-6

A very simple particle system

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
public class ParticleSystem extends GraphicApp {
        private List<Emitter> emitters = new ArrayList<>();
        Random random = new Random();
        public static void main(String[] args) {
                launch();
        }
        @Override
        public void setup() {
                frames(50);
                width = 1200;
                height = 800;
                // you can change it to onMouseDragged
                graphicContext.getCanvas().setOnMouseDragged(e -> emitters.add(new Emitter(5, e.getSceneX(), e.getSceneY())));
                title("Simple Particle System");
        }
        @Override
        public void draw() {
                for (Emitter emitter : emitters) {
                        emitter.emit(graphicContext);
                }
        }
        public class Emitter {
                List<Particle> particles = new ArrayList<>();
                int n = 1;
                double x, y;
                public Emitter(int n, double x, double y) {
                        this.n = n;
                        this.x = x;
                        this.y = y;
                }
                public void emit(GraphicsContext gc) {
                        for (int i = 0; i < n; i++) {
                                int duration = random.nextInt(200) + 2;
                                double yDir = random.nextDouble() * 2.0 + -1.0;
                                double xDir = random.nextDouble() * 2.0 + -1.0;
                                Particle p = new Particle(x, y, duration, xDir, yDir);
                                particles.add(p);
                        }
                        for (Particle particle : particles) {
                                particle.step();
                                particle.show(gc);
                        }
                        particles = particles.stream().filter(p -> p.duration > 0).collect(Collectors.toList());
                }
        }
        public class Particle {
                int duration;
                double x, y, yDir, xDir;
                public Particle(double x, double y, int duration, double yDir, double xDir) {
                        this.x = x;
                        this.y = y;
                        this.duration = duration;
                        this.yDir = yDir;
                        this.xDir = xDir;
                }
                public void step() {
                        x += xDir;
                        y += yDir;
                        duration--;
                }
                public void show(GraphicsContext gc) {
                        gc.setFill(Color.rgb(255, 20, 20, 0.6));
                        gc.fillOval(x, y, 3, 3);
                }
        }
}
Listing 6-7

Very simple particle system

In the code from Listing 6-7, we generate a particle system at the position where the user clicks the canvas. Notice the class emitter generates particles every time the method emit is called and it also draws the existing particles; these two actions could be separated in two different methods. A particle is a simple oval; and it has a duration, an initial x and y position, and a y and x direction. The emitter emits all particles, and after it is done, all the obsolete particles are removed. The code is flexible and easy to extend, for example, when we remove the mouse clicked event listener on canvas and change it to use mouse dragged event, we can “write” using particle systems. See Figure 6-7.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig7_HTML.jpg
Figure 6-7

Using mouse dragged with the particle system

To make the particle system configurable, let’s add a control panel at the bottom of our application so users can configure a lot of aspects of the emitters and particles to experiment the full potential of particle systems. For this purpose, we created an application that allows users to add new emitters when they click the canvas. See our configurable particle system in Figure 6-8.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig8_HTML.jpg
Figure 6-8

Configurable particle system

The code for the emitter creation can be found in Listing 6-8. The way it works is simple. When a click happens on the canvas, a new emitter is added to a List; and in draw method, the emit method of each particle system is called. The configuration in the bottom pane (see Figure 6-8) is passed to each Emitter when it is created, and if users select the toggle button Static Configuration, the configuration for that particular emitter won’t be updated in real time.
@Override
public void setup() {
        frames(20);
        width = 1200;
        height = 800;
        GridPane gpConfRoot = buildConfigurationRoot();
        TitledPane tpConf = new TitledPane("Configuration", gpConfRoot);
        tpConf.setCollapsible(false);
        setBottom(tpConf);
        graphicContext.getCanvas().setOnMouseClicked(e -> {
                Emitter newEmitter;
                if (globalConf.cloneConfProperty.get()) {
                        newEmitter = new Emitter(e.getSceneX(), e.getSceneY(), globalConf.clone());
                } else {
                        newEmitter = new Emitter(e.getSceneX(), e.getSceneY(), globalConf);
                }
                emitters.add(newEmitter);
        });
        title("Particle System configurable");
}
@Override
public void draw() {
        for (Emitter emitter : emitters) {
                emitter.emit(graphicContext);
        }
}
Listing 6-8

Emitter creation and calling draw

The configuration object holds diverse information that is used by the emitter to create a particle. The class ParticleSystemConf (see Listing 6-9) uses JavaFX properties so the property values can be bound directly to the controls we added to the bottom pane. These properties control the number of particles produced every time that emit is called, how many frames the particle will live in the application (particle duration), and the size of the oval that represents the particle opacity. You can also select the particle color and if it will move in a straight line or if it will oscillate and if it should have a fade-out effect. Finally, the configuration has also a clone method , which allows us to create a new configuration that won’t be bound to the controls as shown in Listing 6-9.
public class ParticleSystemConf {
        IntegerProperty numberOfParticlesProperty = new SimpleIntegerProperty();
        IntegerProperty durationProperty = new SimpleIntegerProperty();
        DoubleProperty sizeProperty = new SimpleDoubleProperty();
        DoubleProperty opacityProperty = new SimpleDoubleProperty();
        BooleanProperty oscilateProperty = new SimpleBooleanProperty();
        BooleanProperty fadeOutProperty = new SimpleBooleanProperty();
        ObjectProperty<Color> colorProperty = new SimpleObjectProperty<>();
        BooleanProperty cloneConfProperty = new SimpleBooleanProperty();
        public ParticleSystemConf clone() {
                ParticleSystemConf newConf = new ParticleSystemConf();
                newConf.numberOfParticlesProperty.set(numberOfParticlesProperty.get());
                newConf.durationProperty.set(durationProperty.get());
                newConf.sizeProperty.set(sizeProperty.get());
                newConf.opacityProperty.set(opacityProperty.get());
                newConf.oscilateProperty.set(oscilateProperty.get());
                newConf.fadeOutProperty.set(fadeOutProperty.get());
                newConf.colorProperty.set(colorProperty.get());
                return newConf;
        }
}
Listing 6-9

The configuration object

All the fields of the configuration are later bound to a control that is added to the bottom of the application:
cbBackgrounColor.valueProperty().addListener((a, b, c) -> background(c));
globalConf.numberOfParticlesProperty.bind(sldNumberOfParticles.valueProperty());
globalConf.durationProperty.bind(sldDuration.valueProperty());
globalConf.oscilateProperty.bind(cbOscillate.selectedProperty());
globalConf.sizeProperty.bind(sldPParticleSize.valueProperty());
globalConf.opacityProperty.bind(sldOpacity.valueProperty());
globalConf.fadeOutProperty.bind(cbFadeOut.selectedProperty());
globalConf.colorProperty.bind(cbColor.valueProperty());
globalConf.cloneConfProperty.bind(tbClone.selectedProperty());
Finally, all the configuration is used in Emitter and Particle classes as you can see in Listing 6-10.
public class Emitter {
        List<Particle> particles = new ArrayList<>();
        double x, y;
        private ParticleSystemConf conf;
        public Emitter(double x, double y, ParticleSystemConf conf) {
                this.x = x;
                this.y = y;
                this.conf = conf;
        }
        public void emit(GraphicsContext gc) {
                for (int i = 0; i < conf.numberOfParticlesProperty.get(); i++) {
                        Particle p = new Particle(x, y, conf);
                        particles.add(p);
                }
                for (Particle particle : particles) {
                        particle.step();
                        particle.show(gc);
                }
                particles = particles.stream().filter(p -> p.duration > 0).collect(Collectors.toList());
        }
}
public class Particle {
        int duration, initialDuration;
        double x, y, yDir, xDir, size, opacity, currentOpacity;
        Color color = Color.YELLOW;
        boolean oscilate, fadeOut;
        public Particle(double x, double y, ParticleSystemConf conf) {
                this.x = x;
                this.y = y;
                this.oscilate = conf.oscilateProperty.get();
                this.size = conf.sizeProperty.get();
                this.initialDuration = conf.durationProperty.get() + 1;
                this.yDir = random.nextGaussian() * 2.0 - 1.0;
                this.xDir = random.nextGaussian() * 2.0 + -1.0;
                this.opacity = conf.opacityProperty.get();
                this.fadeOut = conf.fadeOutProperty.get();
                this.duration = initialDuration;
                this.currentOpacity = opacity;
                this.color = conf.colorProperty.get();
        }
        public void step() {
                x += xDir;
                y += yDir;
                if (oscilate) {
                        x += Math.sin(duration) * 10;
                        y += Math.cos(duration) * 10;
                }
                if (fadeOut) {
                        currentOpacity = map(duration, 0, initialDuration, 0, opacity);
                }
                duration--;
        }
        public void show(GraphicsContext gc) {
                Color cl = Color.color(color.getRed(), color.getGreen(), color.getBlue(), currentOpacity);
                gc.setFill(cl);
                gc.fillOval(x, y, size, size);
        }
}
Listing 6-10

Particle and Emitter classes using the configuration object

Not all the code for the configurable particle system was shared in this chapter; however, you can find it in the GitHub repository associated with this book. When you run this application, you will notice that you can quickly make it slow if you add a lot of emitters with a lot of particles generated by frame and mainly if you have too many frames per second. You can improve the performance following the tips provided at the end of this chapter. There are a few nice features that could be added to this application:
  • Particle format selection

  • Particle orientation

  • Exporting the visualization to a file or in a format that could be reused in other applications

We will let these tasks as an exercise for you!

Fractals

A rough definition of a fractal is a geometric shape formed of other small geometric shapes that resemble itself. Using fractals, we can create beautiful and intriguing art, but also understand pattern formations in nature. In our case, we will explore canvas capacity using fractals.

A famous fractal created from a sequence of complex numbers is the Mandelbrot set. To build a Mandelbrot set, you must iterate on function f(z) = z2 + c filing it with values from its own results starting from 0. This function tends to infinity; however, there are a few intermediate values that may lead to interesting results. For example, if you iterate on an image pixel and map the pixel to the values accepted by the Mandelbrot sent and then, using pixel writer, set the pixel color as white when the result tends to infinity and as black otherwise, the result will be something like in Figure 6-9. Notice that in this figure the small part resembles the whole. It looks like we have a small Mandelbrot everywhere. The code for it is in Listing 6-11.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig9_HTML.jpg
Figure 6-9

Simplest Mandelbrot set

private final int MAX_ITERATIONS = 100;
private double zx, zy, cX, cY, tmp;
int i;
@Override
public void setup() {
        width = 1200;
        height = 800;
        frames(0);
}
@Override
public void draw() {
        long start = System.currentTimeMillis();
        for (int x = 0; x < width; x++) {
                for (int y = 0; y < height; y++) {
                        zx = zy = 0;
                        // the known range of accepted values for cx and cy
                        cX = map(x, 0, width, -2.5, 1.0);
                        cY = map(y, 0, height, -1, 1.0);
                        i = 0;
                        while (zx * zx + zy * zy < 4 && i < MAX_ITERATIONS) {
                                tmp = zx * zx - zy * zy + cX;
                                zy = 2.0 * zx * zy + cY;
                                zx = tmp;
                                i++;
                        }
                        // if it is not exploding to infinite
                        if (i < MAX_ITERATIONS) {
                                graphicContext.getPixelWriter().setColor(x, y, Color.WHITE);
                        } else {
                                graphicContext.getPixelWriter().setColor(x, y, Color.BLACK);
                        }
                }
        }
        System.out.println("GEnerating mandelbrot took " + (System.currentTimeMillis() - start)  + " ms");
}
Listing 6-11

Simplest Mandelbrot

If you search for Mandelbrot in online videos, you will find very interesting special effects such as zooming effects and different colors. This is possible due to coloring algorithms and zoom effects. Let’s improve the original Mandelbrot first by allowing a fake zoom. This can be done by manipulating the root pane of the GraphicsApp and wrapping the canvas in a stack pane with a very big size and then wrapping it in a scroll pane, which provides the scrolling functionality. The canvas size can be changed using event listeners: when a user clicks the scroll pane with the left button, it zooms in; when the user clicks using the left button, it zooms out; and clicking with the middle button resets the zoom and centralizes the pane. This is all done in setup method as in Listing 6-12, where you can see the trick for the zoom: we are actually scaling the canvas; it is not a real zoom. In Figure 6-10, you can see the result without zoom. The zoom effect is shown in Figure 6-11. Notice that it does not adjust the resolution, hence, as we said, a fake zoom.
@Override
public void setup() {
        width = 1200;
        height = 800;
        Canvas canvas = graphicContext.getCanvas();
        BorderPane bp = (BorderPane) canvas.getParent();
        bp.setCenter(null);
        StackPane p = new StackPane(canvas);
        p.setMinSize(20000, 20000);
        ScrollPane sp = new ScrollPane(p);
        sp.setPrefSize(1200, 800);
        sp.setVvalue(0.5);
        sp.setHvalue(0.5);
        bp.setCenter(sp);
        sp.setOnMouseClicked(e -> {
                double zoom = 0.2;
                double scaleX = canvas.getScaleX();
                double scaleY = canvas.getScaleY();
                if (e.getButton() == MouseButton.SECONDARY && (canvas.getScaleX() > 0.5)) {
                        canvas.setScaleX(scaleX - zoom);
                        canvas.setScaleY(scaleY - zoom);
                } else if (e.getButton() == MouseButton.PRIMARY) {
                        canvas.setScaleX(scaleX + zoom);
                        canvas.setScaleY(scaleY + zoom);
                } else if (e.getButton() == MouseButton.MIDDLE) {
                        sp.setVvalue(0.5);
                        sp.setHvalue(0.5);
                        canvas.setScaleY(1);
                        canvas.setScaleX(1);
                }
        });
        canvas.setOnMousePressed(canvas.getOnMouseClicked());
        frames(0);
        title("Mandelbrot with color and zoom");
}
Listing 6-12

Trick for zoom into the application canvas

For coloring, we modify the Mandelbrot color. Instead of white, pick a value relative to the last iteration. Using this value, we can play with the generated color. For example, with the values from Listing 6-13, we have a purple-ish value for the outer color and green-ish values in the borders as you can see in Figure 6-10.

// if the steps above are not heading towards infinite we draw the pixel with a specific color
if (i < MAX_ITERATIONS) {
        double newC = ((double) i) / ((double) MAX_ITERATIONS);
        Color c;
        if(newC > 0.4)
        c = Color.color(newC, 0.8, newC);
        else c = Color.color(0.2, newC, 0.2);
        graphicContext.getPixelWriter().setColor(x, y, c);
} else {
        graphicContext.getPixelWriter().setColor(x, y, Color.BLACK);
}
Listing 6-13

Adding colors to the Mandelbrot non-infinite values

../images/468104_1_En_6_Chapter/468104_1_En_6_Fig10_HTML.jpg
Figure 6-10

Mandelbrot with colors and zoom

../images/468104_1_En_6_Chapter/468104_1_En_6_Fig11_HTML.jpg
Figure 6-11

Zooming in to the Mandelbrot

That’s all for the Mandelbrot. Take some time to modify the code, try to generate more interesting colors, and play with the parameters. As our next visual effect, we will create a panel for real-time experiment and also extend the Mandelbrot to also allow us to test Julia set values, generating other fractal forms.

Julia set is a collection of fixed values for Mandelbrot imaginary and real values. Using these fixed values, we can create forms that are derived from Mandelbrot. In our code, we just stopped calculating cx and ci variables from Mandelbrot, and instead we let users choose a value for them using a JavaFX slider added to the bottom part of our root pane. The central pane uses the same trick for zoom that we used in Mandelbrot, and this time we will let the user select values for many different parameters of the fractal form, generating unique images. The changes we did in Mandelbrot code to generate Julia sets can be seen in Listing 6-14, where cx and ci are coming from a configuration object, which we will describe soon. Also, the color now is coming from a specific method that will take the user configuration.
@Override
public void draw() {
        running.set(true);
        totalIterations++;
        for (int x = 0; x < width; x++) {
                for (int y = 0; y < height; y++) {
                        zx = zy = 0;
                        zx = 1.5 * (x - width / 2) / (0.5 * width);
                        zy = (y - height / 2) / (0.5 * height);
                        i = 0;
                        while (zx * zx + zy * zy < 4 && i < totalIterations) {
                                tmp = zx * zx - zy * zy + conf.cx;
                                zy = 2.0 * zx * zy + conf.ci;
                                zx = tmp;
                                i++;
                        }
                        Color c = conf.infinityColor;
                        if (i < totalIterations) {
                                double newC = ((double) i) / ((double) totalIterations);
                                c = getColor(newC);
                        }
                        graphicContext.getPixelWriter().setColor(x, y, c);
                }
        }
        if (totalIterations > conf.maxIterations) {
                running.set(false);
                frames(0);
        }
}
private Color getColor(double newC) {
        double r = newC, g = newC, b = newC;
        if (newC > conf.threshold) {
                if (!conf.computedLighterR)
                        r = conf.lighterR;
                if (!conf.computedLighterG)
                        g = conf.lighterG;
                if (!conf.computedLighterB)
                        b = conf.lighterB;
        } else {
                if (!conf.computedDarkerR)
                        r = conf.darkerR;
                if (!conf.computedDarkerG)
                        g = conf.darkerG;
                if (!conf.computedDarkerB)
                        b = conf.darkerB;
        }
        return Color.color(r, g, b);
}
Listing 6-14

Code for Julia sets. Now the values come from configuration objects

The configuration, however, is not using binding for the reason that binding inside the for-loop in the draw() method will be much slower than using primitive types. To make the configuration object up to date with the configuration, we make use of listeners, so for each element in the UI, we have a listener that will update the configuration object when the control is changed. This way, the loop that draws the fractal form won’t suffer from performance issues due to the use of binding. The configuration and the bottom pane constructions can be found in Listing 6-15. In Figure 6-12, you can see the application in action.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig12_HTML.jpg
Figure 6-12

Our Julia set fractals application

Each control you see in Figure 6-12 is explained in the following:
  • Lighter Colors: The colors for values that are above the threshold. You can use a slider for each value (RGB), and if you select Auto, the value for that specific color part is taken from the algorithm we saw in Listing 6-14.

  • Darker Colors: Just like lighter colors, but used for the values that are below the threshold.

  • Threshold: A threshold for dividing the colors. We can select values for the colors that are above the threshold or below it.

  • Inner Color: A Color Picker that allows you to select the default color when the calculated value tends to infinity.

  • Iterations: A Spinner that contains possible values for iterations. Iterations are the number of times we make our calculation before checking if it tends to infinity or not.

  • cx and cy: These sliders are the range of known values for Julia set. Changing it will change the fractal form.

  • The button Animate will show each step of the fractal evolution by drawing it from iteration 1 until the number you selected in Iterations spinner.

Using these controls, you can create really interesting fractals like the one from Figure 6-13.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig13_HTML.jpg
Figure 6-13

A fractal generated using our application

public static class JuliaSetConf {
        public double threshold = 0.8;
        public double lighterR = 0.7;
        public double lighterG = 0.7;
        public double lighterB = 0.7;
        public double darkerR = 0.3;
        public double darkerG = 0.3;
        public double darkerB = 0.3;
        public double cx = -0.70176;
        public double ci = -0.3842;
        public boolean computedLighterR = true;
        public boolean computedLighterG = true;
        public boolean computedLighterB = true;
        public boolean computedDarkerR = true;
        public boolean computedDarkerG = true;
        public boolean computedDarkerB = true;
        public Color infinityColor = Color.GOLDENROD;
        public int maxIterations = MAX_ITERATIONS / 2;
}
private Node createConfPanel() {
        VBox vbConf = new VBox(5);
        Slider spLigherR = slider(conf.lighterR);
        Slider spLigherG = slider(conf.lighterG);
        Slider spLigherB = slider(conf.lighterB);
        CheckBox chkUseComputedLighterR = checkBox();
        CheckBox chkUseComputedLighterG = checkBox();
        CheckBox chkUseComputedLighterB = checkBox();
        vbConf.getChildren().add(new HBox(10, new Label("Lighter Colors"),
                        spLigherR, chkUseComputedLighterR, spLigherG,
                        chkUseComputedLighterG, spLigherB, chkUseComputedLighterB));
        Slider spDarkerR = slider(conf.darkerR);
        Slider spDarkerG = slider(conf.darkerG);
        Slider spDarkerB = slider(conf.darkerB);
        CheckBox chkUseComputedDarkerR = checkBox();
        CheckBox chkUseComputedDarkerG = checkBox();
        CheckBox chkUseComputedDarkerB = checkBox();
        vbConf.getChildren().add(new HBox(10, new Label("Darker Colors"),
                        spDarkerR, chkUseComputedDarkerR, spDarkerG,
                        chkUseComputedDarkerG, spDarkerB, chkUseComputedDarkerB));
        Slider sldThreshold = slider(conf.threshold);
        Spinner<Integer> spMaxIterations = new Spinner<>(10, MAX_ITERATIONS, MAX_ITERATIONS / 2);
        spMaxIterations.valueProperty().addListener(c -> updateConf.run());
        ColorPicker clInifinity = new ColorPicker(conf.infinityColor);
        clInifinity.valueProperty().addListener(c -> updateConf.run());
        HBox hbGeneral = new HBox(5, new Label("Threshold"), sldThreshold,
                        new Label("Inner Color"), clInifinity,
                        new Label("Iterations"), spMaxIterations);
        hbGeneral.setAlignment(Pos.CENTER_LEFT);
        vbConf.getChildren().add(hbGeneral);
        Slider sldX = slider(-1, 1.0, conf.cx);
        sldX.setMinSize(300, 10);
        Slider sldI = slider(-1, 1.0, conf.ci);
        sldI.setMinSize(300, 10);
        Button btnRun = new Button("Animate");
        // since we are not using bind we need to get all the properties here
        updateConf = () -> {
                conf.lighterR = spLigherR.getValue();
                conf.lighterG = spLigherG.getValue();
                conf.lighterB = spLigherB.getValue();
                conf.darkerR = spDarkerR.getValue();
                conf.darkerG = spDarkerG.getValue();
                conf.darkerB = spDarkerB.getValue();
                conf.threshold = sldThreshold.getValue();
                conf.computedLighterR = chkUseComputedLighterR.isSelected();
                conf.computedLighterG = chkUseComputedLighterG.isSelected();
                conf.computedLighterB = chkUseComputedLighterB.isSelected();
                conf.computedDarkerR = chkUseComputedDarkerR.isSelected();
                conf.computedDarkerG = chkUseComputedDarkerG.isSelected();
                conf.computedDarkerB = chkUseComputedDarkerB.isSelected();
                conf.cx = sldX.getValue();
                conf.ci = sldI.getValue();
                conf.infinityColor = clInifinity.getValue();
                conf.maxIterations = spMaxIterations.getValue();
                totalIterations = conf.maxIterations;
                frames(TOTAL_FRAMES);
        };
        btnRun.setOnAction(e -> {
                updateConf.run();
                totalIterations = 1;
        });
        HBox hbSet = new HBox(5, new Label("cX"), sldX, new Label("cI"), sldI, btnRun);
        vbConf.getChildren().add(hbSet);
        TitledPane pnConf = new TitledPane("Configuration", vbConf);
        pnConf.setExpanded(true);
        pnConf.setCollapsible(false);
        pnConf.disableProperty().bind(running);
        return pnConf;
}
private CheckBox checkBox() {
        CheckBox checkBox = new CheckBox("Auto");
        checkBox.setSelected(true);
        checkBox.selectedProperty().addListener(c -> updateConf.run());
        return checkBox;
}
private Slider slider(double d) {
        return slider(0.0, 1.0, d);
}
private Slider slider(double min, double max, double d) {
        Slider slider = new Slider(min, max, d);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);
        slider.setMajorTickUnit(0.1);
        slider.valueProperty().addListener(c -> updateConf.run());
        return slider;
}
Listing 6-15

Code for the Julia set

High Performance

The performance so far has not been discussed. The focus was entirely on creating our algorithms using JavaFX APIs, meaning that we trusted only the JavaFX hardware acceleration feature that was mentioned before. If you run the fractal and particle examples, you will notice that the performance is compromised once we push it to its limits. In the last part of this chapter, we will make a more advanced discussion, discuss why JavaFX itself won’t bring the best performance for your application, and propose solutions based on Sean M. Phillips’s article published in Java Magazine in May–June 2018: “Producer-Consumer Implementations in JavaFX.”

JavaFX is single-threaded. All the rendering is done in a single thread which means that if you hold the thread with a long-running task, it won’t show anything until the task is done. When you code something in start method of a JavaFX application, you are already on the JavaFX main thread. To make clear this behavior, see the application whose code is in Listing 6-16. In this application, we have an animated label; we have also a button. When you click the button, we call Thread.sleep, and the animation simply stops. You can’t even click the button. The reason is that the main thread was locked by our Thread.sleep call!
import javafx.animation.ScaleTransition;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class LockedThread extends Application {
        public static void main(String[] args) {
                launch();
        }
        @Override
        public void start(Stage stage) throws Exception {
                Label lblHello = new Label("Hello World");
                ScaleTransition st = new ScaleTransition(Duration.seconds(1));
                st.setAutoReverse(true);
                st.setCycleCount(Transition.INDEFINITE);
                st.setByX(2);
                st.setByY(2);
                st.setNode(lblHello);
                Button btnLock = new Button("Sleep for 10 seconds");
                BorderPane bp = new BorderPane(lblHello);
                bp.setBottom(btnLock);
                stage.setScene(new Scene(bp, 300, 200));
                stage.show();
                btnLock.setOnAction(e -> {
                        try {
                                Thread.sleep(10000);
                        } catch (InterruptedException e1) {
                                e1.printStackTrace();
                        }
                });
                st.play();
        }
}
Listing 6-16

Locking the main JavaFX thread

The lesson is don’t do heavy task in the main thread. The solution is to use a separate thread for the actual processing, and once you are done, update the canvas (or the user interface) in the JavaFX thread. With this approach, the load is taken away from the main thread, and the application should run smoothly.

Now that we know this, we will try to call graphic context or make any JavaFX control change in a different thread, and what you will see is an exception of type java.lang.IllegalStateException with message Not on FX application thread. To assure that something is running on the JavaFX thread, we may use javafx.application.Platform.runLater passing a runnable which will run later on JavaFX thread: Platform.runLater(() ➤ gc.fillText(“Safe Fill Text”, 0, 0)). In other words, make sure to do JavaFX control updates on the main thread; otherwise, we may face the exception we mentioned previously.

However, Platform.runLater won’t solve all the issues we will face with concurrent programming in JavaFX. There are other utilities on the javafx.concurrent package, mainly javafx.concurrent.Task class, which is very useful for asynchronous tasks. For this chapter, we will explore the high-density data pattern introduced by Sean M. Phillips in Java Magazine in May–June 2018: “Producer-Consumer Implementations in JavaFX.”

If you check the mentioned article, you will notice that the idea is to have a thread that will do the hard processing and push the result to a queue and then another thread that gets the result once it is available and updates the canvas. The first thread is known as the producer, and it is responsible to do the hard processing without touching the JavaFX thread. The results generated by the publisher are added to a java.util.concurrent.ConcurrentLinkedQueue, which are received by the second thread, the consumer thread, that will then do the graphical processing in the JavaFX thread.

To show the pattern in a real-world application, let’s create an implementation of Conway’s Game of Life. In the Wikipedia article of the same name, you will find that Game of Life is a cellular automaton where a cell dies if it has three or more neighbours due to overpopulation, cells with less than two neighbours die by underpopulation, dead cells surrounded by exactly three neighbours will be reborn, and cells with two or three neighbours remain alive.

We made our Game of Life implementation in Listing 6-17. The cells are represented by boolean values, where true means a live cell. We can set the size of each cell and the width and height of the application, which means that the number of cells can be calculated by width divided by the cell’s size times the height divided by the cell’s size. The application will write a square of size cellSize for each live cell and then calculate the next generation of cells based on the rules we discussed before. To determine if a cell will live or not depends on the number of neighbours, and in method countNeighbours we made a different way to calculate the number of neighbours, which is to check each neighbour position and exclude the cases where the neighbour checking would lead to errors. This approach saved us from an if/else ugly implementation. Since we need to sum the neighbours of each cell, we will have to go through each cell in a for-for loop to find each cell neighbour as you can see in method newGeneration.
import java.util.Arrays;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
public class GameOfLife {
        private int columns;
        private int rows;
        private int cellSize;
        public GameOfLife(int columns, int rows, int cellSize) {
                this.columns = columns;
                this.rows = rows;
                this.cellSize = cellSize;
        }
        public boolean[][] newCells() {
                boolean[][] newCells = new boolean[columns][rows];
                for (int i = 0; i < columns; i++) {
                        for (int j = 0; j < rows; j++) {
                                newCells[i][j] = Math.random() > 0.5;
                        }
                }
                return newCells;
        }
        public void drawCells(boolean[][] cells, GraphicsContext graphicContext) {
                for (int i = 0; i < columns; i++) {
                        for (int j = 0; j < rows; j++) {
                                if (cells[i][j]) {
                                        graphicContext.setFill(Color.BLACK);
                                        graphicContext.fillRect(i * cellSize, j * cellSize, cellSize, cellSize);
                                }
                        }
                }
        }
        public boolean[][] newGeneration(boolean previousGeneration[][]) {
                boolean[][] newGeneration = new boolean[columns][rows];
                for (int i = 0; i < columns; i++) {
                        for (int j = 0; j < rows; j++) {
                                updateCell(previousGeneration, newGeneration, i, j);
                        }
                }
                return newGeneration;
        }
        private void updateCell(boolean[][] previousGeneration, boolean[][] newGeneration, int i, int j) {
                int countNeighbours = countNeighbours(previousGeneration, i, j);
                if (previousGeneration[i][j] && (countNeighbours < 2 || countNeighbours > 3)) {
                        newGeneration[i][j] = false;
                } else if (!previousGeneration[i][j] && countNeighbours == 3) {
                        newGeneration[i][j] = true;
                } else if (previousGeneration[i][j]) {
                        newGeneration[i][j] = true;
                }
        }
        private int countNeighbours(boolean[][] copy, int i, int j) {
                int[][] borders = {
                                {i - 1, j -1}, {i -1, j}, {i -1, j+ 1},
                                {i, j -1}, {i, j + 1},
                                {i +1, j - 1}, {i +1, j}, {i +1, j +1}
                };
                return (int) Arrays.stream(borders)
                        .filter(b -> b[0] > -1 &&
                                        b[0] < columns &&
                                        b[1] > -1      &&
                                        b[1] < rows    &&
                                        copy[b[0]][b[1]])
                        .count();
        }
}
Listing 6-17

A Game of Life implementation

The first and easy way to give life to this game is using a subclass of GraphicApp which will do all the work in draw method. Each time draw is called, the current generation will be rendered, and a new generation will replace the current one. As you know, the method draw runs on the JavaFX thread, which means this implementation will use a single thread to do all work. This implementation can be found in Listing 6-18, and the result can be seen in Figure 6-14.
import javafx.scene.paint.Color;
public class GameOfLifeFXThread extends GraphicApp {
        final int WIDTH = 2500;
        final int HEIGHT = 2500;
        final int CELL_SIZE = 5;
        boolean currentGeneration[][];
        int columns = WIDTH / CELL_SIZE;
        int rows = HEIGHT / CELL_SIZE;
        private GameOfLife gameOfLife;
        public static void main(String[] args) {
                launch();
        }
        @Override
        public void setup() {
                width = WIDTH;
                height = HEIGHT;
                gameOfLife = new GameOfLife(columns, rows, CELL_SIZE);
                currentGeneration = gameOfLife.newCells();
                background(Color.DARKGRAY);
                title("Game of Life");
                frames(5);
        }
        @Override
        public void draw() {
                long initial = System.currentTimeMillis();
                gameOfLife.drawCells(currentGeneration, graphicContext);
                System.out.println("Time to render " + (System.currentTimeMillis() - initial));
                initial = System.currentTimeMillis();
                currentGeneration = gameOfLife.newGeneration(currentGeneration);
                System.out.println("Time to calculate new generation: " + (System.currentTimeMillis() - initial));
        }
}
Listing 6-18

A Game of Life running on the application main thread

../images/468104_1_En_6_Chapter/468104_1_En_6_Fig14_HTML.jpg
Figure 6-14

Our Game of Life

If you run the implementation from Listing 6-18 with size 2500 × 2500 and cell size 5 (2500 × 2500 × 5), you will see in console that the time to calculate the next generation is approximately 30 times greater than the time used to actually render the cells, meaning that most of the time is used to calculate the new generation while the JavaFX thread is locked. The application becomes very slow and unresponsive when we simply change the size of cell to 2 (remember that the amount of cells is dependent on the cell size) because now the main thread is locked doing the new generation. You can see the output of console in Figure 6-15, which was collected for 2500 × 2500 × 2.
../images/468104_1_En_6_Chapter/468104_1_En_6_Fig15_HTML.jpg
Figure 6-15

Time to render × the time to calculate a new generation

Considering that you are running the Game of Life on a multiple-core machine, we can make a small change to transform the outer loop (or the columns loop) in newGeneration method to use a parallel stream instead. This was achieved by adding a new method to GameOfLife class which you can see in Listing 6-19. Using 2500 × 2500 with cell size 2, we can have an improvement of about 30% in a four-core machine, making the application much faster. The results can be found in Figure 6-16. Bear in mind that parallel is not a silver bullet solution. You must observe if the load you are making parallel is worth it; otherwise, the time to divide the work between the cores may be greater than the time to do the actual processing, resulting in performance degradation instead of a performance improvement.
public boolean[][] newGenerationParallel(boolean previousGeneration[][]) {
        boolean[][] newGeneration = new boolean[columns][rows];
        IntStream.range(0, columns).parallel().forEach(i -> {
                for (int j = 0; j < rows; j++) {
                        updateCell(previousGeneration, newGeneration, i, j);
                }
        });
        return newGeneration;
}
Listing 6-19

Method using parallel stream when checking the neighbours for all cells in a column

../images/468104_1_En_6_Chapter/468104_1_En_6_Fig16_HTML.jpg
Figure 6-16

Processing time after using a parallel stream when calculating the new generation

Since we are running everything on the JavaFX thread, we are limited on the improvements that can be done. However, if we use the same idea of the already mentioned high-density pattern, we can have impressive results. The application will rarely become unresponsive because it will take all the processing out of the JavaFX main thread and call Platform.runLater() only to render data. All the processing will be in a producer task, which calculates the new generation, and the result is added to a ConcurrentLinkedQueue. The results are later polled by another task, the consumer task, and then the canvas is updated in the application main thread. We can try to control the number of frames per second by polling the results every X milliseconds, for example, if you want ten frames per second, you can make the consumer thread sleep for 100 milliseconds each time it polls a result from the queue, or you can constantly poll results and update the canvas because the most important result is that the rest of the application will run smoothly without any impact for the end user, which means that the user may see a slow animation, but he still can change controls or do other tasks. The resulting code can be found in Listing 6-20. Further improvements could be done, such as using threads to calculate the new generation. In this case, simply calling parallel on a stream may not help because parallel uses all cores, meaning that it may starve the render thread, because all the cores will be used in the new generation calculation, so a more sophisticated parallel programming will be required.
import java.util.concurrent.ConcurrentLinkedQueue;
import org.examples.canvas.GraphicApp;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.paint.Color;
public class GameOfLifePublisherConsumer extends GraphicApp {
        final int WIDTH = 2500;
        final int HEIGHT = 2500;
        final int CELL_SIZE = 2;
        boolean currentGeneration[][];
        int columns = WIDTH / CELL_SIZE;
        int rows = HEIGHT / CELL_SIZE;
        // this is the desired number of frames
        int numberOfFramesPerSecond = 0;
        private GameOfLife gameOfLife;
        ConcurrentLinkedQueue<boolean[][]> cellsQueue;
        public static void main(String[] args) {
                launch();
        }
        @Override
        public void setup() {
                cellsQueue = new ConcurrentLinkedQueue<>();
                width = WIDTH;
                height = HEIGHT;
                gameOfLife = new GameOfLife(columns, rows, CELL_SIZE);
                currentGeneration = gameOfLife.newCells();
                Task<Void> producerTask = new Task<Void>() {
                        @Override
                        protected Void call() throws Exception {
                                while(true) {
                                        cellsQueue.add(currentGeneration);
                                        currentGeneration = gameOfLife.newGeneration(currentGeneration);
                                }
                        }
                };
                Task<Void> consumerTask = new Task<Void>() {
                        @Override
                        protected Void call() throws Exception {
                                while (true) {
                                        while (!cellsQueue.isEmpty()) {
                                                boolean[][] data = cellsQueue.poll();
                                                Platform.runLater(() -> {
                                                        // we need to draw the background because we are not using draw loop anymore
                                                        graphicContext.setFill(Color.LIGHTGRAY);
                                                        graphicContext.fillRect(0, 0, width, height);
                                                        gameOfLife.drawCells(data, graphicContext);
                                                });
                                                if(numberOfFramesPerSecond > 0) {
                                                        Thread.sleep(1000 / numberOfFramesPerSecond);
                                                }
                                        }
                                }
                        }
                };
                Thread producerThread = new Thread(producerTask);
                producerThread.setDaemon(true);
                Thread consumerThread = new Thread(consumerTask);
                consumerThread.setDaemon(true);
                producerThread.start();
                consumerThread.start();
                frames(0);
                title("Game of Life Using High-Density Data Pattern");
        }
        @Override
        public void draw() {
                // we don't use the main loop anymore, but we have to draw the background in draw cells
        }
}
Listing 6-20

Game of Life with high-density data pattern

Conclusion

JavaFX can be used to generate very complex visualizations. As with any framework that allows to create a user interface, it is very easy to create something with a bad performance. However, in this chapter, we explained a number of tips and tricks that allow you to get excellent performance, even with complex scene graphs and a high number of nodes.

With the basic knowledge about the JavaFX Application thread discussed in this chapter, you can leverage the capabilities JavaFX provides to achieve great performance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.119.106