Years - and I do mean years - ago, I was tutoring someone in finite mathematics, and as I was reading through their textbook I was introduced to Julia sets. I've known about fractals and self-similarity in mathematics for a while by this point, but I never read into it very deeply.
There's Wikipedia pages which explain what a Julia set is in great depth, but to put it simply we can think of it like this - we take a point somewhere on the complex plane, think like the co-ordinate plane or grid you're familiar with from algebra or calculus. We feed that point's \(x\) and \(y\) value into a function, and get two new values out. Then we feed those back into the function again, and we keep doing that until we do it some maximum number of times or until the \(x\) and \(y\) values that are output get too big. The points that get too big, shooting off to infinity, are the Julia set. This set of points, even if we just highlighted it, would have a really neat fractal design that gets finer and finer as we zoom in closer and closer.
There's pseudocode on the Julia set Wikipedia page that describes how you can generally find the points of a Julia set, which I implemented as a set of Python scripts that can be used by a command line application, which I've pasted up below. However, what I also wanted to do is write code that would color the points of the Julia sets, depending on how long it takes them to shoot off to infinity, and where the last set of points ends up before they exceed our boundaries. There's basically infinitely many ways to do this. The first iteration I had was entirely grayscale, the second one I tried coloring points depending on what quadrant of the plane they ended up in, which made an interesting checkerboard pattern.
The third iteration, and the one below, ties the each of the three color channels to one of the co-ordinates or the number of iterations it took to escape, which creates these really neat, almost psychedelic primary color graphics. You can see one as the background for this page!
I also included the ability to generate animated gifs, essentially finding and coloring the Julia sets as we slowly change one function into another. This too can create some pretty interesting images, but they can take a little longer to generate.
import argparse
from math import floor, log
from PIL import Image
from sys import argv
import time
#startX, startY, im_width, im_height, animate, endX, endY, frames
parser = argparse.ArgumentParser(description='An image generator for Julia fractals.')
parser.add_argument('startX',default = 0, nargs = '?', type=float, help = 'Sets the (initial) real value to be used to generate the Julia set.')
parser.add_argument('startY',default = 0, nargs = '?', type=float, help = 'Sets the (initial) imaginary value to be used to generate the Julia set.')
parser.add_argument('im_width',default = 320, nargs = '?', type=int, help = 'Specifies the width of the generated image.')
parser.add_argument('im_height',default = 320, nargs = '?', type=int, help = 'Specifies the height of the generated image.')
parser.add_argument('r_max',default = 2, nargs = '?', type=float, help = 'Specifies max distance from 0 on the real axis.')
parser.add_argument('i_max',default = 2, nargs = '?', type=float, help = 'Specifies max distance from 0 on the imaginary axis.')
parser.add_argument('-animate',action='store_true', help = 'Specifies if an animated gif will be generated.')
parser.add_argument('endX',default = 1, nargs = '?', type=float, help = 'Sets the final real value to be used to generate the Julia set gif.')
parser.add_argument('endY',default = 1, nargs = '?', type=float, help = 'Sets the final imaginary value to be used to generate the Julia set gif.')
parser.add_argument('frames',default = 100, nargs = '?', type=int, help = 'Specifies the numnber of the frames in the gif.')
def image_gen(w,h,cx,cy,r_max,i_max):
fractal = Image.new('RGB', (w,h), (255, 255, 255))
bitmap = fractal.load()
for x in range(w):
for y in range(h):
n = 128
zx = r_max*(x-w/2)/(w/2)
zy = i_max*(y-h/2)/(h/2)
while zx**2 + zy**2 < 4 and n >= 1:
tx = zx**2 - zy**2 + cx
zy = 2*zx*zy + cy
zx = tx
n -= 1
shade = floor(255 * (1 - n/128))
# An old color method is commented out here - makes a very checkerboard like design
#if zx >= 0 and zy >= 0:
# bitmap[x,y] = (shade,shade,shade)
#if zx < 0 and zy >= 0:
# bitmap[x,y] = (shade,0,0)
#if zx < 0 and zy < 0:
# bitmap[x,y] = (0,shade,0)
#if zx >= 0 and zy < 0:
# bitmap[x,y] = (0,0,shade)
bitmap[x,y] = (abs(floor(zx/r_max*255)),abs(floor(zy/i_max*255)),shade)
return fractal
if __name__ == '__main__':
start_time = time.time()
args = parser.parse_args()
w, h = args.im_width, args.im_height
r_max, i_max = args.r_max, args.i_max
startX, startY = args.startX, args.startY
endX, endY = args.endX, args.endY
animate, frames = args.animate, args.frames
if animate:
cx_inc = (endX - startX)/frames
cy_inc = (endY - startY)/frames
frame_list = []
for i in range(frames):
next_frame = image_gen(w,h,startX + i*cx_inc, startY + i*cy_inc,r_max,i_max)
frame_list.append(next_frame)
print(str(round(i/frames*100)) + ' % complete in ' + str(round(time.time() - start_time,2)) + ' seconds.')
filename = ' '.join([str(startX),str(startY),'Animation','.gif'])
frame_list[0].save(filename, format='GIF', append_images=frame_list[1:], save_all=True, duration=1/12,loop=0)
else:
file = image_gen(w,h,startX,startY,r_max,i_max)
filename = ' '.join([str(startX),str(startY),'.gif'])
file.save(filename)
print(f'This was completed in {round(time.time() - start_time,2)} seconds.')