Rendering a Julia set zoom animation in TypeScript
Fractals are beautiful mathematical constructs: infinitely repeating, self-similar, awe-inspiring artefacts of beauty. Lately I have been yearning to render one myself, for no particular reason other than to enjoy the act of creating such a thing. My target goal for this post will thus be to illustrate how I went from an empty project to a full (albeit low-quality) animation:
The Julia set
For this project, I chose to render the Julia set. Or, more specifically, a Julia set, given there are infinitely many. If you snoop on Wikipedia, you will find a very rigorous and likely very obscure definition. Given that my inuitive understanding of complex algebra is relegated to a deep memory of my univeristy days, I went straight away for the more practical construction. Here is what I got from it, in a more digestible form.
Let’s say we have a function f(z) that can be simply expressed as a polynomial in the complex domain over the input variable z. We can similarly define these functions:
- f2(z) = f(f(z))
- f3(z) = f(f(f(z)))
- …
And so on. In general, fn(z) is called the n-th iterate of f and is simply the result of repeatedly applying f to its own result n times. The iterations of f build a sequence. The Julia set of f is the set of all those complex numbers that make such sequence converge. In more mundane terms, no matter how many times you keep re-applying f onto itself, its result will always be bound by a finite constant.
There is a famous complex polynomial that is traditionally used to illustrate the Julia set - and its more distant cousing, the Mandelbrot set. Such polynomial is written as:
fc(z) = z2 + c
where c is an arbitrarily chosen constant. Since the behaviour of fc depends on the value of the constant, this actually defines a broad family of quadratic polynomials, each of which has its own particular Julia set. In the following paragraphs, I will refer to the Julia set of fc simply as “the Julia set”, implying that we are working on the specific quadric polynomial mentioned above, and that we have a fixed value of c.
Colouring points
Starting from the empirical construction given above, it follows that any point in the complex domain either belongs to the Julia set or not. So, how can its visualization contain all those sparkly colour gradients? The answer lies in the rendering implementation. Traditionally, computing whether a point belongs to the set is done through a loop. The number of iterations completed before exiting yields an estimation of how deep into the sequence we got before discovering it does not actually converge.
1 | class Complex { |
In literature, if fc is always less than 2 for a “good number” of iteration, we can assume that the current point belongs to the Julia set. Of course this is just an approximation that programs can use for faster rendering. Notice that in the code above I used 4 as my limit because the condition is applied to the squared magnitude: another trick to get rid of a very expensive square root operation that isn’t really necessary.
In the end, computeConvergence
returns a number between 0 and and 1. In broad terms we can say that this value is a kind of “fuzzy boolean” representing the truth value of the assertion “z belongs to the Julia set”. By defining a palette of various colours ordered in a continuous gradient, it is thus very easy to assign a different colour to each point. You can see how I implemented my colour palette here.
Rendering a complete image
In order to finally render a snapshot of the Julia set onto an image, we just need a few more things:
- the value of the constant c, of course;
- the complex viewport, i.e. which area of the complex plane we are currently displaying;
- and the resulting image resolution for the output.
In my approach, I decomposed the second requirement into two distinct parts that are simpler to assign for the user:
- the complex coordinate of the “focal point”, i.e. the complex number that is going to be rendered exactly in the center of the image. This makes zooming much easier because we just keep the center constant and we shrink the viewport around it symmetrically;
- the level of zoom currently applied, which is simply a number between 1 and… well, infinity, potentially - but for the sake of arguments, let’s say between 1 and 107. The limitation is mainly due to the fact that native floating point arithmetic isn’t suitable for such fine-grained computation. The precision barrier soon makes further zooming impossible without resorting to custom infinite-precision libraries.
In the following excerpt from my implementation, I use a node module called Jimp
to easily create and draw images in memory.
1 | import * as Jimp from 'jimp'; |
Notice that I use the square root of the converge value to interpolate a color: this is just an utility trick to obtain a more aestethically-pleasing result.
Finally, by keeping center
and imageSize
fixed, and by incrementing the zoom factor exponentially over time, it is possible to obtain a sequence of images that can be converted into a video. In my case, I simply used ffmpeg:
ffmpeg -f image2 -framerate 30 -i sequence/out%04d.jpg -vcodec mpeg4 -b 7M output.avi
Limitations and further improvements
The most obvious limitation is of course the JavaScript floating point airthmetic. It is possible to go around it by using other libraries for infinite precision arithemtic, but they have a huge performance impact on the program. A more refined approach would be to use the superfractal math paper, published some years ago now, to dramatically speed up computation of deep zooms; and to use native libraries for 128, 256 and 512 bit floating point operations. On top of that, note that this is a trivially parallelizable problem.
Additionally, my color scheme is very naive and gets stuck into a monochrome gradient after a short while. There are many solutions to the palette limitation, like using the smooth iteration count for better gradients. There is another article I read some time ago about how to create an infinite-recurring palette at any level of zoom using logarithmic level curves, but I can no longer find it 😢
Anyway, I hope you can play around with my code and enjoy some fractal exploration!