2024-11-16 18:20:32 -05:00
---
slug: 2024/11/playing-with-fire
2024-11-17 17:30:07 -05:00
title: "Playing with fire: The fractal flame algorithm"
2024-11-16 18:20:32 -05:00
date: 2024-11-15 12:00:00
authors: [bspeice]
tags: []
---
2024-12-12 17:14:08 -05:00
2024-12-15 15:55:27 -05:00
Wikipedia describes [fractal flames](https://en.wikipedia.org/wiki/Fractal_flame) as:
2024-11-16 18:20:32 -05:00
> a member of the iterated function system class of fractals
2024-12-15 15:55:27 -05:00
It's tedious, but technically correct. I choose to think of them a different way: beauty in mathematics.
2024-11-16 18:20:32 -05:00
2024-11-17 17:30:07 -05:00
import isDarkMode from '@site/src/isDarkMode'
2024-11-29 19:25:29 -05:00
import banner from '../banner.png'
2024-11-16 18:20:32 -05:00
<center>
2024-11-29 19:25:29 -05:00
<img src={banner} style={{filter: isDarkMode() ? '' : 'invert(1)'}}/>
2024-11-16 18:20:32 -05:00
</center>
<!-- truncate -->
2024-12-16 21:29:03 -05:00
I don't remember when exactly I first learned about fractal flames, but I do remember being entranced by the images they created.
2024-12-08 22:50:46 -05:00
I also remember their unique appeal to my young engineering mind; this was an art form I could participate in.
2024-11-16 18:20:32 -05:00
2024-12-16 21:29:03 -05:00
The [Fractal Flame Algorithm paper](https://flam3.com/flame_draves.pdf) describing their structure was too much
2024-12-08 22:50:46 -05:00
for me to handle at the time (I was ~12 years old), so I was content to play around and enjoy the pictures.
2024-12-16 21:29:03 -05:00
But the desire to understand it stuck around. Now, with a graduate degree under my belt, I wanted to revisit it.
2024-12-08 22:50:46 -05:00
2024-12-16 21:29:03 -05:00
This guide is my attempt to explain how fractal flames work so that younger me — and others interested in the art —
2024-12-08 22:50:46 -05:00
can understand without too much prior knowledge.
2024-11-16 18:20:32 -05:00
---
## Iterated function systems
2024-12-09 22:18:13 -05:00
:::note
This post covers section 2 of the Fractal Flame Algorithm paper
:::
As mentioned, fractal flames are a type of "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system),"
2024-12-16 21:29:03 -05:00
or IFS. The formula for an IFS is short, but takes some time to work through:
2024-11-16 18:20:32 -05:00
$$
2024-12-08 22:50:46 -05:00
S = \bigcup_{i=0}^{n-1} F_i(S)
2024-11-16 18:20:32 -05:00
$$
2024-12-11 19:56:39 -05:00
### Solution set
2024-11-18 22:01:31 -05:00
2024-12-11 19:56:39 -05:00
First, $S$. $S$ is the set of points in two dimensions (in math terms, $S \in \mathbb{R}^2$)
that represent a "solution" of some kind to our equation.
2024-12-16 21:29:03 -05:00
Our goal is to find all the points in $S$, plot them, and display that image.
2024-11-16 18:20:32 -05:00
2024-12-11 19:56:39 -05:00
For example, if we say $S = \{(0,0), (1, 1), (2, 2)\}$, there are three points to plot:
2024-12-08 22:50:46 -05:00
2024-11-17 20:42:42 -05:00
import {VictoryChart, VictoryTheme, VictoryScatter, VictoryLegend} from "victory";
export const simpleData = [
{x: 0, y: 0},
{x: 1, y: 1},
{x: 2, y: 2}
]
2024-11-16 18:20:32 -05:00
2024-11-17 20:42:42 -05:00
<VictoryChart theme={VictoryTheme.clean}>
<VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/>
</VictoryChart>
2024-11-16 18:20:32 -05:00
2024-12-16 21:29:03 -05:00
With fractal flames, rather than listing individual points, we use functions to describe the solution.
This means there are an infinite number of points, but if we find _enough_ points to plot, we get a nice picture.
And if the functions change, the solution also changes, and we get something new.
2024-12-09 22:18:13 -05:00
2024-12-11 19:56:39 -05:00
### Transform functions
2024-12-09 22:18:13 -05:00
2024-12-11 19:56:39 -05:00
Second, the $F_i(S)$ functions, also known as "transforms."
2024-12-16 21:29:03 -05:00
Each transform takes in a 2-dimensional point and gives a new point back
2024-12-11 19:56:39 -05:00
(in math terms, $F_i \in \mathbb{R}^2 \rightarrow \mathbb{R}^2$).
2024-12-16 21:29:03 -05:00
While you could theoretically use any function, we'll focus on a specific kind of function
called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)." Every transform uses the same formula:
2024-12-08 22:50:46 -05:00
2024-12-11 19:56:39 -05:00
$$
2024-12-15 21:19:09 -05:00
F_i(a_i x + b_i y + c_i, d_i x + e_i y + f_i)
2024-12-11 19:56:39 -05:00
$$
2024-12-08 22:50:46 -05:00
2024-12-11 19:56:39 -05:00
import transformSource from "!!raw-loader!../src/transform"
2024-12-12 17:14:08 -05:00
import CodeBlock from '@theme/CodeBlock'
2024-11-16 18:20:32 -05:00
2024-12-11 19:56:39 -05:00
<CodeBlock language="typescript">{transformSource}</CodeBlock>
2024-11-16 18:20:32 -05:00
2024-12-16 21:29:03 -05:00
The parameters ($a_i$, $b_i$, etc.) are values we choose.
For example, we can define a "shift" function like this:
2024-11-16 18:20:32 -05:00
$$
2024-12-11 19:56:39 -05:00
\begin{align*}
a &= 1 \\
b &= 0 \\
c &= 0.5 \\
d &= 0 \\
e &= 1 \\
f &= 1.5 \\
2024-12-15 21:19:09 -05:00
F_{shift}(x, y) &= (1 \cdot x + 0.5, 1 \cdot y + 1.5)
2024-12-11 19:56:39 -05:00
\end{align*}
2024-11-16 18:20:32 -05:00
$$
2024-12-16 21:29:03 -05:00
Applying this transform to the original points gives us a new set of points:
2024-11-16 18:20:32 -05:00
2024-12-11 19:56:39 -05:00
import {applyCoefs} from "../src/transform"
export const coefs = {a: 1, b: 0, c: 0.5, d: 0, e: 1, f: 1.5}
export const toData = ([x, y]) => ({x, y})
export const shiftData = simpleData.map(({x, y}) => toData(applyCoefs(x, y, coefs)))
2024-11-17 20:42:42 -05:00
<VictoryChart theme={VictoryTheme.clean}>
<VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/>
<VictoryScatter data={shiftData} size={5} style={{data: {fill: "orange"}}}/>
<VictoryLegend
2024-11-16 18:20:32 -05:00
data={[
2024-11-17 20:42:42 -05:00
{name: "(x,y)", symbol: {fill: "blue"}},
{name: "F(x,y)", symbol: {fill: "orange"}}
2024-11-16 18:20:32 -05:00
]}
2024-11-17 20:42:42 -05:00
orientation={"vertical"}
x={75}
y={10}
2024-11-16 18:20:32 -05:00
/>
2024-11-17 20:42:42 -05:00
</VictoryChart>
2024-11-16 18:20:32 -05:00
2024-12-11 19:56:39 -05:00
Fractal flames use more complex functions, but they all start with this structure.
2024-11-16 18:20:32 -05:00
2024-12-11 19:56:39 -05:00
### Fixed set
2024-12-12 17:14:08 -05:00
With those definitions in place, let's revisit the initial problem:
2024-11-16 18:20:32 -05:00
$$
2024-12-11 19:56:39 -05:00
S = \bigcup_{i=0}^{n-1} F_i(S)
2024-11-16 18:20:32 -05:00
$$
2024-12-16 21:29:03 -05:00
Or, in English, we might say:
2024-12-12 17:14:08 -05:00
> Our solution, $S$, is the union of all sets produced by applying each function, $F_i$,
2024-12-11 19:56:39 -05:00
> to points in the solution.
2024-12-15 21:35:03 -05:00
There's just one small problem: to find the solution, we must already know which points are in the solution.
What?
2024-12-12 17:14:08 -05:00
2024-12-15 21:35:03 -05:00
John E. Hutchinson provides an explanation in the [original paper](https://maths-people.anu.edu.au/~john/Assets/Research%20Papers/fractals_self-similarity.pdf)
defining the mathematics of iterated function systems:
2024-12-12 17:14:08 -05:00
> Furthermore, $S$ is compact and is the closure of the set of fixed points $s_{i_1...i_p}$
> of finite compositions $F_{i_1...i_p}$ of members of $F$.
2024-12-15 15:55:27 -05:00
Before your eyes glaze over, let's unpack this:
2024-12-12 17:14:08 -05:00
2024-12-15 21:35:03 -05:00
- **Furthermore, $S$ is [compact](https://en.wikipedia.org/wiki/Compact_space)...**: All points in our solution will be in a finite range
2024-12-12 17:14:08 -05:00
- **...and is the [closure](https://en.wikipedia.org/wiki/Closure_(mathematics)) of the set of [fixed points](https://en.wikipedia.org/wiki/Fixed_point_(mathematics))**:
2024-12-15 15:55:27 -05:00
Applying our functions to points in the solution will give us other points that are in the solution
2024-12-12 17:14:08 -05:00
- **...of finite compositions $F_{i_1...i_p}$ of members of $F$**: By composing our functions (that is,
2024-12-16 21:29:03 -05:00
using the output of one function as input to the next), we will arrive at the points in the solution
2024-12-12 17:14:08 -05:00
2024-12-15 15:55:27 -05:00
Thus, by applying the functions to fixed points of our system, we will find the other points we care about.
2024-12-12 17:14:08 -05:00
<details>
2024-12-15 15:55:27 -05:00
<summary>If you want a bit more math...</summary>
2024-12-12 17:14:08 -05:00
2024-12-15 15:55:27 -05:00
...then there are some extra details I've glossed over so far.
2024-12-12 17:14:08 -05:00
2024-12-15 15:55:27 -05:00
First, the Hutchinson paper requires that the functions $F_i$ be _contractive_ for the solution set to exist.
2024-12-15 21:19:09 -05:00
That is, applying the function to a point must bring it closer to other points. However, as the fractal flame
2024-12-12 17:14:08 -05:00
algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will
degenerate and produce a bad image.
2024-12-16 21:29:03 -05:00
Second, we're focused on $\mathbb{R}^2$ because we're generating images, but the math
2024-12-15 15:55:27 -05:00
allows for arbitrary dimensions; you could also have 3-dimensional fractal flames.
2024-12-12 17:14:08 -05:00
2024-12-15 15:55:27 -05:00
Finally, there's a close relationship between fractal flames and [attractors](https://en.wikipedia.org/wiki/Attractor).
2024-12-16 21:29:03 -05:00
Specifically, the fixed points of $S$ act as attractors for the chaos game (explained below).
2024-12-12 17:14:08 -05:00
</details>
2024-11-16 18:20:32 -05:00
2024-12-15 15:55:27 -05:00
This is still a bit vague, so let's work through an example.
## [Sierpinski's gasket](https://www.britannica.com/biography/Waclaw-Sierpinski)
2024-11-16 18:20:32 -05:00
2024-12-15 21:35:03 -05:00
The Fractal Flame paper gives three functions to use for a first IFS:
2024-11-16 18:20:32 -05:00
$$
2024-12-08 19:53:06 -05:00
F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\
~\\
F_1(x, y) = \left({{x + 1} \over 2}, {y \over 2} \right) \\
~\\
2024-11-16 18:20:32 -05:00
F_2(x, y) = \left({x \over 2}, {{y + 1} \over 2} \right)
$$
### The chaos game
2024-12-16 21:29:03 -05:00
Now, how do we find the "fixed points" mentioned earlier? The paper lays out an algorithm called the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)"
2024-12-15 21:35:03 -05:00
that gives us points in the solution:
2024-11-16 18:20:32 -05:00
$$
\begin{align*}
2024-12-08 19:53:06 -05:00
&(x, y) = \text{random point in the bi-unit square} \\
2024-11-16 18:20:32 -05:00
&\text{iterate } \{ \\
2024-12-08 19:53:06 -05:00
&\hspace{1cm} i = \text{random integer from 0 to } n - 1 \\
2024-11-16 18:20:32 -05:00
&\hspace{1cm} (x,y) = F_i(x,y) \\
2024-12-08 19:53:06 -05:00
&\hspace{1cm} \text{plot}(x,y) \text{ if iterations} > 20 \\
2024-11-16 18:20:32 -05:00
\}
\end{align*}
2024-11-17 17:30:07 -05:00
$$
2024-12-13 20:03:53 -05:00
:::note
2024-12-15 15:55:27 -05:00
The chaos game algorithm is effectively the "finite compositions of $F_{i_1..i_p}$" mentioned earlier.
2024-12-13 20:03:53 -05:00
:::
2024-12-15 21:35:03 -05:00
Let's turn this into code, one piece at a time.
2024-11-17 17:30:07 -05:00
2024-12-16 21:29:03 -05:00
To start, we need to generate some random numbers. The "bi-unit square" is the range $[-1, 1]$,
and we can do this using an existing API:
2024-11-17 17:30:07 -05:00
2024-11-24 22:37:53 -05:00
import biunitSource from '!!raw-loader!../src/randomBiUnit'
2024-11-17 17:30:07 -05:00
2024-11-23 15:27:02 -05:00
<CodeBlock language="typescript">{biunitSource}</CodeBlock>
2024-11-17 17:30:07 -05:00
Next, we need to choose a random integer from $0$ to $n - 1$:
2024-11-24 22:37:53 -05:00
import randintSource from '!!raw-loader!../src/randomInteger'
2024-11-17 17:30:07 -05:00
2024-11-23 15:27:02 -05:00
<CodeBlock language="typescript">{randintSource}</CodeBlock>
2024-11-17 17:30:07 -05:00
2024-12-11 19:56:39 -05:00
### Plotting
2024-12-16 21:29:03 -05:00
Finally, implementing the `plot` function. This blog series is interactive,
2024-12-15 15:55:27 -05:00
so everything displays directly in the browser. As an alternative,
software like `flam3` and Apophysis can "plot" by saving an image to disk.
2024-12-11 19:56:39 -05:00
2024-12-16 21:29:03 -05:00
To see the results, we'll use the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API).
This allows us to manipulate individual pixels in an image and show it on screen.
2024-12-11 19:56:39 -05:00
2024-12-16 21:29:03 -05:00
First, we need to convert from fractal flame coordinates to pixel coordinates.
2024-12-13 23:28:35 -05:00
To simplify things, we'll assume that we're plotting a square image
with range $[0, 1]$ for both $x$ and $y$:
2024-12-11 19:56:39 -05:00
import cameraSource from "!!raw-loader!./cameraGasket"
<CodeBlock language="typescript">{cameraSource}</CodeBlock>
2024-12-15 15:55:27 -05:00
Next, we'll store the pixel data in an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData).
2024-12-16 21:29:03 -05:00
Each pixel on screen has a corresponding index in the `data` array.
2024-12-15 15:55:27 -05:00
To plot a point, we set that pixel to be black:
2024-11-17 17:30:07 -05:00
import plotSource from '!!raw-loader!./plot'
<CodeBlock language="typescript">{plotSource}</CodeBlock>
2024-12-11 19:56:39 -05:00
Putting it all together, we have our first image:
2024-11-17 17:30:07 -05:00
import Playground from '@theme/Playground'
import Scope from './scope'
2024-11-24 00:06:22 -05:00
import chaosGameSource from '!!raw-loader!./chaosGame'
2024-11-17 17:30:07 -05:00
2024-11-24 00:06:22 -05:00
<Playground scope={Scope} noInline={true}>{chaosGameSource}</Playground>
2024-11-17 20:42:42 -05:00
<hr/>
<small>
2024-12-15 15:55:27 -05:00
The image here is slightly different than in the paper.
2024-12-15 21:19:09 -05:00
I think the paper has an error, so I'm plotting the image
like the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
2024-11-19 21:42:03 -05:00
</small>
2024-12-13 20:03:53 -05:00
### Weights
2024-12-16 21:29:03 -05:00
There's one last step before we finish the introduction. So far, each transform has
the same chance of being picked in the chaos game.
We can change that by giving them a "weight" ($w_i$) instead:
2024-11-24 22:37:53 -05:00
import randomChoiceSource from '!!raw-loader!../src/randomChoice'
<CodeBlock language={'typescript'}>{randomChoiceSource}</CodeBlock>
2024-12-16 21:29:03 -05:00
If we let the chaos game run forever, these weights wouldn't matter.
But because the iteration count is limited, changing the weights
means we don't plot some parts of the image:
2024-11-24 22:37:53 -05:00
import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
2024-12-15 15:55:27 -05:00
:::tip
Double-click the image if you want to save a copy!
:::
2024-11-29 23:22:31 -05:00
import GasketWeighted from "./GasketWeighted";
2024-12-08 19:53:06 -05:00
import {SquareCanvas} from "../src/Canvas";
2024-11-24 22:37:53 -05:00
2024-12-15 15:55:27 -05:00
<SquareCanvas name={"gasket_weighted"}><GasketWeighted/></SquareCanvas>
2024-12-13 20:03:53 -05:00
## Summary
Studying the foundations of fractal flames is challenging,
2024-12-16 21:29:03 -05:00
but we now have an understanding of the mathematics
and the implementation of iterated function systems.
2024-12-13 20:03:53 -05:00
2024-12-16 21:29:03 -05:00
In the next post, we'll look at the first innovation of fractal flame algorithm: variations.