The other day I got obsessed with trying to find the smallest possible code to create the most interesting visual complexity. Fractals and in particular the Mandelbrot set come to mind quickly, so I wanted to visualize the Mandelbrot set, and I wanted to do it in Javascript in such a way that I can just paste it into a browser’s URL bar.
The Mandelbrot set is defined by an iterative equation in the complex plane: you calculate the following equation until its absolute value is larger than 4, or until some predefined number of iterations are done and the value hasn’t converged. You use the number of iterations in each point as the value at those particular coordinates.
So the real part becomes
And the imaginary part is
Now you just traverse x and y of a canvas, use those as coordinates cr and ci in the equation above, and iterate until the absolute value of the complex number is larger than 4, or you exceed some maximum amount of iterations.
Now you confront the question of how to visualize the set: you have an iteration count for each coordinate in your image, but how do you draw that? You can do all kinds of color tricks (for example, RGB = {# iterations, # iterations + 128, # iterations + 64} looks nice), but I thought it would look nice to just show contours. So I’m calculating and plotting the binary difference in r- and i-direction in each image (using just one array for that in the code). In the code, that’s variable p.
I wanted to have something dynamic and interesting to watch though, and this just creates a static image. So instead, let’s successively zoom into the Mandelbrot set. The Mandelbrot set is a fractal, so it’s self-similar and infinite. You can just pick a smaller and smaller range for variables cr and ci in floating point, and each range will produce results. So the first step is easy: pick a new starting coordinate for each new Mandelbrot image by multiplying the previous coordinate with a constant scale factor (and do some corrections to keep the image centered in your canvas). That’s the second r0 and i0 update block in the code. s is the current scale that gets multiplied with the same factor in each image iteration (I chose 0.98, decrease it to make it zoom more quickly).
The problem is now that this visualization can quickly become boring, if you’re looking at the wrong place in the complex plane where there’s nothing much going on. So I want to modify the starting coordinates for each frame in each image iteration in such a way that the algorithm is “looking for” the greatest complexity in the image. We do that by calculating the center of gravity of black pixels in each image, essentially as the simple sum of all vectors pointing from the center of the frame to each pixel (variables wx and wy). In each image iteration, we now want the focus to move away from the center of gravity if there is an overhang of black pixels, or towards the center of gravity if not. That’s determined by a running sum of black (1) and white (-1) pixels in variable w. Then you can calculate each image iteration’s movement in the x- and y-direction as the product of the signs of variables wx and w or wy and w, respectively.
The more you zoom in, the longer you need to iterate in the complex equation above to find any kind of interesting “information”. Otherwise you’ll soon find yourself in just black or white. So I’m solving that in an incredibly simple way: if the density of black pixels falls below 20% of the overall image, increase the iteration range m by 1. That means the zoom operation will never run out of interesting stuff. (I guess the floating point range could run out? I don’t know how Javascript reflects those, to be honest. I thought I could replace floating point variables with integers and moving the fixed point, but it’s not THAT trivial, although I think there is an approximation that could be found somewhere in the fact that we put a limit of 4 on the absolute value, but anyway.)
Finally, this can get pretty slow, particularly further “down” in the Mandelbrot set. So I want to downsample the whole thing, and only show every b-th pixel, and then blow those out in the image. So the code does exactly that, skips to each b-th pixel and then draws b pixels in x- and y-direction around it. It seems to me that b=2 works well on a laptop, and b>=3 on a mobile browser.
Randomize the starting point within a small predefined range, and because the Mandelbrot set is chaotic (i.e., starting conditions extremely quickly lead to unpredictability in the set), the journey will look extremely different each time.
Here is the code:
https://github.com/marioschlosser/urlfractal/blob/main/fractal.html
Or copy and paste this into the browser address bar:
data:text/html,<canvas id=’v’></canvas><script>ca=document.getElementById(‘v’);ct=ca.getContext(‘2d’);cw=890;ch=890/window.innerWidth*window.innerHeight;ca.width=cw;ca.height=ch;r0=-2.0*Math.random();rs=3.0;i0=-1.5*Math.random();is=rs/cw*ch;m=80;s=0.98;b=2;g=ct.createImageData(cw,ch);function main(){window.requestAnimationFrame(main);wx=0;wy=0;w=0;p=0;d=0;for(x=0;x<cw/b;x++){for(y=0;y<ch/b;y++){k=(y*b*cw+x*b)*4;r=r0+(x*b/cw)*rs;i=i0+(y*b/ch)*is;zr=0;zi=0;n=0;while((zr*zr+zi*zi<=4)&&(n<m)){zrn=zr*zr-zi*zi+r;zin=2*zr*zi+i;zr=zrn;zi=zin;n+=1;}c=255-parseInt(n*255/m);g[‘data’][k]=c;if(k<=cw*b*4) continue;p=((g[‘data’][k-cw*b*4]!=c)&&(g[‘data’][k-b*4]!=c));for(b_y=0;b_y<b;b_y++){for(b_x=0;b_x<b;b_x++){t=k+(-cw*b+cw*b_y+b_x)*4;g[‘data’][t]=p?0:255;g[‘data’][t+1]=p?0:255;g[‘data’][t+2]=p?0:255;g[‘data’][t+3]=255;}}wx+=p?(r-(r0+rs/2)):0;wy+=p?(i-(i0+is/2)):0;w+=p?1:-1;d+=p?1:0;}}m+=d/(x*y)<0.2?1:0;ct.putImageData(g,0,0);r0+=rs/100*Math.sign(wx)*-Math.sign(w);i0+=is/100*Math.sign(wy)*-Math.sign(w);r0+=rs/2*(1-s);i0+=is/2*(1-s);rs*=s;is*=s;}main();</script>