JavaFX - Fractal

29.03.2013

JavaFX - Fractal: The Mandelbrot and the Julia Set

The following article describes how to paint the Mandelbrot and the Julia set with JavaFX. Also I explain how to change the colors of the graphical representations of the sets. Additional the look of the Julia set can be changed by adapting the function values.

In the article The Mandelbrot set I described what is behind the Mandelbrot set and how to paint it with JavaFX. This article shows how to extend the application so it is possible to paint the Julia set and change the look of the fractals.

For the implementation I used different JavaFX controls.

  • CheckBox: Switch between Mandelbrot and Julia set. Also to enable/disable a coordinate system.
  • ChoiceBox: To change the color schema of the set.
  • ColorPicker: Set a convergence color for the set.
  • TextField: Change the function values of the Julia set.

For the layout I used a GridLayout and a BorderPane layout. Also diverse EventHandlers are used to handle the user events, when a control changed its state or value.

The Mandelbrot and Julia set will be painted into a javafx.scene.canvas.Canvas with the methods GraphicsContext.setFill() and GraphicsContext.fillRect(). For the coordinate system I used the javafx.scene.shape.Line and javafx.scene.text.Text class and their Builder classes.

The following screenshot shows the finished application with the several controls and a Julia set:

Create New Project

This short video clip demonstrates the JavaFX-Fractal application in action:

I will not describe the Mandelbrot set here again (please see: The Mandelbrot set). The following paragraph explains the differences between the Mandelbrot and the Julia set.

The algorithm to calculate the color of the points of the Julia set is similar to the calculation of the colors in the Mandelbrot set. The Julia set is a set of points in a two-dimensional system, too. The Julia set is defined through the recursive definition z(n+1) = z(n)^2 + c where z(n) and c are complex numbers. This time we set start values for z(n) and iterate the values of c. The interesting points of the Julia set are between z = {-1,5..1,5} and zi = {-1,5..1,5}. (Note: z(n) = z + zi).

To generate a graphical representation of the Julia set you calculate the speed of the convergence (convergenceValue) of every point in the set. The convergenceValue is mapped to a color. So every point in the set has a color and the result is a graphical representation of the Julia set.

Because of the many parameters which are dependent of the set type (Mandelbrot or Julia set), I use a Bean class to store them. The name of the class is MandelbrotBean and contains the following variables: (The getters and setter are not listed.)

public class MandelbrotBean {

    public enum ColorSchema {

        GREEN, RED, YELLOW, BLUE, CYAN, MAGENTA
    }
    // Paint and calculation sizes
    private double reMin;
    private double reMax;
    private double imMin;
    private double imMax;
    // z + zi for Julia set
    private double z;
    private double zi;
    private int convergenceSteps;
    private Color convergenceColor = Color.WHITE;
    private ColorSchema colorSchema = ColorSchema.GREEN;

    public boolean isIsMandelbrot() {
        // if z is 0 then it is a Mandelbrot set
        return (getZ() == 0 && getZi() == 0) ? true : false;
    }

    public MandelbrotBean(int convergenceSteps, double reMin, double reMax, double imMin, double imMax, double z, double zi) {
        this.convergenceSteps = convergenceSteps;
        this.reMin = reMin;
        this.reMax = reMax;
        this.imMin = imMin;
        this.imMax = imMax;
        this.z = z;
        this.zi = zi;
    }

    ...

The function checkConvergence() was extended with the parameters z and zi, because now it is possible to pass start values for z(n). The rest of the function remains unchanged.

private int checkConvergence(double ci, double c, double z, double zi, int convergenceSteps) {
    for (int i = 0; i < convergenceSteps; i++) {
        double ziT = 2 * (z * zi);
        double zT = z * z - (zi * zi);
        z = zT + c;
        zi = ziT + ci;

        if (z * z + zi * zi >= 4.0) {
            return i;
        }
    }
    return convergenceSteps;
}

But when we call the method checkConvergence for the Julia set we have to switch the order of c(n) and z(n) compared to the order for the Mandelbrot set (see line 8 and 10). This might be a little bit confusing, because the parameter names in the method checkConvergence remains unchanged. (Are you confused? Thats ok.. ;-) ).

