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 EventHandler
s 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:
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 TextField
s for z
The TextField
s z
and zi
for the Julia set are created in the methods createZTextField()
and createZiTextField()
. Both TextField
s 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 ColorSchema
s 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 TextField
s 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.