Skip to main content

How to Create the Mandelbrot Set with MoonBit?

· 8 min read

Have you ever watched the following video?

video source: https://www.youtube.com/shorts/MLEHL8hcKJk

It's the famous Mandelbrot set, a collection of points that form a fractal on the complex plane. This fractal is the most famous creation in the field of fractal theory, proposed by the mathematician Benoit B. Mandelbrot.

What makes this set truly extraordinary is that as you zoom in infinitely, you discover exquisite details, all generated by a simple formula. Some even consider the Mandelbrot set to be "the most peculiar and magnificent geometric form ever produced by mankind", often referred to as "God's fingerprint".

Today, we will show you what fractal theory is, how to create the Mandelbrot fractal using MoonBit, and discover the beauty of mathematics with MoonBit.

What is Fractal Theory?

To begin with, let's dive into what fractal theory is.

Fractal theory was created by Mandelbrot in 1975 and derives its name from the Latin term "fractus", which means "broken" or "fractured". The mathematical foundation of fractal theory lies in fractal geometry. Its fundamental characteristic is the description and study of objective phenomena from the perspective of fractional dimensions and mathematical methods.

Precisely because of this, fractal theory transcends the dimensions of our everyday world, allowing for a more concrete and realistic depiction of complex systems, providing insight into the intricacies and diversities of objective phenomena.

Because of the "infinite complexity" inherent in fractals, one might think they are difficult to create. In fact, it's a remarkably straightforward process. Creating a fractal involves repeating the same process over and over again, which in mathematical terms is an iterative equation.

One of the most famous fractals is the Mandelbrot set, which is based on a simple complex number cc. The mathematician Adrien Douady defined the following function:

fc(z)=z2+c f_c(z) = z^2 + c

In homage to Mandelbrot, it was named the Mandelbrot set. When iterated from z=0z=0, it does not diverge to infinity. Essentially, it's an iterative formula where the variables involved are complex numbers. As you perform computations according to this formula, local patterns tend to resemble the global structure, often concentrating in subtle details that require careful observation to discern.

How to Create the Mandelbrot Set with MoonBit?

Next, we will show you how to create the Mandelbrot set using MoonBit.

To define the region of the image we want to draw, we must need to know the concept of coordinate regions. A point in the complex plane is represented by a complex number (d=x+yi)(d=x+yi). Adding width and height helps to define a rectangular region on the complex plane.

Suppose an image has a width of ww pixels and a height of hh pixels. We need to calculate the colors of w×hw\times h pixels and then plot them.

We use Moonbit to handle the color calculation part, and then pass the computed colors to JavaScript, where we use canvas to draw the image.

Color Calculation

pub fn calc_color(col: Int, row: Int, ox: Double, oy: Double, width: Double) -> Int {
let pixel_size = width / image_width
let cx = (float_of_int(col) - coffset) * pixel_size + ox
let cy = (float_of_int(row) - roffset) * pixel_size + oy
var r = 0
var g = 0
var b = 0
var i = -1
while i <= 1 {
var j = -1
while j <= 1 {
let d = iter(
cx + float_of_int(i) * pixel_size / 3.0,
cy + float_of_int(j) * pixel_size / 3.0,
)
let c = get_color(d)
r = r + c.asr(16).land(0xFF)
g = g + c.asr(8).land(0xFF)
b = b + c.land(0xFF)
j = j + 1
}
i = i + 1
}
r = r / 9
g = g / 9
b = b / 9
return r.lsl(16).lor(g.lsl(8)).lor(b)
}

Here, we calculate the coordinates of the square's center in the complex plane represented by the pixel at row row and column col.

let pixel_size = width / image_width
let cx = (float_of_int(col) - coffset) * pixel_size + ox
let cy = (float_of_int(row) - roffset) * pixel_size + oy

We know that for a complex number cc, it belongs to the Mandelbrot set if and only if the infinite sequence of complex numbers generated by the following recursive definition always stays within a circle in the complex plane centered at the origin with a radius of 22: z0=0; zn=zn12+cz_0 = 0;\ z_n = z_{n-1}^2 + c; If we express zkz_k as xk+ykix_k+y_ki, separating the real and imaginary parts, and also express c as cx+cyic_x+c_yi (where cxc_x and cyc_y are the real and imaginary parts of cc, respectively), then the recursive definition above can be rewritten as: x0=0,y0=0; xn=xn12yn12+cx,yn=2xn1yn1+cyx_0 = 0, y_0 = 0;\ x_n = x_{n-1}^2 - y_{n-1}^2 + c_x, y_n = 2x_{n-1}y_{n-1} + c_y. A complex number cx+cyic_x+c_yi belongs to the Mandelbrot set if and only if, for all natural numbers nn, xn2+yn2<22=4x_n^2+y_n^2 < 2^2 = 4.

