8.4.3 Animated Fractals#

Duration:

25-30 minutes

Level:

Intermediate

Prerequisites:

Module 4.1 (Classical Fractals), Module 8.1 (Animation Fundamentals)

Overview#

Fractals are infinitely complex patterns that repeat at every scale. When you add the dimension of time to fractal visualization, you unlock one of the most mesmerizing experiences in generative art: the fractal zoom. In this exercise, you will create smooth animations that appear to dive infinitely into the Mandelbrot set, revealing layer after layer of intricate detail.

This exercise bridges your knowledge of static fractal generation (Module 4) with animation principles (Module 8), demonstrating how time can serve as a parameter in mathematical visualization. The techniques you learn here apply broadly to any mathematical visualization where exploring parameter space over time creates compelling visual narratives.

Learning Objectives#

By the end of this exercise, you will be able to:

  • Parameterize fractal generation over time to create smooth animations

  • Implement exponential zoom interpolation for visually pleasing zoom effects

  • Convert Mandelbrot iteration counts to color gradients

  • Generate animated GIFs using frame-based rendering and imageio [ImageIODocs]

Quick Start: See It In Action#

Run this code to create your first animated fractal zoom:

Create a Mandelbrot zoom animation#
 1import numpy as np
 2import imageio.v2 as imageio
 3
 4def mandelbrot_frame(width, height, x_min, x_max, y_min, y_max, max_iter):
 5    x = np.linspace(x_min, x_max, width)
 6    y = np.linspace(y_min, y_max, height)
 7    X, Y = np.meshgrid(x, y)
 8    C = X + 1j * Y
 9    Z = np.zeros_like(C)
