Applied Math for Creative Coders
  1. Math Models for Creative Coders
  2. Geometry
  3. Affine Transformation Fractals
  • Math Models for Creative Coders
    • Tech
      • Tools and Installation
      • Adding Libraries to p5.js
      • Using Constructor Objects in p5.js
      • The Open Sound Protocol
    • Maths
      • Vectors
      • Matrix Algebra Whirlwind Tour
      • Things at Right Angles
    • Systems
      • Frequency and Time Domains
      • Fourier Series and Fourier Transform
      • Filters
      • Convolution
    • Geometry
      • Circles
      • Complex Numbers
      • Fractals
      • Affine Transformation Fractals
      • L-Systems
      • Kolams and Lusona
    • Media
      • Additive Sound Synthesis
      • content/courses/MathModelsDesign/Modules/35-Media/65-GrainSynth/index.qmd
      • Making Noise Predictably
      • The Karplus-Strong Guitar Algorithm
    • AI
      • Working with Neural Nets
      • The Perceptron
      • The Multilayer Perceptron
      • MLPs and Backpropagation
      • Gradient Descent
    • Projects
      • Projects

On this page

  • Inspiration
  • Introduction
  • What is an Affine Transformation?
  • Some Examples of Affine Transformations
  • Designing with Affine Transformations
  • Wait, But Why?
  • References
  1. Math Models for Creative Coders
  2. Geometry
  3. Affine Transformation Fractals

Affine Transformation Fractals

Created in Translation

Iterated Functions
Fractals
Affine Transformations
Barnsley
Published

January 28, 2025

Modified

August 5, 2025

Plot Fonts and Theme

Show the Code
library(systemfonts)
library(showtext)
## Clean the slate
systemfonts::clear_local_fonts()
systemfonts::clear_registry()
##
showtext_opts(dpi = 96) # set DPI for showtext
sysfonts::font_add(
  family = "Alegreya",
  regular = "../../../../../../fonts/Alegreya-Regular.ttf",
  bold = "../../../../../../fonts/Alegreya-Bold.ttf",
  italic = "../../../../../../fonts/Alegreya-Italic.ttf",
  bolditalic = "../../../../../../fonts/Alegreya-BoldItalic.ttf"
)

sysfonts::font_add(
  family = "Roboto Condensed",
  regular = "../../../../../../fonts/RobotoCondensed-Regular.ttf",
  bold = "../../../../../../fonts/RobotoCondensed-Bold.ttf",
  italic = "../../../../../../fonts/RobotoCondensed-Italic.ttf",
  bolditalic = "../../../../../../fonts/RobotoCondensed-BoldItalic.ttf"
)
showtext_auto(enable = TRUE) # enable showtext
##
theme_custom <- function() {
  font <- "Alegreya" # assign font family up front
  "%+replace%" <- ggplot2::"%+replace%" # nolint

  theme_classic(base_size = 14, base_family = font) %+replace% # replace elements we want to change

    theme(
      text = element_text(family = font), # set base font family

      # text elements
      plot.title = element_text( # title
        family = font, # set font family
        size = 24, # set font size
        face = "bold", # bold typeface
        hjust = 0, # left align
        margin = margin(t = 5, r = 0, b = 5, l = 0)
      ), # margin
      plot.title.position = "plot",
      plot.subtitle = element_text( # subtitle
        family = font, # font family
        size = 14, # font size
        hjust = 0, # left align
        margin = margin(t = 5, r = 0, b = 10, l = 0)
      ), # margin

      plot.caption = element_text( # caption
        family = font, # font family
        size = 9, # font size
        hjust = 1
      ), # right align

      plot.caption.position = "plot", # right align

      axis.title = element_text( # axis titles
        family = "Roboto Condensed", # font family
        size = 12
      ), # font size

      axis.text = element_text( # axis text
        family = "Roboto Condensed", # font family
        size = 9
      ), # font size

      axis.text.x = element_text( # margin for axis text
        margin = margin(5, b = 10)
      )

      # since the legend often requires manual tweaking
      # based on plot content, don't define it here
    )
}