private void paintSet(GraphicsContext ctx, MandelbrotBean bean) {
    double precision = Math.max((bean.getReMax() - bean.getReMin()) / CANVAS_WIDTH, (bean.getImMax() - bean.getImMin()) / CANVAS_HEIGHT);

    double convergenceValue;
    for (double c = bean.getReMin(), xR = 0; xR < CANVAS_WIDTH; c = c + precision, xR++) {
        for (double ci = bean.getImMin(), yR = 0; yR < CANVAS_HEIGHT; ci = ci + precision, yR++) {
            if (bean.isIsMandelbrot()) {
                convergenceValue = checkConvergence(ci, c, 0, 0, bean.getConvergenceSteps());
            } else {
                convergenceValue = checkConvergence(bean.getZi(), bean.getZ(), ci, c, bean.getConvergenceSteps());
            }
            double t1 = (double) convergenceValue / bean.getConvergenceSteps();
            double c1 = Math.min(255 * 2 * t1, 255);
            double c2 = Math.max(255 * (2 * t1 - 1), 0);

            if (convergenceValue != bean.getConvergenceSteps()) {
                //Set color schema
                ctx.setFill(getColorSchema(c1, c2));
            } else {
                ctx.setFill(bean.getConvergenceColor());
            }
            ctx.fillRect(xR, yR, 1, 1);
        }
    }
}

The method paintSet was not changed much, too. Besides all parameters are read from the class MandelbrotBean and the ColorSchema is determined by the function getColorSchema(). Currently the application support six color schemas.

private Color getColorSchema(double c1, double c2) {
    MandelbrotBean.ColorSchema colorSchema = bean.getColorSchema();
    switch (colorSchema) {
        case RED:
            return Color.color(c1 / 255.0, c2 / 255.0, c2 / 255.0);
        case YELLOW:
            return Color.color(c1 / 255.0, c1 / 255.0, c2 / 255.0);
        case MAGENTA:
            return Color.color(c1 / 255.0, c2 / 255.0, c1 / 255.0);
        case BLUE:
            return Color.color(c2 / 255.0, c2 / 255.0, c1 / 255.0);
        case GREEN:
            return Color.color(c2 / 255.0, c1 / 255.0, c2 / 255.0);
        case CYAN:
            return Color.color(c2 / 255.0, c1 / 255.0, c1 / 255.0);
        default:
            return Color.color(c2 / 255.0, c1 / 255.0, c2 / 255.0);
    }
}

The TextFields for z

The TextFields z and zi for the Julia set are created in the methods createZTextField() and createZiTextField(). Both TextFields have an EventHandler which is called when the value in the TextField was changed by the user. After a change the set is repainted by calling paintSet.

private TextField createZTextField() {
    TextField zField = TextFieldBuilder.create()
            .text(String.valueOf(bean.getZ()))
            .prefWidth(60)
            .disable(true)
            .build();

    zField.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent t) {
            TextField textField = (TextField) t.getSource();
            String number = textField.getText();
            try {
                bean.setZ(Double.valueOf(number));
                paintSet(canvas.getGraphicsContext2D(), bean);
            } catch (NumberFormatException e) {
                textField.setText(String.valueOf(bean.getZ()));
            }
        }
    });

    return zField;
}

private TextField createZiTextField() {
    TextField ziField = TextFieldBuilder.create()
            .text(String.valueOf(bean.getZi()))
            .disable(true)
            .prefWidth(60)
            .build();

    ziField.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent t) {
            TextField textField = (TextField) t.getSource();
            String number = textField.getText();
            try {
                bean.setZi(Double.valueOf(number));
                paintSet(canvas.getGraphicsContext2D(), bean);
            } catch (NumberFormatException e) {
                textField.setText(String.valueOf(bean.getZi()));
            }
        }
    });
    return ziField;
}

The ColorPicker for the convergence color

The creation of a ColorPicker is very simple. JavaFX provides a very nice component to choose a color. You have to register a ChangeListener which calls the method paintSet if the color was changed by the user. That is all.

If you run the application you will see that the color of the Mandelbrot set (i.e. Julia set) changes "live". If you change the position of a slider you can see the color change in the representation of the set.

private ColorPicker createConvergenceColorPicker() {
    ColorPicker colorPicker = new ColorPicker(Color.WHITE);

    colorPicker.valueProperty().addListener(new ChangeListener<Color>() {
        @Override
        public void changed(ObservableValue ov, Color oldColorSchema, Color newColorSchema) {
            bean.setConvergenceColor(newColorSchema);
            paintSet(canvas.getGraphicsContext2D(), bean);
        }
    });

    return colorPicker;
}

The ChoiceBox for the color schema