The calc_color function then calls iter to calculate xnx_n and yny_n. This function returns the number of iterations at which the escape radius is first exceeded, or 1.0-1.0 if it iterates max_iter_number times without escaping.

pub fn iter(cx : Double, cy : Double) -> Double {
var x = 0.0
var y = 0.0
var newx = 0.0
var newy = 0.0
var smodz = 0.0
var i = 0
while i < max_iter_number {
newx = x * x - y * y + cx
newy = 2.0 * x * y + cy
x = newx
y = newy
i = i + 1
smodz = x * x + y * y
if smodz >= escape_radius {
return float_of_int(i) + 1.0 - log(log(smodz) * 0.5) / log(2.0)
}
}
return -1.0
}

Next, we need to choose the appropriate color based on the returned number of iterations. To begin with, we need a color palette, and this is the fuction of interpolation, which is used to generate a color gradient.

fn interpolation(f : Double, c0 : Int, c1 : Int) -> Int {
let r0 = c0.asr(16).land(0xFF)
let g0 = c0.asr(8).land(0xFF)
let b0 = c0.land(0xFF)
let r1 = c1.asr(16).land(0xFF)
let g1 = c1.asr(8).land(0xFF)
let b1 = c1.land(0xFF)
let r = floor((1.0 - f) * float_of_int(r0) + f * float_of_int(r1) + 0.5)
let g = floor((1.0 - f) * float_of_int(g0) + f * float_of_int(g1) + 0.5)
let b = floor((1.0 - f) * float_of_int(b0) + f * float_of_int(b1) + 0.5)
return r.lsl(16).lor(g.lsl(8).lor(b))
}

The get_color function first performs some transformations on the iteration count and then passes it to the interpolation function to obtain the corresponding color.

pub fn get_color(d : Double) -> Int {
if d >= 0.0 {
var k = 0.021 * (d - 1.0 + log(log(128.0)) / log(2.0))
k = log(1.0 + k) - 29.0 / 400.0
k = k - float_of_int(floor(k))
k = k * 400.0
if k < 63.0 {
return interpolation(k / 63.0, 0x000764, 0x206BCB)
} else if k < 167.0 {
return interpolation((k - 63.0) / (167.0 - 63.0), 0x206BCB, 0xEDFFFF)
} else if k < 256.0 {
return interpolation((k - 167.0) / (256.0 - 167.0), 0xEDFFFF, 0xFFAA00)
} else if k < 342.0 {
return interpolation((k - 256.0) / (342.0 - 256.0), 0xFFAA00, 0x310230)
} else {
return interpolation((k - 342.0) / (400.0 - 342.0), 0x310230, 0x000764)
}
} else {
return 0x000000
}
}

The caculation of color is now complete.

Drawing with Canvas

Create a canvas

<html>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

Obtain the canvas element in the JavaScript code and set its size.

let canvas = document.getElementById('canvas')
var IMAGEWIDTH = 800
var IMAGEHEIGHT = 600
canvas.width = IMAGEWIDTH
canvas.height = IMAGEHEIGHT

Create an ImageData object to store the computed pixel colors.

var imagedata = context.createImageData(IMAGEWIDTH, IMAGEHEIGHT)

Then import the wasm compiled from the MoonBit code above:

WebAssembly.instantiateStreaming(
fetch('target/mandelbrot.wasm'),
spectest
).then((obj) => {
obj.instance.exports._start()
const calcColor = obj.instance.exports['mandelbrot/lib::calc_color']
const drawColor = obj.instance.exports['mandelbrot/lib::draw_color']

//...
})

Draw image:

function saveImage() {
context.putImageData(imagedata, 0, 0)
}

function generateImage() {
for (row = 0; row < IMAGEHEIGHT; row++) {
for (col = 0; col < IMAGEWIDTH; col++) {
let x = +ox.value
let y = +oy.value
let w = +width.value
var color = calcColor(col, row, x, y, w)
drawColor(imagedata, col, row, color)
}
}

saveImage()
}

This is the final presented rendering:

The rendering of the Mandelbrot involves numerous mathematical derivations, which are not extensively explained in this blog post. You can refer to: https://en.wikipedia.org/wiki/Mandelbrot_set

Complete code: https://github.com/moonbitlang/moonbit-docs/pull/69/files

That is all we want to share today. We look forward to your creations.