Below are Java implementations of various fractals. All examples are built with the core Processing library, which is under Creative Commons licensing. Some examples utilize complex number functionality with jcomplexnumber which is licensed under an MIT license, and these are credited respectively.


A Note on Arbitrary Resolution, Size, and Time

The implementations on this page allow for very high resolution renderings of these fractals. So high, in fact, that the image size of the resultant PNG files can easily be specified well beyond what any program capable of opening PNG files can actually manage (though if you enjoy watching Paint crash, go for it ;^] ).

These implementations subvert this issue by simply dividing the whole image into many PNG files. The image is "partitioned" into rectangles such that there are an equal number of partitions of both the horizontal and vertical axes. This is specified with the constant PER_AXIS_PARTITIONS. For example, a PER_AXIS_PARTITIONS = 3 would result in 9 PNG files that together form a 3x3 grid of the whole image.

In theory (and in practice when I've been bored), the implementations on this page can be implemented with arbitrary precision, thus eliminating the limitations of double precision. A number of classes are available for this in Java. However, these implementations are universally extraordinarily slow. Such a goal would certainly be more sensible in a high-performance language, which is on my bucket list of future projects.

Multi-Julia Sets

A Short Primer on Julia Sets

Julia sets are a topic of complex dynamics. Each is defined by a holomorphic complex function \(f\). Fractal images can be generated with these defining functions by choosing a domain in the complex plane and iterating \(f\) on each input within that domain. Since \(f\) is complex, it takes this complex input and returns a complex output, making this iteration possible. If upon iteration a given input escapes a predefined escape radius from the origin within a predefined number of maximum iterations, said given input's corresponding location on the image will be colored similarly to other inputs that behave the same (have the same number of iterations).

Some popular defining functions are complex quadratic polynomials, which take the form

\[f_c(z)=z^2+c\]

where \(c\) is a predetermined complex parameter. For the implementation below, possible defining functions have been generalized further to any complex polynomial

\[f_c(z)=z^p+c\]

where \(p\) is the degree of this polynomial. Julia sets defined by complex polynomial functions that have an exponent greater than 2 are typically referred to as multi-Julia sets.

A Julia set itself is all of the values in which a small perturbation in the input results in a drastic change in the iteration count (in complex dynamics, this is known as chaos), hence these sets of values are the ones which are likely to generate more remarkable images under an iteration visualization technique. The set of values that do no exhibit this chaotic behavior are called Fatou sets.

Details of this Multi-Julia Set Visualization

Implementing the iteration from a given value and keeping track of how many iterations before escaping the radius is relatively trivial. Deciding how best to map this to a dynamically specifiable raster is a bit tricky. This was my approach.

For this implementation, the overall image is always centered at the origin. The total dimensions of the domain in the complex plane are specified by X_RANGE and Y_RANGE. The number of pixels that represent each partition (see note at the top of the page) of those real and imaginary dimensions are given by X_DENSITY and Y_DENSITY on the horizontal and vertical axes of the image, respectively.

For example, if PER_AXIS_PARTITION=1, X_RANGE = 2.2 and X_DENSITY = 500, then 500 horizontal pixels will represent -1.1 to 1.1 on the real-axis.

If PER_AXIS_PARTITION=2, X_RANGE = 2.2 and X_DENSITY = 500, then 500 horizontal pixels will represent -1.1 to 0 on the real-axis in one image, and another 500 horizontal pixels will represent 0 to 1.1 on the real-axis in another image.

The same logic applies to vertical pixels' mapping to the imaginary axis.

A Summary of Important Global Variables

  • COMP_PAR the defining complex parameter \(c\)
  • FUNC_EXPON the degree \(p\) of the defining complex polynomial
  • X_RANGE and Y_RANGE dimensions of the domain in the complex plane
  • X_DENSITY and Y_DENSITY number of pixels that represent each partitionof those real and imaginary dimensions
  • PER_AXIS_PARTITIONS number of partitions of both the horizontal and vertical axes
  • MAX_ITERATION maximum iteration of any single starting value

Limitations

As mentioned above, we are still bound by the limitations of double precision. This does not, however, prevent us from obtaining extremely beautiful high-resolution visualizations of multi-Julia sets.

A slight missed opportunity was the assumption of a center at the origin, though this is something I may improve in future versions.

Example Imagery

Still Images

Here is a render small enough that I feel comfortable embedding it on this page directly:

A higher-resolution of the same parameters, with a different color gradient:

Higher-Resolution Image Here (11000x11000)

Finally, the same parameters, divided into 5 partitions per axis, with each partition having the same 11000x11000 resolution as the image in the "Higher-Resolution" image above! (Empty partitions are excluded.)

1 2 3 4 5
1 Empty Empty Empty
2 Empty
3
4 Empty
5 Empty Empty Empty

Video

One benefit of choosing not to implement arbitrary precision is the fairly quick rendering time. Below is a 4k video made utilizing this implementation by changing the \(c\) parameter over time. This is a "Non-Standard" Multi-Julia set, as the exponent in the complex polynomial is 4. This took my fairly modest machine about 3 hours to render the images for.

Java Implementation

Each function is and group of variables are fairly well annoted throughout by the in-source comments/documentation, which is provided below.

import processing.core.PApplet;
import processing.core.PImage;

/**
 * This class exports visualizations of Julia Sets as PNG files in a /juliaSills directory
 * utilizing Processing Application functionality.
 * 
 * This class also utilizes jcomplexnumber's ComplexNumber class, which is abbreviated CN. 
 * 
 * @author Matthew Hefner
 *
 */
public class JuliaSet extends PApplet{
    // Domain on the Real-Axis
    private final double X_RANGE = 2.2;
    
    // Domain on the Imaginary-Axis
    private final double Y_RANGE = 2.2;
    
    // Pixel density per section on the Real-Axis
    private final int X_DENSITY = 500;
    
    // Pixel density per section on the Imaginary-Axis
    private final int Y_DENSITY = 500;
    
    // Number of partitions on each axis in the complex plane
    // i.e., total number of sections is this value squared
    private final int PER_AXIS_PARTITIONS = 1;
    
    // Maximum number of iterations to determine divergence 
    private final int MAX_ITERATION = 256;
    
    // Complex parameter that defines a particular julia set
    private final CN COMP_PAR = CN.multiply(new CN(0.7885,0),  
            CN.exp(new CN(0, ((float) 285) / 200)));
    
    // Iteration escape radius
    private final byte ESCAPE_RADIUS = 4;
    
    // Exponent of the defining iteration function
    private final int FUNC_EXPON = 4;
    
    // Iteration increment variable and temp variable used in calculations, 
    // declared as a fields for faster garbage collection on large renders
    private int iteration;
    private double xtmp;
    
    /**
     * The main method creates a new PApplet defined by this class and runs it.
     * 
     * @param args  Not used
     */
    public static void main(String[] args){
        String[] processingArgs = {"MySketch"};
        JuliaSet sketch = new JuliaSet();
        PApplet.runSketch(processingArgs, sketch);
    }
    
    /**
     * Mandatory method for processing applets.  All PApplets initialize
     * a window for the application; this window will only be used
     * as an indicator for when the program has compled rendering
     * all of the sections of the fractal image.
     */
    public void settings(){
        // Size of application window
        size(400, 400);
    }
    
    /**
     * Mandatory method for processing applets.  Ran after settings,
     * this method establishes that the draw() method will only run
     * once, that colors described will be in Hue/Sat/Bri format.
     */
    public void setup() {
        noLoop();
        colorMode(HSB);
    }
    
    /**
     * Mandatory method for processing applets.  Ran after setup,
     * this method will loop through the sections and call the
     * fractal image rendering method for each.
     */
    public void draw(){
        // Backrgound of application window is black until rendered
        background(0);
        
        for (int section = 0; section < PER_AXIS_PARTITIONS * PER_AXIS_PARTITIONS; section++) {
            // call to the fractal image rendering method
            produceFractalPart(
                    // New PImage for the section
                    createImage((int) (X_DENSITY * X_RANGE / PER_AXIS_PARTITIONS),
                    (int) (Y_DENSITY * Y_RANGE / PER_AXIS_PARTITIONS), RGB),
                    // 0-indexed Real-Axis Section
                    section % PER_AXIS_PARTITIONS,
                    // 0-indexed Imaginary-Axis Section
                    section / PER_AXIS_PARTITIONS);
        }
        
        // Backrgound of application window turns white when 
        // all sections are rendered
        background(255);
    }
    
    /**
     * Fractal image rendering and export method.
     * @param img   The PImage on which to render the fractal image.
     * @param secX  The 0-indexed Real-Axis Section
     * @param secY  The 0-indexed Imaginary-Axis Section
     */
    public void produceFractalPart(PImage img, int secX, int secY) {
        // Load the pixels of the PImage
        img.loadPixels();
        
        // Locates section's domain in the complex plane
        double xDisplacement = (secX * X_RANGE / PER_AXIS_PARTITIONS - X_RANGE / 2);
        double yDisplacement = (secY * Y_RANGE / PER_AXIS_PARTITIONS - Y_RANGE / 2);
        
        // Loop through each pixel in the domain
        for (int i = 0; i < img.width; i++) {
            for (int j = 0; j < img.height; j++) {
                // Initializes Z, ComplexNumber corresponding to the pixel 
                double ix = ((double) i / img.width) * (X_RANGE / PER_AXIS_PARTITIONS) + xDisplacement;
                double jx = ((double) j / img.height) * (Y_RANGE / PER_AXIS_PARTITIONS) + yDisplacement;
                CN z = new CN(ix, jx);
                
                // Call to the julia iteration function and sets the pixel to
                // the returned color (in integer integer as definited by Processing)
                img.pixels[i + img.width * j] = iterateMultiJulia(z);
            }
        }
        
        // Updates pixels for export of image
        img.updatePixels();
        // Exports section's fractal image as a PNG to /juliaStills/ subdir
        img.save("juliaStills/" + (secY * PER_AXIS_PARTITIONS + secX) + ".png");
    }
    
    /**
     * This method takes a complex number input and iterates the 
     * defining function on that input until either the max number
     * of iterations have occurred or the escape radius is escaped.
     * 
     * @param z     Complex Input
     * @return      Color corresponding to number of iterations
     */
    public int iterateMultiJulia(CN z) {
        // obtain input's real and imaginary components
        double zx = z.getRe();
        double zy = z.getIm();
        
        // initialize iteration to 1
        iteration = 1;
        
        // Iterate; stop when escape radius is left or max iteration is achieved
        while (zx * zx + zy * zy < ESCAPE_RADIUS  &&  iteration < MAX_ITERATION) 
        {
            // Set real and imaginary components to output of the defining function
            // Easiest done with this temporary variable
            xtmp = Math.pow((zx * zx + zy * zy), 
                    (FUNC_EXPON / 2)) * Math.cos(FUNC_EXPON * Math.atan2(zy, zx)) 
                    + (float) COMP_PAR.getRe();
            zy = Math.pow((zx * zx + zy * zy), 
                    (FUNC_EXPON / 2)) * Math.sin(FUNC_EXPON * Math.atan2(zy, zx)) 
                    + (float) COMP_PAR.getIm();
            zx = xtmp;
            
            // Increment iteration counter
            iteration = iteration + 1;
        }
        
        // Calculate and return HSB color integer based on number of iterations
        // Tweak here for aethetic purposes!
        return color(iteration, 255, iteration);
    }
}

Burning Ship

Using a similar implementation as above, we can achieve very different and aesthetically impressive fractal images by relaxing our requirements for the defining function.

For Julia sets, we required an analytic function (i.e., the functions obey the Cauchy-Riemann equations). If instead we do not, we may use the function

\[f_c(z)=(|\text{Re}(z)|+i|\text{Im}(z)|)^2+c\]

By doing so, we may generate images of the Burning Ship.

The other main difference between this iterative method and the one used to visualize Julia sets is that the pixels of the fractal images -- and their corresponding locations on the complex plane -- are now the complex parameter \(c\). The function is iterated at every point with the assumption that the first \(z_0=0\).

Implementation

The following changes to the Julia code above are necessary and sufficient to generate images of the Burning Ship.

Since the center focus will change from simply the origin, we must create a new field for it:

public class BurningShip extends PApplet{
    // Center focus of the image
    private CN center = new CN(0,0);
    
    ...
    

We can also eliminate the field COM_PAR since the location in the domain will serve as our new \(c\). The FUNC_EXPON and xtmp are no longer necessary since the function is more strictly defined.

BurningShip sketch = new BurningShip(); should replace JuliaSet sketch = new JuliaSet(); in the main method. This simply tells the processing library that we are running our BurningShip sketch.

Next, we will be passing a \(c\) into the iteration function, so the produceFractalPart method should change to reflect this. We should also take into account that our center is no longer at the origin in our displacements. The new method will look like this:

/**
 * Fractal image rendering and export method.
 * @param img   The PImage on which to render the fractal image.
 * @param secX  The 0-indexed Real-Axis Section
 * @param secY  The 0-indexed Imaginary-Axis Section
 */
public void produceFractalPart(PImage img, int secX, int secY) {
    // Load the pixels of the PImage
    img.loadPixels();
    
    // Locates section's domain in the complex plane
    double xDisplacement = (secX * X_RANGE / PER_AXIS_PARTITIONS - X_RANGE / 2) + center.getRe();
    double yDisplacement = (secY * Y_RANGE / PER_AXIS_PARTITIONS - Y_RANGE / 2) + center.getIm();
    
    // Loop through each pixel in the domain
    for (int i = 0; i < img.width; i++) {
        for (int j = 0; j < img.height; j++) {
            // Initializes c, ComplexNumber corresponding to the pixel 
            double ix = ((double) i / img.width) * (X_RANGE / PER_AXIS_PARTITIONS) + xDisplacement;
            double jx = ((double) j / img.height) * (Y_RANGE / PER_AXIS_PARTITIONS) + yDisplacement;
            CN c = new CN(ix, jx);
            
            // Call to the julia iteration function and sets the pixel to
            // the returned color (in integer integer as definited by Processing)
            img.pixels[i + img.width * j] = iterateBurningShip(c);
        }
    }
    
    // Updates pixels for export of image
    img.updatePixels();
    // Exports section's fractal image as a PNG to /juliaStills/ subdir
    img.save("shipStills/1.png");
}

Finally, we must create an iterateBurningShip method.

/**
 * This method takes a complex number input and iterates the 
 * defining function on that input until either the max number
 * of iterations have occurred or the escape radius is escaped.
 * 
 * @param z     Complex Input
 * @return      Color corresponding to number of iterations
 */
public int iterateBurningShip(CN c) {
    // initialize z_0 = 0
    double zx = 0;
    double zy = 0;
    
    // initialize iteration to 1
    iteration = 1;
    
    // Iterate; stop when escape radius is left or max iteration is achieved
    while (zx * zx + zy * zy < ESCAPE_RADIUS  &&  iteration < MAX_ITERATION) 
    {
        // Set real and imaginary components to output of the defining function
        // Easiest done with this temporary variable
        float xtemp = (float) (zx*zx - zy*zy + c.getRe());
        zy = abs((float) (2*zx*zy)) + c.getIm(); //abs returns the absolute value
        zx = abs(xtemp);
        
        // Increment iteration counter
        iteration = iteration + 1;
    }
    
    // Calculate and return HSB color integer based on number of iterations
    // Tweak here for aethetic purposes!
    return color(256 - map(iteration, 0, 64, 0, 256), 255, map(iteration, 0, 32, 0, 256));
}
    

Example Images



Original code presented this site is under Creative Commons licensing which makes it possible to re-use this content for non-commercial purposes.