## Use available fonts in ggplot text geoms too!
ggplot2::update_geom_defaults(geom = "text", new = list(
  family = "Roboto Condensed",
  face = "plain",
  size = 3.5,
  color = "#2b2b2b"
))

## Set the theme
ggplot2::theme_set(new = theme_custom())
Error in theme_classic(base_size = 14, base_family = font): could not find function "theme_classic"

Inspiration

This is a mathematically created fern! It uses, (gasp!) repeated matrix multiplication and addition!

We’ll see.

Introduction

The self-similarity of fractals suggests that we could create new fractals from a basic shape using the following procedure:

  1. Start with a basic shape, e.g. a rectangle
  2. Define a set of transformations: scaling / mirroring / translation / combination (say n scaled+rotated replicates)
  3. Run these transformations on the basic shape
  4. Feed the output back to the input ( Classic IFR )
  5. Wait for the pattern to emerge.

See the figure below to get an idea of this process.

Figure 1: Emerging Fractal

Well, this works, provided the transformations include significant amounts of scaling (i.e. reduction in size). You can imagine that if the basic shape does not shrink fast enough, the pattern converges very slowly and would remain chunky even after a large number of iterations.

Secondly, the number of operations quickly becomes exponentially high, as each stage creates n-fold computation increase. Indeed, if we run \(d\) iterations, then the computations scale as \(n^d\), which can very quickly become out of hand!!

So what to do? Just like with the DeepSeek-R1 algorithm that simplified a great deal of AI algorithms, we have recourse to what is called the Barnsley Algorithm. NOTE: especially note the terrific pictures on this stackexchange page!

First let us understand what are Affine Transformations and then build our fractals.

What is an Affine Transformation?

Affine Transformations are defined as a transformations of a space that are:

  • linear (no nonlinear functions of an x-coordinate, say \(e^x\))
  • reversible.

Affine transformations can be represented by matrices which multiply the coordinates of a shape in space. Multiple transformations can be understood a series of matrix multiplications, and can indeed be collapsed into a SINGLE matrix multiplication of the coordinates of the shape.

See this webpage at Mathigon to get an idea of rigid transformations of shape.

Some Examples of Affine Transformations

Here are some short videos of affine transformations:

Figure 2: Scaling Along X
Figure 3: Scaling Along Y
Figure 4: Shearing Along X
Figure 5: Shearing Along Y
Figure 6: Translation Along X
Figure 7: Translation Along Y

Designing with Affine Transformations

So how do we use these Affine Transformations? Let us paraphrase what Gary William Flake says in his book The Computational Beauty of Nature:

If \(p\) is a point in space, and its affine transformation(s) is \(L(p\)), then:

  • If \(p\) is on the final fractal, then so is \(L(p)\);
  • If \(p\) is not part of the final fractal, then \(L(p)\) will be atleast closer to the final fractal than \(p\) itself.

These ideas give us our final algorithm for designing a fractal with affine transformations.

  • Start with any point \(p\)
  • Pick a (set of) Affine transformations \(L_i(p)\) that allow us to imagine the final shape
  • Take the affine transformation \(L_i(p)\) of point \(p\). Choose \(i\) at random!
  • Use an IFR: pipe the result back into the input
  • Make a large number of iterations
  • Plot all intermediate points that come out of the IFR

With this approach, the points rapidly land up on the fractal which builds up over multiple iterations. We can start anywhere in space and it will still converge.

The additional feature of the Barnsley algorithm is the randomness: since most fractals use not one but several affine transformations to create a multiplicity of forms, at each iteration we can randomly choose between them!

The block diagram of the Barnsley Algorithm looks like this:

  • Using p5.js
  • Using R

How to understand this sketch? Here is Dan Shiffman again!

In the code below, the Affine transformations \(Af_i\) are of the form

\[ AF_i = A_i * X + B_i, ~ i = 1...4 \tag{1}\]

