Mandelbrot Set

Location: Mathematics / Fractals / Mandelbrot Set

Summary

This article aims to explain the theoretical basis of the Mandelbrot set and also to provide a JavaScript visualization of the Mandelbrot set fractal (HTML canvas, full source code, code guide, JsFiddle for experimenting, screenshots). The theoretical part is an almost complete copy of the article "Introduction to the Mandelbrot Set. A guide for people with little math experience.", 1996, by David Dewey, which is no longer unavailable online (the original publication URL is http://www.ddewey.net/mandelbrot/ ).

bic.wiki / Mandelbrot Set with JavaScript source code

Status

This article is under reconstruction:

  • The "Source Code" section should be merged into the "Code Guide" section with detailed explanation of every part of the source code.

Introduction to the Mandelbrot Set. A guide for people with little math experience. By David Dewey

The Mandelbrot set, named after Benoit Mandelbrot, is a fractal. Fractals are objects that display self-similarity at various scales. Magnifying a fractal reveals small-scale details similar to the large-scale characteristics. Although the Mandelbrot set is self-similar at magnified scales, the small scale details are not identical to the whole. In fact, the Mandelbrot set is infinitely complex. Yet the process of generating it is based on an extremely simple equation involving complex numbers.

Understanding complex numbers

The Mandelbrot set is a mathematical set, a collection of numbers. These numbers are different than the real numbers that you use in everyday life. They are complex numbers. A complex number consists of a real number plus an imaginary number. The real number is an ordinary number, for example, -2. The imaginary number is a real number times a special number called i, for example, 3i. An example of a complex number would be -2 + 3i.

The number i was invented because no real number can be squared (multiplied by itself) and result in a negative number. This means that you can not take the square root of a negative number and get a real number. By definition, when you take the square root of a number, you find a number that can be squared to get that number. The number i is defined to be the square root of -1. This means that i squared is equal to -1. So when you square an imaginary number you can get a negative number. For example, 3i squared is -9.

The real number line

The real number line (Mandelbrot set, Bio-Inspired Computing Wiki, bic.wiki)
Real numbers can be represented on a one dimensional line called the real number line. Negative numbers like -2 are plotted to the left of zero and positive numbers like 2 are plotted to the right of zero. Any real number can be graphed on the real number line.

The complex number plane

The complex plane (Mandelbrot set, Bio-Inspired Computing Wiki, bic.wiki)
Since complex numbers have two parts, a real one and an imaginary one, we need a second dimension to graph them. We simply add a vertical dimension to the real number line for the imaginary part. Since our graph is now two-dimensional, it is a plane, the complex number plane. We can graph any complex number on this plane. The colored dots on this graph represent the complex numbers [2 + 1i] (red), [-1.5 + 0.5i] (blue), [2 - 2i] (lime), [-0.5 - 0.5i] (green), [0 + 1i] (pink), and [2 + 0i] (khaki).

Graphing the Mandelbrot set

The Mandelbrot set is a set of complex numbers, so we graph it on the complex number plane. However, first we have to find many numbers that are part of the set. To do this we need a test that will determine if a given number is inside the set or outside the set. The test is based on the equation Z = Z² + C. C represents a constant number, meaning that it does not change during the testing process. C is the number we are testing, the point on the complex plane that will be plotted when testing is complete. Z starts out as zero, but it changes as we repeatedly iterate this equation. With each iteration we create a new Z that is equal to the old Z squared plus the constant C. So the number Z keeps changing throughout the test.

We're not really interested in the actual value of Z as it changes, we just look at its magnitude. The magnitude of a number is its distance from zero. For example, the number -9 is a distance of 9 from zero, so it has a magnitude of 9. The magnitude of a complex number is harder to measure. To calculate it, we add the square of the number's distance from the x-axis (the horizontal real axis) to the square of the number's distance from the y-axis (the imaginary vertical axis) and take the square root of the result. In this illustration, a is the distance from the y-axis, b is the distance from the x-axis, and d is the magnitude, the distance from zero.

Complex number vector magnitude (Mandelbrot set, Bio-Inspired Computing Wiki, bic.wiki)

As we iterate our equation, Z changes and the magnitude of Z also changes. The magnitude of Z will do one of two things. It will either stay equal to or below 2 forever, or it will eventually surpass two. Once the magnitude of Z surpasses 2, it will increase forever. In the first case, where the magnitude of Z stays small, the number we are testing is part of the Mandelbrot set. If the magnitude of Z eventually surpasses 2, the number is not part of the Mandelbrot set (look below at the "Formal definition" section for a proff).

Mandelbrot set complex number magnitude over iterations (Mandelbrot set, Bio-Inspired Computing Wiki, bic.wiki)

As we test many complex numbers we can graph the ones that are part of the Mandelbrot set on the complex number plane. If we plot thousands of points, an image of the set will appear:

Mandelbrot set points (Mandelbrot set, Bio-Inspired Computing Wiki, bic.wiki)

The Mandelbrot set is an incredible object. It's really amazing that the simple iterated equation Z = Z² + C can produce such beautiful works of mathematical art.

If you're interested in learning more about the Mandelbrot set, fractals, and chaos theory I highly recommend reading James Gleick's classic book Chaos: Making a New Science.

--David Dewey

Formal definition

Taken all number sequences S(c) -> f(z) = z*z + c starting with z = 0, where z and c are complex numbers, the Mandelbrot set is the set of all values of c for which S(c) is bounded.

Examples

In the following examples, the sequence values z = (real, imaginary) are printed in the format (real, imaginary, magnitude), where magnitude = ||z|| (see Operations with vectors).

For c = (1, 1), S(c) is unbounded (magnitude diverges towards infinity)

`S(c) = (1, 1, 1.41), (1, 3, 3.16), (-7, 7, 9.9), (1, -97, 97.01), (-9407, -193, 9408.98), (88454401, 3631103, 88528899.04), (7810996147272193, 642374081668607, 7837365965265411), (6.059901635190146e+31, 1.0035162954042004e+31, 6.142430527350064e+31), (3.5715362873038435e+63, 1.2162420078919742e+63, 3.772945278332198e+63), (1.1276626829767021e+127, 8.687704930658948e+126, 1.4235116073289227e+127), (5.168609569562561e+253, 1.9593601302033588e+254, Infinity)`

For c = (0.1, 0.1), S(c) is bounded (magnitude tends to 0.15)

`S(c) = (0.1, 0.1, 0.14), (0.1, 0.12, 0.16), (0.1, 0.12, 0.16), (0.09, 0.12, 0.16), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15), (0.09, 0.12, 0.15)`

The Wikipedia article elaborates further on the formal mathematical definition of the Mandelbrot set. It also states that if the magnitude of S(c) ever exceeds 2 the sequence can be safely considered to be unbounded.

Here is the proof of the above statement, proposed by Mike Hurley (mgh3@po.cwru.edu) in April 1993 on USENET (source: Mandelbrot Set - Escape Radius, Robert P. Munafo, 1997 Nov 19.):

The sequence generated by f(z) = z^2 + c beginning with 0 begins
 0,c,c^2 + c, ...
Call these terms z0, z1, z2, ... .
 
In fact this sequence will be unbounded if any of its terms
has magnitude larger than 2.
 
The main idea is that if the magnitude |z| is bigger than both
2 and the magnitude |c|, then |f(z)|/|z| > |z| - 1 > 1, so that
by induction the sequence of magnitudes will grow geometrically.
 
To establish the inequality, note that
  |f(z)|/|z| = |z^2 + c|/|z|
        >= (|z|^2-|c|)/|z|          [ triangle inequality ]
         = |z| - (|c|/|z|)
         > |z| - 1                  [ |z| > |c| ]
         > 1                        [ |z| > 2 ]
 
If  |c| <= 2 and  |zn| > 2 then certainly
z = zn satisfies these hypotheses, so the sequence is
unbounded. 
 
If |c| > 2 then |c^2+c| >= |c|^2 - |c| = |c|(|c|-1) > |c| > 2
so that z=z2 satisfies the hypotheses of the argument above.

Mandelbrot set coloring

The black-and-white image above plots the complex numbers that are within the Mandelbrot set. But what about the complex numbers that do not belong to the set? Do they have any structure? One quality of a given complex number C that provides structure is how early the sequence diverges (i.e. exceeds 2) for this number.

Let's select a complex number C that does not belong to the Mandelbrot set. By definition, at some point the iteration of Z = Z² + C will diverge; let's say, the first element of the sequence that exceeds 2 is its n-th element (we will call n a mandelbrot value). In order to visualize the magnitude of n we plot Con the chart using a shade of gray that corresponds to the value n - the higher the number (i.e. the later the sequence diverges), the brighter the color.

In practice the number of iterations performed ot determine whether the sequence Z = Z² + C, Z0 = 0 is bounded is always capped to some constant, let's call it MAXN. This means that any plausible plot of the Mandelbrot set is an approximation, because no matter how large MAXN, there is always chance that there will be undetected sequences that diverge after MAXN iterations. On the other hand, the existance of the MAXN value allows for a map between the mandelbrot value, which would be a number in the interval [1, NAXN], and a RGB grayscale color value in the interval [1, 255].

Source Code

Because Mandelbrot set is based on complex numbers, some complex number operation library is required. In this Mandelbrot set implementation, all complex number math is incapsulated in a global utility.complex namespace. Some of the utility.complex.* functions are never used in the program and are provided here for completeness. Note that any complex number can be also considered as a two dimensional vector, e.g. the complex number a+bi is equivalent to the vector (a, b).

Here is a pseudo-code summary of the utility.complex interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace utility.complex
{
    newComplex(real: number, imaginary: number): object;    // creates a complex number with both real and imaginary parts
    newImaginary(imaginary: number): object;    // creates a pure imaginary complex number
    newReal(real: number): object;    // creates a pure real complex number
    magnitude(c: object): number;    // calculates the magnitude `|c|` for the complex number's vector `c`
    scale(c: object, scale: number): object;    // multiplies a complex number's vector by a scalar
    translate(c: object, dre: number, dim: number): object;    // translates a complex number's vector
    sum(a: object, b: object): object;    // summates two complex numbers
    sub(a: object, b: object): object;    // subtracts two complex numbers
    mul(a: object, b: object): object;    // multiplies two complex numbers
    div(a: object, b: object): object;    // devides two complex numbers
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
var utility =
{
    complex:
    {
        //    creates a complex number with both real and imaginary parts
        newComplex: function(real, imaginary)
        {
            return {
                re: real,
                im: imaginary,
            };
        },
        //    creates a pure imaginary complex number
        newImaginary: function(imaginary)
        {
            return {
                re: 0,
                im: imaginary,
            };
        },
        //    creates a pure real complex number
        newReal: function(real)
        {
            return {
                re: real,
                im: 0,
            };
        },
        //    calculates the magnitude |c| for the complex number's vector c
        magnitude: function(c)
        {
            return Math.sqrt(c.im * c.im + c.re * c.re);
        },
        //    multiplies a complex number's vector by a scalar
        scale: function(c, scale)
        {
            return {
                re: c.re * scale,
                im: c.im * scale,
            };
        },
        //    translates a complex number's vector
        translate: function(c, dre, dim)
        {
            return {
                re: c.re + dre,
                im: c.im + dim,
            };
        },
        //    summates two complex numbers
        sum: function(a, b)
        {
            return {
                re: a.re + b.re,
                im: a.im + b.im,
            };
        },
        //    subtracts two complex numbers
        sub: function(a, b)
        {
            return {
                re: a.re - b.re,
                im: a.im - b.im,
            };
        },
        //    multiplies two complex numbers
        mul: function(a, b)
        {
            //  (a.re + a.im * i) * (b.re + b.im * i) =
            //  = a.re * (b.re + b.im * i) + a.im * i * (b.re + b.im * i) =
            //  = a.re * b.re + a.re * b.im * i + a.im * i * b.re + a.im * i * b.im * i =
            //  = a.re * b.re + a.re * b.im * i + a.im * i * b.re  =
            //  = (a.re * b.re - a.im * b.im) + (a.re * b.im + a.im * b.re) * i
            return {
                re: a.re * b.re - a.im * b.im,
                im: a.re * b.im + a.im * b.re,
            };
        },
        //    devides two complex numbers
        div: function(a, b)
        {
            //  a.re = a
            //  a.im = b
            //  b.re = c
            //  b.im = d
            //  (a.re*b.re + a.im*b.im)/(b.re*b.re + b.im*b.im) + i*(a.im*b.re - a.re*b.im)/(b.re*b.re + b.im*b.im)
            //  r.re = (a.re*b.re + a.im*b.im)/(b.re*b.re + b.im*b.im)
            //  r.im = (a.im*b.re - a.re*b.im)/(b.re*b.re + b.im*b.im)
            return {
                re: (a.re * b.re + a.im * b.im) / (b.re * b.re + b.im * b.im),
                im: (a.im * b.re - a.re * b.im) / (b.re * b.re + b.im * b.im),
            };
        },
    },
};

The goal of this program is to visualise the Mandelbrot set in an HTML canvas. For that purpose a simple graphics device is provided (class CanvasDevice), which wraps around the native canvas element and adds the following functionality:

  • buffering - all drawing operations are performed on a hidden canvas and are flushed to the screen at once, on demand;
  • image scaling and translation;
  • vector and raster (pixel) drawing; the vector drawing is limited to drawing a rectangle;
  • pixel-accurate visualization of both vector and raster graphics.

Here is a pseudo-code summary of the CanvasDevice interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CanvasDevice
{
    constructor(canvasElement: HTMLElement, width: number, height: number);
 
    setScale(value: number): void;
    getScale(): number;
    setOrigin(offsetX, offsetY): void;
    getScaledOriginX(): number;
    getScaledOriginY(): number;
 
    lockVectorGraphics(): object;
    releaseVectorGraphics(): void;
    lockRasterGraphics(): object;
    releaseRasterGraphics(): void;
 
    flush(): void;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
function CanvasDevice(canvasElement, width, height)
{
    this.width = width;
    this.height = height;
 
    this.scale = 1;
    this.offsetX = 0;
    this.offsetY = 0;
    this.scaledOffsetX = 0;
    this.scaledOffsetY = 0;
 
    this.canvasElement = canvasElement;
    this.canvasElement.style.width = this.width + "px";
    this.canvasElement.style.height = this.height + "px";
    this.canvasElement.width = this.canvasElement.offsetWidth;
    this.canvasElement.height = this.canvasElement.offsetHeight;
 
    this.canvasGraphics = this.canvasElement.getContext("2d", {alpha: false});
    this.canvasGraphics.imageSmoothingEnabled = false;
 
    this.bufferElement = document.createElement("canvas");
    this.bufferElement.style.width = this.canvasElement.style.width;
    this.bufferElement.style.height = this.canvasElement.style.height;
    this.bufferElement.width = this.canvasElement.width;
    this.bufferElement.height = this.canvasElement.height;
 
    this.bufferGraphics = this.bufferElement.getContext("2d", {alpha: false});
    this.bufferGraphics.imageSmoothingEnabled = false;
    this.bufferGraphics.translate(0.5, 0.5);    //  decrease antialiasing, grant pixel-accurate h- and v-lines
 
    this.lockType = CanvasDevice.LOCK_TYPE_NONE;
    this.pixelGraphics = null;
    this.vectorGraphics = null;
 
    this.setScale = function(value)
    {
        this.canvasGraphics.scale(1/this.scale, 1/this.scale);
        this.scale = value;
        this.scaledOffsetX = Math.round(this.offsetX / this.scale);
        this.scaledOffsetY = Math.round(this.offsetY / this.scale);
        this.canvasGraphics.scale(this.scale, this.scale);
    };
 
    this.getScale = function()
    {
        return this.scale;
    };
 
    this.setOrigin = function(offsetX, offsetY)
    {
        this.bufferGraphics.translate(-this.scaledOffsetX, -this.scaledOffsetY);
        this.offsetX = offsetX;
        this.offsetY = offsetY;
        this.scaledOffsetX = Math.round(this.offsetX / this.scale);
        this.scaledOffsetY = Math.round(this.offsetY / this.scale);
        this.bufferGraphics.translate(this.scaledOffsetX, this.scaledOffsetY);
    };
 
    this.getScaledOriginX = function()
    {
        return this.scaledOffsetX;
    };
 
    this.getScaledOriginY = function()
    {
        return this.scaledOffsetY;
    };
 
    this.lockVectorGraphics = function()
    {
        if(this.lockType != CanvasDevice.LOCK_TYPE_NONE)
        {
            throw "ASSERTION FAILED: this.lockType == CanvasDevice.LOCK_TYPE_NONE";
        }
        this.lockType = CanvasDevice.LOCK_TYPE_VECTOR;
 
        this.vectorGraphics =
        {
            graphics: this.bufferGraphics,
            device: this,
            clear: function(fillStyle)
            {
                this.graphics.beginPath();
                this.graphics.fillStyle = fillStyle;
                this.graphics.fillRect(
                    -this.device.getScaledOriginX() - 1,
                    -this.device.getScaledOriginY() - 1,
                    this.device.width + 1,
                    this.device.height + 1
                );
                this.graphics.fill();
            },
            drawRect: function(x, y, width, height, strokeStyle)
            {
                this.graphics.beginPath();
                this.graphics.strokeStyle = strokeStyle.strokeStyle;
                this.graphics.lineWidth = strokeStyle.lineWidth;
                this.graphics.rect(x, y, width, height);
                this.graphics.stroke();
            },
        };
 
        return this.vectorGraphics;
    };
 
    this.releaseVectorGraphics = function()
    {
        if(this.lockType != CanvasDevice.LOCK_TYPE_VECTOR)
        {
            throw "ASSERTION FAILED: this.lockType == CanvasDevice.LOCK_TYPE_VECTOR";
        }
        this.vectorGraphics = null;
        this.lockType = CanvasDevice.LOCK_TYPE_NONE;
    };
 
    this.lockRasterGraphics = function()
    {
        if(this.lockType != CanvasDevice.LOCK_TYPE_NONE)
        {
            throw "ASSERTION FAILED: this.lockType == CanvasDevice.LOCK_TYPE_NONE";
        }
        this.lockType = CanvasDevice.LOCK_TYPE_RASTER;
 
        this.pixelGraphics =
        {
            pixelData: this.bufferGraphics.getImageData(0, 0, this.bufferElement.width, this.bufferElement.height),
            device: this,
            setPixel: function(x, y, r, g, b, a)
            {
                if(this.device.lockType != CanvasDevice.LOCK_TYPE_RASTER)
                {
                    throw "ASSERTION FAILED: this.lockType == CanvasDevice.LOCK_TYPE_RASTER";
                }
 
                x = Math.round(x + this.device.scaledOffsetX);
                y = Math.round(y + this.device.scaledOffsetY);
 
                if(x >= this.pixelData.width || x < 0)
                {
                    return;
                }
                if(y >= this.pixelData.height || y < 0)
                {
                    return;
                }
 
                var index = (x + y * this.pixelData.width) * 4;
                this.pixelData.data[index++] = r;
                this.pixelData.data[index++] = g;
                this.pixelData.data[index++] = b;
                this.pixelData.data[index++] = a;
            },
        };
 
        return this.pixelGraphics;
    };
 
    this.releaseRasterGraphics = function()
    {
        if(this.lockType != CanvasDevice.LOCK_TYPE_RASTER)
        {
            throw "ASSERTION FAILED: this.lockType == CanvasDevice.LOCK_TYPE_RASTER";
        }
 
        this.bufferGraphics.putImageData(this.pixelGraphics.pixelData, 0, 0);
        this.pixelGraphics = null;
        this.lockType = CanvasDevice.LOCK_TYPE_NONE;
    };
 
    this.flush = function()
    {
        this.canvasGraphics.drawImage(this.bufferElement, 0, 0);
    };
}
 
CanvasDevice.LOCK_TYPE_NONE = 0;
CanvasDevice.LOCK_TYPE_VECTOR = 1;
CanvasDevice.LOCK_TYPE_RASTER = 2;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
function MandelbrotView(device, width, height, style)
{
    this.device = device;
    this.device.setOrigin(this.device.width / 2, this.device.height / 2);
 
    this.width = width;
    this.height = height;
 
    this.zoom = 1;
    this.offsetX = 0;
    this.offsetY = 0;
 
    this.sensitivity = 100;
 
    this.mandelbrotDocument = null;
 
    this.refresh = function()
    {
        var v = this.device.lockVectorGraphics();
        try
        {
            v.clear("white");
        }
        finally
        {
            this.device.releaseVectorGraphics();
        }
 
        var r = this.device.lockRasterGraphics();
        try
        {
            var start = new Date();
 
            var height = this.height;
            var width = this.width;
            var scaled_height = Math.ceil(height / this.device.getScale());
            var scaled_width = Math.ceil(width / this.device.getScale());
 
            var c_scale = this.device.getScale()/this.zoom;
            var c_dre = -this.offsetX;
            var c_dim = -this.offsetY;
 
            for(var x = -this.device.getScaledOriginX(); x < scaled_width; ++x)
            {
                for(var y = -this.device.getScaledOriginY(); y < scaled_height; ++y)
                {
                    var c = utility.complex.newComplex(x / width, y / height);
                    var c_scaled = utility.complex.scale(c, c_scale);
                    var c_scaled_translated = utility.complex.translate(c_scaled, c_dre, c_dim);
 
                    var mandelbrotValue = this.mandelbrotDocument.getMandelbrotValue(c_scaled_translated) / this.sensitivity;
 
                    if(mandelbrotValue > 1)
                    {
                        mandelbrotValue = 1;
                    }
 
                    var red = 0, green = 0, blue = 0;
                    if(mandelbrotValue != -1)
                    {
                        red = Math.ceil(255 * mandelbrotValue * 1.9);
                        if(red > 255) red = 255;
                        green = Math.ceil(255 * mandelbrotValue * 1.3);
                        if(green > 255) green = 255;
                        blue = Math.ceil(255 * mandelbrotValue / 1.4);
                        if(blue > 255) blue = 255;
                    }
                    r.setPixel(x, y, red, green, blue, 255);
                }
            }
 
            var end = new Date();
            var time = end.getTime() - start.getTime();
            console.log(912, Math.round(time/10)/100 + "s");
        }
        finally
        {
            this.device.releaseRasterGraphics();
        }
    };
 
    this.setDocument = function(value)
    {
        this.mandelbrotDocument = value;
    };
 
    this.getDocument = function(value)
    {
        return this.mandelbrotDocument;
    };
 
    this.setCoarsness = function(value)
    {
        this.device.setScale(value);
    };
 
    this.getCoarsness = function()
    {
        return this.device.getScale();
    };
 
    this.setZoom = function(value)
    {
        this.zoom = value;
    };
 
    this.getZoom = function()
    {
        return this.zoom;
    };
 
    this.setOffsetX = function(value)
    {
        this.offsetX = value;
    };
 
    this.getOffsetX = function()
    {
        return this.offsetX;
    };
 
    this.setOffsetY = function(value)
    {
        this.offsetY = value;
    };
 
    this.getOffsetY = function()
    {
        return this.offsetY;
    };
 
    this.setSensitivity = function(value)
    {
        this.sensitivity = value;
    };
 
    this.getSensitivity = function()
    {
        return this.sensitivity;
    };
}
 
function MandelbrotDocument()
{
    this.detail = 250;
 
    this.setDetail = function(value)
    {
        this.detail = value;
    };
 
    this.getDetail = function()
    {
        return this.detail;
    };
 
    //  c: complex, c.re (- [0, 1], c.im (- [0, 1]
    //  returns (- [0, 1]
    this.getMandelbrotValue = function(c)
    {
        var length = this.detail;
        var z = c;
        var i = 0;
        for(; i < length; ++i)
        {
            var z_magnitude = utility.complex.magnitude(z);
            if(z_magnitude > 2)
            {
                break;
            }
 
            var z_squared = utility.complex.mul(z, z);
            z = utility.complex.sum(z_squared, c);
        }
 
        if(i >= length)
        {
            return -1;
        }
 
        return i;
    }
 
    this.getMandelbrotSequence = function(c, iterationCount)
    {
        var result = [];
 
        var length = iterationCount;
        var z = c;
        var i = 0;
        for(; i < length; ++i)
        {
            var z_magnitude = utility.complex.magnitude(z);
 
            result.push({re: z.re, im: z.im, m:z_magnitude});
 
            var z_squared = utility.complex.mul(z, z);
            z = utility.complex.sum(z_squared, c);
        }
 
        return result;
    }
}
 
function Application()
{
    this.run = function()
    {
        var deviceCanvas = document.createElement("canvas");
        document.body.appendChild(deviceCanvas);
 
        this.device = new CanvasDevice(deviceCanvas, 800, 800);
 
        this.mandelbrotDocument = new MandelbrotDocument();
 
        this.mandelbrotView = new MandelbrotView(this.device, this.device.width, this.device.height);
        this.mandelbrotView.setDocument(this.mandelbrotDocument);
 
        this.mandelbrotView.setCoarsness(2);
 
        //  provides interesting details up to zoom of 1024000000
        this.mandelbrotView.setOffsetX( 1.7480271785);
        this.mandelbrotView.setOffsetY(-0.0000087174);
 
        //  BEGIN: uncomment only one of the comment blocks
 
        //this.mandelbrotView.setZoom(1024000000);
        //this.mandelbrotView.setSensitivity(400);
        //this.mandelbrotDocument.setDetail(1500);
 
        //this.mandelbrotView.setZoom(102400000);
        //this.mandelbrotView.setSensitivity(300);
        //this.mandelbrotDocument.setDetail(400);
 
        //this.mandelbrotView.setZoom(10240000);
        //this.mandelbrotView.setSensitivity(300);
        //this.mandelbrotDocument.setDetail(400);
 
        //this.mandelbrotView.setZoom(1024000);
        //this.mandelbrotView.setSensitivity(200);
        //this.mandelbrotDocument.setDetail(300);
 
        //this.mandelbrotView.setZoom(102400);
        //this.mandelbrotView.setSensitivity(150);
        //this.mandelbrotDocument.setDetail(300);
 
        //this.mandelbrotView.setZoom(10240);
        //this.mandelbrotView.setSensitivity(140);
        //this.mandelbrotDocument.setDetail(250);
 
        //this.mandelbrotView.setZoom(1024);
        //this.mandelbrotView.setSensitivity(140);
        //this.mandelbrotDocument.setDetail(250);
 
        //this.mandelbrotView.setZoom(102);
        //this.mandelbrotView.setSensitivity(140);
        //this.mandelbrotDocument.setDetail(250);
 
        //this.mandelbrotView.setZoom(10);
        //this.mandelbrotView.setSensitivity(140);
        //this.mandelbrotDocument.setDetail(250);
 
        //this.mandelbrotView.setZoom(5);
        //this.mandelbrotView.setSensitivity(140);
        //this.mandelbrotDocument.setDetail(250);
 
        //this.mandelbrotView.setZoom(1);
        //this.mandelbrotView.setSensitivity(140);
        //this.mandelbrotDocument.setDetail(250);
 
        this.mandelbrotView.setZoom(0.35);
        this.mandelbrotView.setOffsetX(0.8);
        this.mandelbrotView.setOffsetY(0);
        this.mandelbrotView.setSensitivity(120);
        this.mandelbrotDocument.setDetail(250);
 
        //  END: uncomment only one of the comment blocks
 
        this.mandelbrotView.refresh();
        this.device.flush();
    };
 
    this.runSequenceGenerator = function()
    {
        function _printSequence(c, sequence)
        {
            var sb = [];
 
            sb.push("c = ");
            sb.push("(");
            sb.push(c.re);
            sb.push(", ");
            sb.push(c.im);
            sb.push("), ");
 
            sb.push("S(c) = ");
            for(var length = sequence.length, i = 0; i < length; ++i)
            {
                var z = sequence[i];
                if(isNaN(z.m)) break;
                if(i != 0) sb.push(", ");
                sb.push("(");
                sb.push(Math.round(z.re * 100) / 100);
                sb.push(", ");
                sb.push(Math.round(z.im * 100) / 100);
                sb.push(", ");
                sb.push(Math.round(z.m * 100) / 100);
                sb.push(")");
            }
 
            return sb.join("")
        }
 
        this.mandelbrotDocument = new MandelbrotDocument();
 
        var c1 = utility.complex.newComplex(1, 1);
        var sequence1 = this.mandelbrotDocument.getMandelbrotSequence(c1, 20)
        var text1 = _printSequence(c1, sequence1);
        console.log(9111, text1);
 
        var c2 = utility.complex.newComplex(0.1, 0.1);
        var sequence2 = this.mandelbrotDocument.getMandelbrotSequence(c2, 20)
        var text2 = _printSequence(c2, sequence2);
        console.log(9112, text2);
    };
}
 
var application = new Application();
application.runSequenceGenerator();
application.run();

Code Guide

Code points of interest

CanvasDevice

CanvasDevice is a basic, general purpose buffered drawing device that uses HTML canvas. It is capable of drawing both raster and vector graphics, limited to what's needed for this example specifically.

  • this.bufferGraphics.translate(0.5, 0.5); - this line of code decreases the antialiasing and makes horisontal and vertical single-pixel lines as well as single pixels more crisp.
  • Device scaling is applied on the canvasGraphics. In order to make this.bufferGraphics.translate(0.5, 0.5); work, we need to keep bufferGraphics's scale to 1.
  • Device translation is performed by the bufferGraphics. (reasoning required, or refactor to perform translation on canvasGraphics)
  • The HTML canvas stacks .scale and .translate calls, so when setting the scale or offset to an absolute value, the canvas must be reverted to it's original scale or translation first: this.canvasGraphics.scale(1/this.scale, 1/this.scale); or this.bufferGraphics.translate(-this.scaledOffsetX, -this.scaledOffsetY);

MandelbrotDocument

MandelbrotDocument represents the abstraction of the Mandelbrot set and is in charge of determining, which values of c belong to the set and which do not. For the values of c, which are not within the set, it provides information on how quickly S(c) diverges (mandelbrotValue as we call it in the code). The implementation is pretty straightforward.

MandelbrotDocument properties
  • detail (getDetail, setDetail) - the number of iterations to perform before assuming that S(c) is bounded

MandelbrotView

MandelbrotView is the component that knows how to draw a MandelbrotDocument to a CanvasDevice. The complex plane, containing all possible values of c, is mapped to device coordinates: (re -> x, im -> y) - and positioned within the view port through offset and scaling. The glow level corresponds to the mandelbrotValue, returned by the document. The coloring we use in this implementation is a result of several experiments and its purpose is to reveal detail, rather than to create beauty.

  • The MandelbrotView logs to the console the time it took to perform the rendering.
  • Setting coarseness
1
2
3
4
this.setCoarsness = function(value)
{
    this.device.setScale(value);
};

preseves the perceived zoom of the view by scaling the device and negating that scale computationally during image refresh:

1
2
3
4
var scaled_height = Math.ceil(height / this.device.getScale());
var scaled_width = Math.ceil(width / this.device.getScale());
 
var c_scale = this.device.getScale()/this.zoom;
MandelbrotView properties
  • offsetX (getOffsetX, setOffsetX) - x-offset of the image from the view center
  • offsetY (getOffsetY, setOffsetY) - y-offset of the image from the view center
  • zoom (getZoom, setZoom) - level of zooming (0.35 fits the while figure inside the viewing area)
  • sensitivity (getSensitivity, setSensitivity) - the mandelbrotValue, returned by the document, increases with increasing the zoom; use the sensitivity value to compensate and to reduce the glow in the image; higher values yeld lower glow
  • coarsness (getCoarsness, setCoarsness) - the size of the pixel (higher values produce more coarse images and yeld better performance speed);

Application

Application's method run setups a device, a document and a view, and lets the view draw on the screen. A bunch of commented alternative document/view configurations can be uncommented and used to explore the Mandelbot set fractal with different zoom levels.

Screenshots

These images are generated with the JavaScript code above. To produce a specific image, uncomment the corresponding mandelbrotDocument configuration block at the end of the code. Note that only one mandelbrotDocument configuration block at a time can stay uncommented.

bic.wiki / Mandelbrot Set with JavaScript source code, zoom 0.35, offset (0.8, 0)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 1, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 5, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 10, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 102, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 1024, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 10240, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 102400, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 1024000, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 10240000, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 102400000, offset (1.7480271785, -0.0000087174)
bic.wiki / Mandelbrot Set with JavaScript source code, zoom 1024000000, offset (1.7480271785, -0.0000087174)

JsFiddle

Resources

YouTube

<iframe width="560" height="315" src="https://www.youtube.com/embed/NGMRB4O922I" frameborder="0" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/AyLvyrU9SMU" frameborder="0" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/zXTpASSd9xE" frameborder="0" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/9gk_8mQuerg" frameborder="0" allowfullscreen></iframe>

Publication

Dates: Jul 4. 2017 (last content edit), Feb 11. 2017 (first publication)
Credits: Daniel Apostolov, David Dewey, Mike Hurley