To display the ColorSchemas I used a ChoiceBox. There you have to register a ChangeListener, too, which repaints the set if the ColorSchema was changed by the user.

private ChoiceBox createSchemaChoiceBox() {
    ChoiceBox colorSchema = new ChoiceBox(FXCollections.observableArrayList(MandelbrotBean.ColorSchema.values()));
    colorSchema.getSelectionModel().select(bean.getColorSchema());

    colorSchema.valueProperty().addListener(new ChangeListener<MandelbrotBean.ColorSchema>() {
        @Override
        public void changed(ObservableValue ov, MandelbrotBean.ColorSchema oldColorSchema, MandelbrotBean.ColorSchema newColorSchema) {
            bean.setColorSchema(newColorSchema);
            paintSet(canvas.getGraphicsContext2D(), bean);
        }
    });
    return colorSchema;
}

The Line and Text objects for the coordinate system

The coordinate system is created in the method createdCoordinateSystem. I am not very proud of this method. There is a lot of room for improvements. ;-) But for this small example application it is sufficient.

    private List<Node> createCoordinateSystem(MandelbrotBean bean) {
        List<Node> coordinateSystem = new ArrayList<>();

        double stepsX = (bean.getReMax() - bean.getReMin()) / CANVAS_WIDTH;

        int xPos = X_OFFSET;
        double eps = 0.01;
        for (double x = bean.getReMin(); x < bean.getReMax() + eps; x = x + stepsX, xPos++) {
            if (x == 0 || (x > 0 && x < 0.0001)) {
                // Paint y-Axis
                coordinateSystem.add(
                     LineBuilder.create()
                     .startX(xPos)
                     .startY(Y_OFFSET)
                     .endX(xPos)
                     .endY(500 + Y_OFFSET)
                     .strokeWidth(1)
                     .stroke(Color.RED)
                     .build());
                coordinateSystem.add(
                     TextBuilder.create()
                     .x(xPos - 20)
                     .y(Y_OFFSET - 10)
                     .text(String.valueOf(bean.getImMax()))
                     .styleClass("coordinate-system-text")
                     .build());
                coordinateSystem.add(
                     TextBuilder.create()
                     .x(xPos - 20)
                     .y(500 + Y_OFFSET + 25)
                     .text(String.valueOf(bean.getImMin()))
                     .styleClass("coordinate-system-text")
                     .build());
            } else if (Math.abs(x) == 1 || Math.abs(x) > 1 && Math.abs(x) < 1 + eps) {
                coordinateSystem.add(
                     LineBuilder.create()
                     .startX(xPos)
                     .startY(260 + Y_OFFSET)
                     .endX(xPos)
                     .endY(240 + Y_OFFSET)
                     .strokeWidth(1)
                     .stroke(Color.RED)
                     .build());
            }
        }

        // Paint x-Axis
        coordinateSystem.add(
             LineBuilder.create()
             .startX(X_OFFSET)
             .startY((HEIGHT/2)-Y_OFFSET).endX(WIDTH - X_OFFSET).endY((HEIGHT/2)-Y_OFFSET)
             .strokeWidth(1)
             .stroke(Color.RED)
             .build());

        // x-Text
        coordinateSystem.add(
             TextBuilder.create()
             .x(X_OFFSET - 50)
             .y(255 + Y_OFFSET)
             .text(String.valueOf(bean.getReMin()))
             .styleClass("coordinate-system-text")
             .build());
        coordinateSystem.add(
             TextBuilder.create()
             .x(749 + X_OFFSET + 10)
             .y(255 + Y_OFFSET)
             .text(String.valueOf(bean.getReMax()))
             .styleClass("coordinate-system-text")
             .build());

        return coordinateSystem;
    }

The method creates some lines and places four numbers. Every object is a Node which is stored in a List. This list will be added to the Pane later.

The CheckBox for the coordinate system and the switch between sets

Now we will create the CheckBox for the coordinate system. The CheckBox has an EventHandler which enables and disables the coordinate system if the user switches the CheckBox. The Node objects which are created by the method createCoordinateSystem are simply added and removed from the fractalRootPane.