10    iterations = np.zeros(C.shape)
11    for i in range(max_iter):
12        mask = np.abs(Z) <= 2
13        Z[mask] = Z[mask]**2 + C[mask]
14        iterations[mask] = i
15    return (iterations / max_iter * 255).astype(np.uint8)
16
17frames = []
18for frame in range(60):
19    zoom = 500 ** (frame / 59)
20    width = 3.0 / zoom
21    cx, cy = -0.743644, 0.131826
22    img = mandelbrot_frame(400, 400, cx-width/2, cx+width/2, cy-width/2, cy+width/2, 150)
23    frames.append(np.stack([img, img//2, 255-img], axis=-1))
24imageio.mimsave('fractal_zoom.gif', frames, fps=30, loop=0)
Animated GIF showing a smooth zoom into the Mandelbrot set, revealing increasingly detailed spiral patterns

A 60-frame animation zooming 500x into the “Seahorse Valley” region of the Mandelbrot set. Notice how each frame reveals new layers of self-similar structure.#

You just created an infinite zoom animation. The magic lies in exponential interpolation: each frame shows a view that is a fixed percentage smaller than the previous one, creating the illusion of constant-speed zooming despite the view window shrinking by a factor of 500.

Core Concepts#

Concept 1: Time as a Fractal Parameter#

Static fractals are beautiful, but they show only a single view of an infinite mathematical object. By making the view window a function of time, we can explore the fractal’s structure dynamically [Peitgen1986].

The key insight is that zoom level should change exponentially, not linearly. If we zoom linearly (adding the same amount each frame), the early frames would feel extremely slow while the later frames would rush past. Exponential zoom creates the perceptually constant speed that makes fractal animations hypnotic.

The Exponential Zoom Formula

Given a starting view width W_0 and a total zoom factor Z over N frames, the view width at frame f is:

\[W_f = W_0 \cdot Z^{-f/N}\]

For example, with W_0 = 3.0, Z = 500, and N = 60:

  • Frame 0: Width = 3.0 (full view)

  • Frame 30: Width = 3.0 / sqrt(500) = 0.134 (22x zoom)

  • Frame 60: Width = 3.0 / 500 = 0.006 (500x zoom)

Exponential zoom calculation#
def get_view_window(frame, total_frames, center_x, center_y, initial_width, zoom_factor):
    # Progress from 0 to 1
    progress = frame / (total_frames - 1)

    # Exponential interpolation: width shrinks exponentially
    current_width = initial_width * (zoom_factor ** (-progress))

    # Calculate bounds centered on zoom target
    x_min = center_x - current_width / 2
    x_max = center_x + current_width / 2
    y_min = center_y - current_width / 2
    y_max = center_y + current_width / 2

    return x_min, x_max, y_min, y_max
Diagram showing nested rectangles representing view windows at different zoom levels, demonstrating how the view area shrinks exponentially toward the zoom center

The zoom window progression during animation. Each nested rectangle represents the view at a different frame. The exponential spacing ensures visually smooth zooming.#

Concept 2: Fractal Zoom Mechanics#

The Mandelbrot set is defined by iterating the formula z = z^2 + c starting from z = 0, where c is a complex number corresponding to each pixel. Points where the iteration does not escape (|z| <= 2 after many iterations) are inside the set [Mandelbrot1982].

Choosing a Zoom Target

The most visually interesting regions of the Mandelbrot set lie on its boundary, where the set transitions from inside (black) to outside (colored). Famous zoom targets include:

  • Seahorse Valley (-0.743644, 0.131826): Intricate spiral patterns

  • Elephant Valley (0.275, 0.0): Trunk-like structures

  • Mini Mandelbrot (-1.768, 0.0): A tiny copy of the entire set

The coordinates above have been discovered by fractal explorers over decades and represent particularly rich regions of the boundary [Douady1984].

Resolution vs. Iteration Depth Tradeoff

As you zoom deeper, you need more iterations to see fine detail. The relationship is roughly logarithmic: doubling the zoom depth requires only a modest increase in iterations. However, this creates a computational tradeoff:

  • More iterations = finer boundary detail, slower rendering

  • Fewer iterations = coarser boundaries, faster rendering

For a 500x zoom, 150-200 iterations typically provides good detail. For deeper zooms (10000x+), you may need 500+ iterations [Devaney1992].

Three side-by-side images showing the Mandelbrot set at different zoom levels: 1x showing the full set, 22x showing detail structure, and 500x showing deep spiral patterns

Frame comparison showing zoom progression. Left: Starting view (1x). Center: Midway (22x). Right: Final zoom (500x). Notice how new patterns emerge at each scale.#

Color Mapping

The iteration count for each pixel tells us how quickly that point escaped. Converting this to color creates the characteristic Mandelbrot visualization:

Simple color mapping from iterations#
def iterations_to_colors(iterations, max_iter):
    # Normalize to 0-1
    normalized = iterations / max_iter

    # Create RGB array
    colors = np.zeros((*iterations.shape, 3), dtype=np.uint8)

    # Points inside set (reached max_iter) are black
    inside = iterations >= (max_iter - 1)

    # Color gradient for escaped points
    colors[:, :, 0] = np.where(inside, 0, normalized * 200)  # Red
    colors[:, :, 1] = np.where(inside, 0, normalized * 100)  # Green
    colors[:, :, 2] = np.where(inside, 0, 255 - normalized * 200)  # Blue

    return colors

Did You Know?

The Mandelbrot set has a deep mathematical connection to Julia sets. Each point c in the complex plane corresponds to a unique Julia set generated by the same iteration z = z^2 + c. Points inside the Mandelbrot set produce connected Julia sets, while points outside produce disconnected “dust” Julia sets. This relationship, discovered by Adrien Douady and John Hubbard, explains why the boundary of the Mandelbrot set is where all the visual complexity lives [Douady1984].

Hands-On Exercises#

Now apply what you have learned with three progressively challenging exercises.

Exercise 1: Execute and Explore#

Run the animated_fractal.py script and observe the output. Then answer these reflection questions:

Reflection Questions:

  1. Why does the zoom appear to continue at a constant visual speed despite the view shrinking by 500x?

  2. What happens to the level of detail as we zoom deeper? Why?

  3. The “Seahorse Valley” contains spiral patterns. What mathematical property of the Mandelbrot set creates these spirals?

  4. Why are points inside the Mandelbrot set colored black while the boundary has rich colors?

Answers and Explanation

1. Constant visual speed

The zoom uses exponential interpolation (zoom_factor ** progress), not linear. Each frame shrinks the view by a constant percentage (about 11% per frame for 500x over 60 frames). This matches human perception, which operates on ratios rather than absolute differences.

2. Detail vs. depth

As we zoom deeper, we need more iterations to resolve fine boundary details. The script uses 200 iterations, which provides good detail up to about 500x zoom. Beyond that, boundaries start to look pixelated or noisy because points that would escape with more iterations appear black.

3. Spiral patterns

The spirals emerge from the iteration dynamics near specific points called “Misiurewicz points.” These are points where the iteration eventually becomes periodic. The boundary near these points forms logarithmic spirals, a direct consequence of the complex multiplication in z^2 which rotates and scales the complex plane.

4. Black interior vs. colored boundary

Points inside the set never escape (|z| stays bounded forever), so they reach the maximum iteration count and are colored black. Points outside escape at different rates. The color represents how quickly they escaped, creating the gradient. The boundary is where escape times transition from finite to infinite.

Exercise 2: Modify Parameters#

Experiment with different parameters to create varied animations.

Goal 1: Change the zoom target to explore different regions

Try these alternative coordinates:

Alternative zoom targets#
# Elephant Valley - trunk-like patterns
CENTER_X = 0.275
CENTER_Y = 0.0

# Mini Mandelbrot - a tiny copy of the whole set
CENTER_X = -1.768
CENTER_Y = 0.0

# Spiral galaxy region
CENTER_X = -0.761574
CENTER_Y = -0.0847596
Hint: Finding interesting coordinates

The most interesting regions are always on the boundary of the set. You can find coordinates by:

  1. Starting with a full view and noting coordinates of interesting areas

  2. Searching online for “Mandelbrot zoom coordinates”

  3. Looking for “filaments” (thin black lines extending from the main set)

Goal 2: Adjust animation speed and duration

Speed variations#
# Slower, longer zoom (smoother)
NUM_FRAMES = 120
ZOOM_FACTOR = 1000

# Quick preview
NUM_FRAMES = 30
ZOOM_FACTOR = 100

Goal 3: Modify the color palette

Color palette variations#
# Warm colors (fire theme)
colors[:, :, 0] = np.where(inside, 0, 255 - normalized * 100)  # Red stays high
colors[:, :, 1] = np.where(inside, 0, normalized * 200)  # Green increases
colors[:, :, 2] = np.where(inside, 0, normalized * 50)   # Blue low

# Cool colors (ocean theme)
colors[:, :, 0] = np.where(inside, 0, normalized * 50)
colors[:, :, 1] = np.where(inside, 0, normalized * 200)
colors[:, :, 2] = np.where(inside, 0, 255 - normalized * 50)

Goal 4: Increase iteration depth for finer detail

Iteration variations#
MAX_ITERATIONS = 100   # Fast but coarse boundaries
MAX_ITERATIONS = 300   # Detailed but slower
MAX_ITERATIONS = 500   # Very detailed for deep zooms
Solutions

Goal 1: Different coordinates reveal dramatically different structures. The Mini Mandelbrot location shows a perfect miniature copy of the entire set, demonstrating the ultimate self-similarity of fractals.

Goal 2: More frames with higher zoom creates smoother, longer animations. For presentation quality, use 90-120 frames at 30 fps.

Goal 3: The warm palette creates a “molten” look, while cool colors give an underwater feel. Experiment with different channel formulas for unique effects.

Goal 4: Higher iterations are essential for deep zooms. As a rule of thumb, for zoom factor Z, use at least log2(Z) * 50 iterations.

Exercise 3: Re-code from Scratch#

Build your own fractal animation using the animated_fractal_starter.py template.

Part A: Complete the Implementation

The starter code has TODO comments guiding you through implementing:

  1. Coordinate grid creation using np.linspace and np.meshgrid

  2. Mandelbrot iteration with the formula z = z^2 + c

  3. Color mapping from iteration counts to RGB values

  4. Zoom window calculation using exponential interpolation

Key implementation steps#
# Step 1: Create coordinate arrays
real_values = np.linspace(x_min, x_max, width)
imag_values = np.linspace(y_min, y_max, height)

# Step 2: Create 2D grids
real_grid, imag_grid = np.meshgrid(real_values, imag_values)

# Step 3: Complex number array
c_values = real_grid + 1j * imag_grid

# Step 5: Mandelbrot iteration
z_values[still_iterating] = z_values[still_iterating] ** 2 + c_values[still_iterating]
Complete Solution
 1def compute_mandelbrot(width, height, x_min, x_max, y_min, y_max, max_iter):
 2    real_values = np.linspace(x_min, x_max, width)
 3    imag_values = np.linspace(y_min, y_max, height)
 4    real_grid, imag_grid = np.meshgrid(real_values, imag_values)
 5    c_values = real_grid + 1j * imag_grid
 6
 7    z_values = np.zeros_like(c_values, dtype=complex)
 8    iteration_counts = np.zeros(c_values.shape, dtype=float)
 9
10    for iteration in range(max_iter):
11        still_iterating = np.abs(z_values) <= 2
12        z_values[still_iterating] = z_values[still_iterating] ** 2 + c_values[still_iterating]
13        iteration_counts[still_iterating] = iteration
14
15    return iteration_counts
16
17def iterations_to_colors(iteration_counts, max_iter):
18    normalized = iteration_counts / max_iter
19    height, width = iteration_counts.shape
20    colors = np.zeros((height, width, 3), dtype=np.uint8)
21    inside_set = iteration_counts >= (max_iter - 1)
22
23    colors[:, :, 0] = np.where(inside_set, 0, (normalized * 200).astype(np.uint8))
24    colors[:, :, 1] = np.where(inside_set, 0, (normalized * 100).astype(np.uint8))
25    colors[:, :, 2] = np.where(inside_set, 0, (255 - normalized * 200).astype(np.uint8))
26
27    return colors
28
29def calculate_zoom_window(frame_index, total_frames, center_x, center_y,
30                          initial_width, zoom_factor):
31    progress = frame_index / (total_frames - 1) if total_frames > 1 else 0
32    current_width = initial_width * (zoom_factor ** (-progress))
33    current_height = current_width
34
35    x_min = center_x - current_width / 2
36    x_max = center_x + current_width / 2
37    y_min = center_y - current_height / 2
38    y_max = center_y + current_height / 2
39
40    return x_min, x_max, y_min, y_max

Part B: Challenge Extension

Create a Julia set morphing animation where the parameter c changes over time:

Julia set animation concept#
# Julia set uses fixed c, varying starting z
def julia_frame(width, height, c, max_iter):
    x = np.linspace(-2, 2, width)
    y = np.linspace(-2, 2, height)
    X, Y = np.meshgrid(x, y)
    Z = X + 1j * Y  # Starting z values (not c!)

    iterations = np.zeros(Z.shape)
    for i in range(max_iter):
        mask = np.abs(Z) <= 2
        Z[mask] = Z[mask]**2 + c  # c is constant, z varies
        iterations[mask] = i

    return iterations

# Animate by changing c along the Mandelbrot boundary
for frame in range(60):
    angle = frame * 2 * np.pi / 60
    c = complex(-0.7 + 0.1 * np.cos(angle), 0.27 + 0.1 * np.sin(angle))
    # Generate frame with this c value...
Challenge Solution
 1import numpy as np
 2import imageio.v2 as imageio
 3
 4def julia_frame(width, height, c, max_iter):
 5    x = np.linspace(-1.5, 1.5, width)
 6    y = np.linspace(-1.5, 1.5, height)
 7    X, Y = np.meshgrid(x, y)
 8    Z = X + 1j * Y
 9
10    iterations = np.zeros(Z.shape, dtype=float)
11    for i in range(max_iter):
12        mask = np.abs(Z) <= 2
13        Z[mask] = Z[mask]**2 + c
14        iterations[mask] = i
15
16    normalized = iterations / max_iter
17    colors = np.zeros((*iterations.shape, 3), dtype=np.uint8)
18    inside = iterations >= (max_iter - 1)
19    colors[:, :, 0] = np.where(inside, 0, (normalized * 255).astype(np.uint8))
20    colors[:, :, 1] = np.where(inside, 0, (normalized * 128).astype(np.uint8))
21    colors[:, :, 2] = np.where(inside, 0, (255 - normalized * 200).astype(np.uint8))
22    return colors
23
24# Create morphing animation
25frames = []
26for frame in range(60):
27    angle = frame * 2 * np.pi / 60
28    c = complex(-0.7 + 0.15 * np.cos(angle), 0.27 + 0.15 * np.sin(angle))
29    img = julia_frame(400, 400, c, 150)
30    frames.append(img)
31
32imageio.mimsave('julia_morph.gif', frames, fps=24, loop=0)
33print("Saved: julia_morph.gif")

Summary#

Key Takeaways#

  • Time parameterizes fractals: By making view coordinates functions of time, static fractals become dynamic explorations [Shiffman2012]

  • Exponential zoom creates perceptually constant speed, essential for smooth animations

  • The Mandelbrot iteration z = z^2 + c determines whether points escape, with escape speed creating the color gradient

  • Zoom targets matter: the most interesting animations explore the set’s boundary where complexity lives

  • Iteration depth must increase with zoom level to maintain detail

  • Julia sets offer an alternative animation approach where the parameter c changes instead of the view

Common Pitfalls#

  • Linear zoom: Creates jarring speed changes. Always use exponential interpolation.

  • Insufficient iterations: Deep zooms look “muddy” or lose detail. Increase MAX_ITERATIONS for deeper zooms.

  • Choosing interior points: Zooming into the black interior is boring. Target the boundary.

  • Large GIF files: High resolution + many frames = huge files. Balance quality with file size.

  • Forgetting aspect ratio: Non-square windows distort the fractal if not handled correctly.

Connection to Future Learning#

This exercise establishes foundations for more advanced generative topics:

  • Module 9.4 Feature Visualization: Neural network feature maps can be animated similar to fractal parameter exploration

  • Module 12.2 VAE Interpolation: Latent space navigation uses similar interpolation concepts

  • Module 12.3 Diffusion Models: The denoising process can be visualized as temporal evolution

Next Steps#

Continue your exploration of generative art and animation:

  • ../../8.2_organic_motion/8.2.1_flower_assembly/flower_movie/README to create organic motion patterns

  • ../../../Module_04_fractals_recursion/4.1_classical_fractals/4.1.1_fractal_square/fractal_square/README to review static fractal generation

References#

[Mandelbrot1982]

Mandelbrot, B. B. (1982). The Fractal Geometry of Nature. W. H. Freeman and Company. ISBN: 978-0-7167-1186-5

[Peitgen1986]

Peitgen, H.-O., & Richter, P. H. (1986). The Beauty of Fractals: Images of Complex Dynamical Systems. Springer-Verlag. ISBN: 978-3-540-15851-8

[Douady1984] (1,2)

Douady, A., & Hubbard, J. H. (1984). Exploring the Mandelbrot set: The Orsay Notes. Publications Mathematiques d’Orsay, 84-02.

[Devaney1992]

Devaney, R. L. (1992). A First Course in Chaotic Dynamical Systems: Theory and Experiment. Westview Press. ISBN: 978-0-201-55406-9

[Barnsley1988]

Barnsley, M. F. (1988). Fractals Everywhere. Academic Press. ISBN: 978-0-12-079062-3

[Shiffman2012]

Shiffman, D. (2012). The Nature of Code, Chapter 8: Fractals. https://natureofcode.com/book/chapter-8-fractals/

[Pearson2011]

Pearson, M. (2011). Generative Art: A Practical Guide Using Processing. Manning Publications. ISBN: 978-1-935182-62-5

[NumPyDocs]

NumPy Developers. (2024). NumPy Reference: Array Creation and Manipulation. https://numpy.org/doc/stable/reference/

[ImageIODocs]

imageio Contributors. (2024). imageio Documentation. https://imageio.readthedocs.io/

[Sweller1988]

Sweller, J. (1988). Cognitive load during problem solving: Effects on learning. Cognitive Science, 12(2), 257-285. https://doi.org/10.1207/s15516709cog1202_4