with four options each for matrix \(A\) and matrix \(B\), and \(X = (x,y)\), the current point coordinates (seed input, then output feedback for recursion). There are 50000 iterations performed and at each interation, a random A and a random B are picked to provide the Affine Transformation for that iteration.

The starting “seed point” is simply \(X = (0,0)\).

The probabilities with which each affine transformation is chosen are not all equal; these can be tweaked to see the effect on the fractal.

The four options for the \(A_i\) matrices are:

Show the Code
A <- vector(mode = "list", length = 4)
# Four Affine translation Matrices
A[[1]] <- matrix(c(0, 0, 0, 0.18), nrow = 2)
A[[2]] <- matrix(c(0.85, -0.04, 0.04, 0.85), nrow = 2)
A[[3]] <- matrix(c(0.2, 0.23, -0.26, 0.22), nrow = 2)
A[[4]] <- matrix(c(-0.15, 0.36, 0.28, 0.24), nrow = 2)
as_sym(A[[1]])
as_sym(A[[2]])
as_sym(A[[3]])
as_sym(A[[4]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
$$ \[\begin{bmatrix} 0.00 & 0.00 \\ 0.00 & 0.18 \\ \end{bmatrix}\]

$$ {#eq-A1}

$$ \[\begin{bmatrix} 0.85 & 0.04 \\ -0.04 & 0.85 \\ \end{bmatrix}\]

$$

$$ \[\begin{bmatrix} 0.20 & -0.26 \\ 0.23 & 0.22 \\ \end{bmatrix}\]

$$

$$ \[\begin{bmatrix} -0.15 & 0.28 \\ 0.36 & 0.24 \\ \end{bmatrix}\]

$$

\[ \mathbf{X} = \mathbf{U} \mathbf{\Lambda} \mathbf{V} \tag{2}\]

$$ \[\begin{bmatrix} 0.00 & 0.00 \\ 0.00 & 0.18 \\ \end{bmatrix}\]

$$ {#eq-A1}

And the four options for the \(B_i\) matrices are:

Show the Code
# Four Simple translation Matrices
b <- vector(mode = "list", length = 4)
b[[1]] <- matrix(c(0, 0))
b[[2]] <- matrix(c(0, 1.6))
b[[3]] <- matrix(c(0, 1.6))
b[[4]] <- matrix(c(0, 0.54))
as_sym(b[[1]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(b[[2]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(b[[3]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(b[[4]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()

By randomly choosing any of the \(16\) resulting transformations, with different but fixed probablilities, we compute and render the Barnsley fern:

Show the Code
# Iteratively build the fern
theme_set(theme_custom())
#
n <- 50000
x <- numeric(n)
y <- numeric(n)
x[1] <- 0
y[1] <- 0 # Starting point (0,0). Can be anything!

for (i in 1:(n - 1)) {
  # Randomly sample the 4 + 4 translations based on a probability
  # Change these to try different kinds of ferns
  trans <- sample(1:4, prob = c(.02, .9, .09, .08), size = 1)

  # Translate **current** xy based on the selected translation
  # Apply one of 16 possible affine transformations
  xy <- A[[trans]] %*% c(x[i], y[i]) + b[[trans]]
  x[i + 1] <- xy[1] # Save x component
  y[i + 1] <- xy[2] # Save y component
}
# Plot this baby
# plot(y,x,col= "pink",cex=0.1)
gf_point(y ~ x,
  colour = "lightgreen", size = 0.02,
  title = "Barnsley Fern"
)

Show the Code
X <- matrix(c(5, 5), nrow = 2)
as_sym(A[[1]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(A[[1]]) %*% as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()

Vertical movement with Shrinkage

Show the Code
X <- matrix(c(5, 5), nrow = 2)
as_sym(A[[2]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(A[[2]]) %*% as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()

Modest Shrinkage of Both X and Y, X more than Y

Show the Code
X <- matrix(c(5, 5), nrow = 2)
as_sym(A[[3]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(A[[3]]) %*% as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()

Large Shrinkage of Both X and Y, Y more than X

Show the Code
X <- matrix(c(5, 5), nrow = 2)
as_sym(A[[4]])
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()
Show the Code
as_sym(A[[4]]) %*% as_sym(X)
Error in ensure_sympy(): Both Python3 and 'SymPy' >= 1.4 must be available.
Please verify Python version with 'reticulate::py_config()'.
Remember to configure reticulate (e.g. 'reticulate::use_condaenv("anaconda3")') before loading caracas.
To install SymPy, please run this command:
caracas::install_sympy()

Shrinkage of Both X and Y, X more than Y

Wait, But Why?

OK, so why did this become a fern??

If we look at the list of affine transformations, we see that there are essentially 4 movements possible: https://en.wikipedia.org/wiki/Barnsley_fern

  • a simple vertical y-axis movement, with shrinkage
  • a gentle rotation with very little shrinkage
  • a rotation to the right with shrinkage
  • a rotation to the left with shrinkage

The second transformation is the one most commonly used!! The others are relatively rarely used! So the points slowly slope to the right and do now get squashed up close to the start: they retain sufficient size in (x,y) coordinates for the fern to slowly spread to the right.

So we can design the affine transformations based on an intuition of how we might draw the fractal by hand, say larger strokes to the right, smaller to the left etc, and and decide on the frequency of strokes based on how often these strokes might be used in drawing.

References

  1. Ryan Bradley-Evans. (Oct 7, 2020). Barnsley’s Fern Fractal in R. https://astro-ryan.medium.com/barnsleys-fern-fractal-in-r-e52a357e23db
  2. Affine Transformations @ The Algorithm Archive. https://www.algorithm-archive.org/contents/affine_transformations/affine_transformations.html
  3. Iterated Function systems @ The Algorithm Archivehttps://www.algorithm-archive.org/contents/IFS/IFS.html
  4. p5.js Tutorial: Coordinates and Transformations. https://p5js.org/tutorials/coordinates-and-transformations/
  5. The Coding Train: Algorithmic Botany. https://thecodingtrain.com/tracks/algorithmic-botany
  6. Barnsley Fern @ Wikipedia https://en.wikipedia.org/wiki/Barnsley_fern
R Package Citations
Package Version Citation
caracas 2.1.1 Andersen and Højsgaard (2023)
matlib 1.0.0 Friendly, Fox, and Chalmers (2024)
Andersen, Mikkel Meyer, and Søren Højsgaard. 2023. caracas: Computer Algebra. https://doi.org/10.32614/CRAN.package.caracas.
Friendly, Michael, John Fox, and Phil Chalmers. 2024. matlib: Matrix Functions for Teaching and Learning Linear Algebra and Multivariate Statistics. https://doi.org/10.32614/CRAN.package.matlib.
Back to top

Citation

BibTeX citation:
@online{2025,
  author = {},
  title = {\textless Iconify-Icon Icon=“mdi:reiterate” Width=“1.2em”
    Height=“1.2em”\textgreater\textless/Iconify-Icon\textgreater{}
    \textless Iconify-Icon Icon=“gravity-Ui:function” Width=“1.2em”
    Height=“1.2em”\textgreater\textless/Iconify-Icon\textgreater{}
    {Affine} {Transformation} {Fractals}},
  date = {2025-01-28},
  url = {https://mathforcoders.netlify.app/content/courses/MathModelsDesign/Modules/25-Geometry/42-AffineFractals/},
  langid = {en}
}
For attribution, please cite this work as:
“<Iconify-Icon Icon=‘mdi:reiterate’ Width=‘1.2em’ Height=‘1.2em’></Iconify-Icon> <Iconify-Icon Icon=‘gravity-Ui:function’ Width=‘1.2em’ Height=‘1.2em’></Iconify-Icon> Affine Transformation Fractals.” 2025. January 28, 2025. https://mathforcoders.netlify.app/content/courses/MathModelsDesign/Modules/25-Geometry/42-AffineFractals/.
Iterated Functions
Chaos Games

License: CC BY-SA 2.0

Website made with ❤️ and Quarto, by Arvind V.

Hosted by Netlify .