private CheckBox createShowCoordinateCheckBox() {
    CheckBox showCoordinateSystemCheckBox = CheckBoxBuilder.create()
            .text("Coordinates")
            .styleClass("fractal-text")
            .selected(true)
            .onAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent e) {
            boolean checkBoxSelected = ((CheckBox) e.getSource()).isSelected();
            if (checkBoxSelected) {
                fractalRootPane.getChildren().addAll(coordinateSystemNodes);
            } else {
                fractalRootPane.getChildren().removeAll(coordinateSystemNodes);
            }
        }
    }).build();

    return showCoordinateSystemCheckBox;
}

Every time the user switched between the Mandelbrot and the Julia set the coordinate system is re-created. This is done by the method switchCoordnateSystem. (see createMandelbrotJuliaCheckBox())

private void switchCoordnateSystem(MandelbrotBean bean) {
    if (showCoordinateSystem.isSelected()) {
        fractalRootPane.getChildren().removeAll(coordinateSystemNodes);
        coordinateSystemNodes = createCoordinateSystem(bean);
        fractalRootPane.getChildren().addAll(coordinateSystemNodes);
    } else {
        //Also calculate new coordinate system when the system is not visible
        // later it maybe...
        coordinateSystemNodes = createCoordinateSystem(bean);
    }
}

The CheckBox for switching between the Mandelbrot and the Julia set has an EventHandler, too. If the set is switched a new MandelbrotBean with values is created and the method painSet is called.

private CheckBox createMandelbrotJuliaCheckBox() {
    CheckBox switchMandelbrotJuliaSetCheckBox = CheckBoxBuilder.create()
            .text("Switch Mandelbrot/Julia")
            .styleClass("fractal-text")
            .selected(true)
            .onAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent e) {
            boolean checkBoxSelected = ((CheckBox) e.getSource()).isSelected();
            if (checkBoxSelected) {
                bean = new MandelbrotBean(50, MANDELBROT_RE_MIN, MANDELBROT_RE_MAX, MANDELBROT_IM_MIN, MANDELBROT_IM_MAX, 0, 0);
                z.setDisable(true);
                zi.setDisable(true);

                canvas.setLayoutX(X_OFFSET);
                canvas.setLayoutY(Y_OFFSET);
            } else {
                bean = new MandelbrotBean(50, JULIA_RE_MIN, JULIA_RE_MAX, JULIA_IM_MIN, JULIA_IM_MAX, 0.3, -0.5);

                z.setDisable(false);
                zi.setDisable(false);

                // Reset value of z and zi
                z.setText(String.valueOf(bean.getZ()));
                zi.setText(String.valueOf(bean.getZi()));

                // Move canvans to the middlepoint
                canvas.setLayoutX(CANVAS_WIDTH / (bean.getReMax() - bean.getReMin()) / 2 + X_OFFSET/2);
                canvas.setLayoutY(CANVAS_HEIGHT/ (bean.getImMax() - bean.getImMin()) / 2 - Y_OFFSET*2);
            }
            bean.setColorSchema((MandelbrotBean.ColorSchema) colorSchemeChoiceBox.getSelectionModel().getSelectedItem());
            bean.setConvergenceColor(convergenceColorPicker.getValue());
            paintSet(canvas.getGraphicsContext2D(), bean);
            switchCoordnateSystem(bean);
        }
    }).build();

    return switchMandelbrotJuliaSetCheckBox;
}

Depending on the set type the TextFields z and zi are disabled. Also the Canvas have to be positioned in the center of the Pane.

The start method with the layout

The last thing to do is implementing the start method of the JavaFX application. This method creates the layout and calls the create methods above. For the layout of the controls a GridPane is used.

The following figure shows the GridPane with its columns and rows:

To place the GridPane and the Canvas a BorderPane is used.

public class JavaFXFractal extends Application {

    private static final int CANVAS_WIDTH = 750;
    private static final int CANVAS_HEIGHT = 600;
    // Left and right border
    private static final int X_OFFSET = 100;
    // Top and Bottom border
    private static final int Y_OFFSET = 50;
    // Width of the Application Scene
    private static final int WIDTH = (2 * X_OFFSET) + CANVAS_WIDTH;
    // Height of the Application Scene
    private static final int HEIGHT = (2 * Y_OFFSET) + CANVAS_HEIGHT;
    // Size of the coordinate system for the Mandelbrot set
    private static double MANDELBROT_RE_MIN = -2;
    private static double MANDELBROT_RE_MAX = 1;
    private static double MANDELBROT_IM_MIN = -1;
    private static double MANDELBROT_IM_MAX = 1;
    // Size of the coordinate system for the Julia set
    private static double JULIA_RE_MIN = -1.5;
    private static double JULIA_RE_MAX = 1.5;
    private static double JULIA_IM_MIN = -1.5;
    private static double JULIA_IM_MAX = 1.5;
    // List with the Nodes for the Coordinate system (Lines, Text)
    private List<Node> coordinateSystemNodes;
    // Main Panel with den Mandelbrot set
    private Pane fractalRootPane;
    // Canvas for the Mandelbrot oder Julia set
    private Canvas canvas = new Canvas(CANVAS_WIDTH, CANVAS_HEIGHT);
    private CheckBox showCoordinateSystem;
    private CheckBox switchMandelbrotJuliaSet;
    private ChoiceBox colorSchemeChoiceBox;
    private ColorPicker convergenceColorPicker;
    private TextField z;
    private TextField zi;
    private MandelbrotBean bean;

    @Override
    public void start(Stage primaryStage) {
        fractalRootPane = new Pane();

        // Move Canvas to the correct position
        canvas.setLayoutX(X_OFFSET);
        canvas.setLayoutY(Y_OFFSET);

        // Paint canvas with initial Mandelbrot set
        fractalRootPane.getChildren().add(canvas);
        bean = new MandelbrotBean(50, MANDELBROT_RE_MIN, MANDELBROT_RE_MAX, MANDELBROT_IM_MIN, MANDELBROT_IM_MAX, 0, 0);
        paintSet(canvas.getGraphicsContext2D(), bean);

        // Create controls
        coordinateSystemNodes = createCoordinateSystem(bean);
        switchMandelbrotJuliaSet = createMandelbrotJuliaCheckBox();
        showCoordinateSystem = createShowCoordinateCheckBox();
        convergenceColorPicker = createConvergenceColorPicker();
        colorSchemeChoiceBox = createSchemaChoiceBox();
        z = createZTextField();
        zi = createZiTextField();

        // Grid layout for the controls
        GridPane grid = new GridPane();
        grid.setHgap(10); // LEFT, RIGHT
        grid.setVgap(5); // TOP, BOTTOM
        grid.setPadding(new Insets(10, 10, 0, 10));
        // Show the GridLines
        //grid.setGridLinesVisible(true);

        grid.add(showCoordinateSystem, 0, 0);
        grid.add(switchMandelbrotJuliaSet, 0, 1);

        grid.add(TextBuilder.create().text("Color Schema: ").styleClass("fractal-text").build(), 1, 0);
        grid.add(TextBuilder.create().text("Convergence Color: ").styleClass("fractal-text").build(), 1, 1);

        grid.add(colorSchemeChoiceBox, 2, 0);
        grid.add(convergenceColorPicker, 2, 1);

        grid.add(LabelBuilder.create().text("z:").styleClass("fractal-text").build(), 3, 0);
        grid.add(LabelBuilder.create().text("zi:").styleClass("fractal-text").build(), 3, 1);

        grid.add(z, 4, 0);
        grid.add(zi, 4, 1);

        BorderPane border = new BorderPane();
        border.setTop(grid);
        border.setCenter(fractalRootPane);
        fractalRootPane.getChildren().addAll(coordinateSystemNodes);

        Scene scene = new Scene(border, WIDTH, HEIGHT);

        primaryStage.setTitle("JavaFX Fractal");
        primaryStage.setScene(scene);
        primaryStage.getScene().getStylesheets().add("fractal");
        primaryStage.show();
    }

 ...


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

CSS files

And the last thing is the CSS file fractal.css with its styling informations.

.root {
    -fx-background-color: black;
}

.coordinate-system-text {
    -fx-font-family: sans-serif;
    -fx-font-style: normal;
    -fx-font-weight: lighter;
    -fx-font-size: 16;
    -fx-fill: red;
    -fx-stroke: red;
}

.select-box {
    -fx-stroke: red;
    -fx-fill: null;
}

.fractal-text {
    -fx-padding: 5;
    -fx-background-color: black;
    -fx-text-fill: grey;
    -fx-fill: grey;


    -fx-font-family: sans-serif;
    -fx-font-style: normal;
    -fx-font-weight: lighter;
    -fx-font-size: 16;
}

Source code

The full source code is listed above. But for convenience I pushed it on GitHub. If you want to extent the application, you are free to do it. Pull requests are welcome...

GitHub: JavaFX-Fractal

Please note: I saw the typo Covergence, but I am not in the mood to make new screenshots. :-) If you find other typos don't hesitate to contact me via email.