First pass proof-reading

This commit is contained in:
Bradlee Speice 2024-12-15 15:55:27 -05:00
parent a05acf6748
commit 1ce6137c17
7 changed files with 132 additions and 118 deletions

View File

@ -14,7 +14,7 @@ export function Render({f}) {
export default function Gasket({f}) { export default function Gasket({f}) {
return ( return (
<SquareCanvas> <SquareCanvas name={"gasket"}>
<Render f={f}/> <Render f={f}/>
</SquareCanvas> </SquareCanvas>
) )

View File

@ -31,7 +31,7 @@ export default function GasketWeighted() {
<> <>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<p><TeX>{title}</TeX>: {weight}</p> <p><TeX>{title}</TeX>: {weight}</p>
<input type={'range'} min={0} max={1} step={.01} style={{width: '100%', background: 'transparent'}} value={weight} <input type={'range'} min={0} max={1} step={.01} value={weight}
onInput={e => setWeight(Number(e.currentTarget.value))}/> onInput={e => setWeight(Number(e.currentTarget.value))}/>
</div> </div>
</> </>

View File

@ -7,11 +7,11 @@ tags: []
--- ---
Wikipedia describes fractal flames fractal flames as: Wikipedia describes [fractal flames](https://en.wikipedia.org/wiki/Fractal_flame) as:
> a member of the iterated function system class of fractals > a member of the iterated function system class of fractals
It's a bit tedious, but technically correct. I choose to think of them a different way: beauty in mathematics. It's tedious, but technically correct. I choose to think of them a different way: beauty in mathematics.
import isDarkMode from '@site/src/isDarkMode' import isDarkMode from '@site/src/isDarkMode'
import banner from '../banner.png' import banner from '../banner.png'
@ -22,14 +22,15 @@ import banner from '../banner.png'
<!-- truncate --> <!-- truncate -->
I don't remember exactly when I first learned about fractal flames, but I do remember becoming entranced by the images they created. I don't remember exactly when I first learned about fractal flames, but I do remember being entranced by the images they created.
I also remember their unique appeal to my young engineering mind; this was an art form I could participate in. I also remember their unique appeal to my young engineering mind; this was an art form I could participate in.
The original [Fractal Flame Algorithm paper](https://flam3.com/flame_draves.pdf) describing their structure was too much The original [Fractal Flame Algorithm paper](https://flam3.com/flame_draves.pdf) describing their structure was too much
for me to handle at the time (I was ~12 years old), so I was content to play around and enjoy the pictures. for me to handle at the time (I was ~12 years old), so I was content to play around and enjoy the pictures.
But the desire to understand it stuck around. Now, with a graduate degree under my belt, maybe I can make some progress. But the desire to understand it stuck around. Now, with a graduate degree under my belt, I want to revisit it
and try to make some progress.
This guide is my attempt to explain fractal flames in a way that younger me &mdash; and others interested in the art &mdash; This guide is my attempt to explain how fractal flames work in a way that younger me &mdash; and others interested in the art &mdash;
can understand without too much prior knowledge. can understand without too much prior knowledge.
--- ---
@ -41,7 +42,7 @@ 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)," As mentioned, fractal flames are a type of "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system),"
or IFS. The formula for an IFS is short, but will take some time to unpack: or IFS. The formula for an IFS is short, but takes some time to unpack:
$$ $$
S = \bigcup_{i=0}^{n-1} F_i(S) S = \bigcup_{i=0}^{n-1} F_i(S)
@ -69,15 +70,15 @@ export const simpleData = [
However, this is a pretty boring image. With fractal flames, rather than listing individual points, However, this is a pretty boring image. With fractal flames, rather than listing individual points,
we use functions to describe which points are part of the solution. This means there are an infinite we use functions to describe which points are part of the solution. This means there are an infinite
number of points, but if we find _enough_ points to plot, we'll end up with a nice picture. number of points, but if we find _enough_ points to plot, we'll end up with a nice picture.
And if we change the functions, our solution changes, and we'll get a new picture. And if the functions change, the solution also changes, and we get a new picture.
### Transform functions ### Transform functions
Second, the $F_i(S)$ functions, also known as "transforms." Second, the $F_i(S)$ functions, also known as "transforms."
At their most basic, each $F_i$ takes in a 2-dimensional point and gives back a new point At their most basic, each $F_i$ takes in a 2-dimensional point and gives back a new point
(in math terms, $F_i \in \mathbb{R}^2 \rightarrow \mathbb{R}^2$). (in math terms, $F_i \in \mathbb{R}^2 \rightarrow \mathbb{R}^2$).
While you could theoretically use any function, we'll focus on a specific kind of function While you could theoretically use any function, we'll start with a specific kind of function
known as an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)." called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)."
The general form of an affine transformation is: The general form of an affine transformation is:
@ -102,7 +103,7 @@ d &= 0 \\
e &= 1 \\ e &= 1 \\
f &= 1.5 \\ f &= 1.5 \\
F_{shift}(x,y) &= (1 \cdot x + 0 \cdot y + 0.5, 0 \cdot x + 1 \cdot y + 0.5) \\ F_{shift}(x,y) &= (1 \cdot x + 0 \cdot y + 0.5, 0 \cdot x + 1 \cdot y + 0.5) \\
F_{shift}(x, y) &= (x + 0.5, y + 0.5) F_{shift}(x, y) &= (x + 0.5, y + 1.5)
\end{align*} \end{align*}
$$ $$
@ -139,7 +140,7 @@ $$
S = \bigcup_{i=0}^{n-1} F_i(S) S = \bigcup_{i=0}^{n-1} F_i(S)
$$ $$
Or, to put it in English, we would get something like this: Or, to put it in English, we might say:
> Our solution, $S$, is the union of all sets produced by applying each function, $F_i$, > Our solution, $S$, is the union of all sets produced by applying each function, $F_i$,
> to points in the solution. > to points in the solution.
@ -154,40 +155,41 @@ explaining the mathematics of iterated function systems:
> of finite compositions $F_{i_1...i_p}$ of members of $F$. > of finite compositions $F_{i_1...i_p}$ of members of $F$.
:::note :::note
I've tweaked the wording slightly to match the conventions in the Fractal Flame paper I've tweaked the wording slightly to match the conventions of the Fractal Flame paper
::: :::
Before your eyes glaze over, let's unpack this explanation: Before your eyes glaze over, let's unpack this:
- **$S$ is [compact](https://en.wikipedia.org/wiki/Compact_space)...**: All points in our solution will be in a finite range - **$S$ is [compact](https://en.wikipedia.org/wiki/Compact_space)...**: All points in our solution will be in a finite range
- **...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))**: - **...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))**:
Applying our functions to these points does not change them Applying our functions to points in the solution will give us other points that are in the solution
- **...of finite compositions $F_{i_1...i_p}$ of members of $F$**: By composing our functions (that is, - **...of finite compositions $F_{i_1...i_p}$ of members of $F$**: By composing our functions (that is,
using the output of one function as input to the next function), we will arrive at the points we care about using the output of one function as input to the next function), we will arrive at the points in the solution
Thus, by applying the functions in our system to "fixed points," we will find the other points we care about. Thus, by applying the functions to fixed points of our system, we will find the other points we care about.
However, this is all a bit vague, so let's work through an example.
<details> <details>
<summary>If you want a bit more math first...</summary> <summary>If you want a bit more math...</summary>
...then there are some details worth mentioning that I've glossed over so far. ...then there are some extra details I've glossed over so far.
First, the Hutchinson paper requires that the functions $F_i$ be _contractive_ tor the solution set to exist. First, the Hutchinson paper requires that the functions $F_i$ be _contractive_ for the solution set to exist.
That is, applying the function to a point must bring it closer to other points. However, as the Fractal Flame That is, applying the function to a point must bring it closer to other points. However, as the Fractal Flame
algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will
degenerate and produce a bad image. degenerate and produce a bad image.
Second, we're focused $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper Second, we're focused $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper
allows for arbitrary dimensions - which means you could also have 3-dimensional fractal flames. allows for arbitrary dimensions; you could also have 3-dimensional fractal flames.
TODO: Mention attractors? https://en.wikipedia.org/wiki/Chaos_game Finally, there's a close relationship between fractal flames and [attractors](https://en.wikipedia.org/wiki/Attractor).
Specifically, the fixed points of $S$ act as attractors for the chaos game (explained in detail below).
</details> </details>
## Sierpinski's gasket This is still a bit vague, so let's work through an example.
The Fractal Flame paper gives us three functions we can use for our function system: ## [Sierpinski's gasket](https://www.britannica.com/biography/Waclaw-Sierpinski)
The Fractal Flame paper gives us three functions to use for our first IFS:
$$ $$
F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\ F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\
@ -200,7 +202,7 @@ $$
### The chaos game ### The chaos game
Next, how do we find the "fixed points" we mentioned earlier? The paper lays out an algorithm called the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)" Next, how do we find the "fixed points" we mentioned earlier? The paper lays out an algorithm called the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)"
that will give us points in the solution set. that gives us points in the solution set:
$$ $$
\begin{align*} \begin{align*}
@ -214,12 +216,13 @@ $$
$$ $$
:::note :::note
In effect, the chaos game algorithm implements the "finite compositions of $F_{i_1..i_p}$ mentioned earlier. The chaos game algorithm is effectively the "finite compositions of $F_{i_1..i_p}$" mentioned earlier.
::: :::
Now, let's turn this into code, one piece at a time. Now, let's turn this into code, one piece at a time.
First, the "bi-unit square" is the range $[-1, 1]$. We can : First, we need to generate some random numbers. The "bi-unit square" is the range $[-1, 1]$,
so we generate a random point using an existing API:
import biunitSource from '!!raw-loader!../src/randomBiUnit' import biunitSource from '!!raw-loader!../src/randomBiUnit'
@ -233,14 +236,12 @@ import randintSource from '!!raw-loader!../src/randomInteger'
### Plotting ### Plotting
Finally, implementing the `plot` function. This blog series Finally, implementing the `plot` function. This blog series is designed to be interactive,
is designed to be interactive, so everything shows so everything displays directly in the browser. As an alternative,
real-time directly in the browser. As an alternative, software like `flam3` and Apophysis can "plot" by saving an image to disk.
software like `flam3` an Apophysis can also save an image.
To display the results, we'll use the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). To show 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 an image, This allows us to manipulate individual pixels in an image and display it on screen.
and display it on screen.
First, we need to convert from Fractal Flame coordinates to pixel coordinates. First, we need to convert from Fractal Flame coordinates to pixel coordinates.
To simplify things, we'll assume that we're plotting a square image To simplify things, we'll assume that we're plotting a square image
@ -250,10 +251,9 @@ import cameraSource from "!!raw-loader!./cameraGasket"
<CodeBlock language="typescript">{cameraSource}</CodeBlock> <CodeBlock language="typescript">{cameraSource}</CodeBlock>
Next, we'll use an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) Next, we'll store the pixel data in an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData).
to store the pixel data.
Each pixel in the image on screen has a corresponding index in the `data` array. Each pixel in the image on screen has a corresponding index in the `data` array.
To plot our image, we set that pixel to be black: To plot a point, we set that pixel to be black:
import plotSource from '!!raw-loader!./plot' import plotSource from '!!raw-loader!./plot'
@ -271,18 +271,16 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
<hr/> <hr/>
<small> <small>
The image here is slightly different than the one in the paper. The image here is slightly different than in the paper.
I think the paper has an error, so I'm choosing to plot the image I think the paper has an error, so I'm choosing to plot the image
in a way that's consistent with [`flam3` itself](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441). the same way as the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
</small> </small>
### Weights ### Weights
Finally, we'll introduce a "weight" ($w_i$) for each function that controls how often we choose There's one last step before we finish the introduction. So far, each function $F_i$ has
that function during the chaos game relative to each other function. the same chance of being chosen. We can change that by introducing a "weight" ($w_i$)
to each transform in the chaos game:
For Sierpinski's Gasket, we start with equal weighting,
but you can see how changing the function weights affects the image below:
import randomChoiceSource from '!!raw-loader!../src/randomChoice' import randomChoiceSource from '!!raw-loader!../src/randomChoice'
@ -292,16 +290,23 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock> <CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
For Sierpinski's Gasket, we start with equal weighting,
but changing the transform weights affects how often
the chaos game "visits" parts of the image:
:::tip
Double-click the image if you want to save a copy!
:::
import GasketWeighted from "./GasketWeighted"; import GasketWeighted from "./GasketWeighted";
import {SquareCanvas} from "../src/Canvas"; import {SquareCanvas} from "../src/Canvas";
<SquareCanvas><GasketWeighted/></SquareCanvas> <SquareCanvas name={"gasket_weighted"}><GasketWeighted/></SquareCanvas>
## Summary ## Summary
Studying the foundations of fractal flames is challenging, Studying the foundations of fractal flames is challenging,
but we now have an understanding of both the mathematics but we now have an understanding of both the mathematics
and implementation of iterated function systems. and implementations of iterated function systems.
In the next post, we'll study the first innovation that fractal flames In the next post, we'll study the first innovation of fractal flames: variations.
bring: variations.

View File

@ -12,9 +12,9 @@ shapes and patterns that fractal flames are known for.
<!-- truncate --> <!-- truncate -->
:::info :::info
This post uses a set of [reference parameters](../params.flame) to demonstrate the fractal flame algorithm. This post uses [reference parameters](../params.flame) to demonstrate the fractal flame algorithm.
If you're interested in tweaking the parameters, or generating your own art, [Apophysis](https://sourceforge.net/projects/apophysis/) If you're interested in tweaking the parameters, or generating your own art, [Apophysis](https://sourceforge.net/projects/apophysis/)
can load that file and you can try tweaking things yourself! can load that file to play around with!
::: :::
## Variations ## Variations
@ -30,8 +30,8 @@ playing the chaos game leads to an image of Sierpinski's Gasket. Even though we
the image it generates is exciting. But it's still not nearly as exciting as the images the Fractal Flame the image it generates is exciting. But it's still not nearly as exciting as the images the Fractal Flame
algorithm is known for. algorithm is known for.
This leads us to the first big innovation of the Fractal Flame algorithm: applying non-linear functions This leads us to the first big innovation of the Fractal Flame algorithm: adding non-linear functions
after the affine transform has happened. These functions are called "variations": after the affine transform. These functions are called "variations":
$$ $$
F_i(x, y) = V_j(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i) F_i(x, y) = V_j(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i)
@ -41,8 +41,9 @@ import variationSource from '!!raw-loader!../src/variation'
<CodeBlock language="typescript">{variationSource}</CodeBlock> <CodeBlock language="typescript">{variationSource}</CodeBlock>
Just like transforms, variations ($V_j$) are functions that map $(x, y)$ coordinates to new coordinates. Just like transforms, variations ($V_j$) are functions that take in $(x, y)$ coordinates
However, the sky is the limit for what we can do in between input and output. and give back new $(x, y)$ coordinates.
However, the sky is the limit for what happens between input and output.
The Fractal Flame paper lists 49 different variation functions, The Fractal Flame paper lists 49 different variation functions,
and the official `flam3` implementation supports [98 different variations](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c). and the official `flam3` implementation supports [98 different variations](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c).
@ -70,8 +71,6 @@ apply the affine coefficients to the input point, and use that as the output poi
This variation is a good example of the non-linear functions we can use. It uses both trigonometry This variation is a good example of the non-linear functions we can use. It uses both trigonometry
and probability to produce interesting shapes: and probability to produce interesting shapes:
TODO: Connection with the julia set?
$$ $$
\begin{align*} \begin{align*}
r &= \sqrt{x^2 + y^2} \\ r &= \sqrt{x^2 + y^2} \\
@ -93,7 +92,7 @@ import juliaSrc from '!!raw-loader!../src/julia'
### Popcorn (variation 17) ### Popcorn (variation 17)
Some variations rely on knowing their transform's affine coefficients; they're known as "dependent variations." Some variations rely on knowing the transform's affine coefficients; they're called "dependent variations."
For the popcorn variation, we use $c$ and $f$: For the popcorn variation, we use $c$ and $f$:
$$ $$
@ -106,7 +105,7 @@ import popcornSrc from '!!raw-loader!../src/popcorn'
### PDJ (variation 24) ### PDJ (variation 24)
Some variations have extra parameters we can choose; these are known as "parametric variations." Some variations have extra parameters; they're called "parametric variations."
For the PDJ variation, there are four extra parameters we can choose: For the PDJ variation, there are four extra parameters we can choose:
$$ $$
@ -122,7 +121,7 @@ import pdjSrc from '!!raw-loader!../src/pdj'
Now, one variation is fun, but we can also combine variations in a process called "blending." Now, one variation is fun, but we can also combine variations in a process called "blending."
Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs. Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs.
We'll also give each variation a weight (called $v_{ij}$) that changes how much it contributes to the transform: We'll also give each variation a weight ($v_{ij}$) that changes how much it contributes to the result:
$$ $$
F_i(x,y) = \sum_{j} v_{ij} V_j(a_i \cdot x + b_i \cdot y + c_i, \hspace{0.2cm} d_i \cdot x + e_i \cdot y + f_i) F_i(x,y) = \sum_{j} v_{ij} V_j(a_i \cdot x + b_i \cdot y + c_i, \hspace{0.2cm} d_i \cdot x + e_i \cdot y + f_i)
@ -144,11 +143,12 @@ Try using the variation weight sliders to figure out which parts of the image ea
import {SquareCanvas} from "../src/Canvas"; import {SquareCanvas} from "../src/Canvas";
import FlameBlend from "./FlameBlend"; import FlameBlend from "./FlameBlend";
<SquareCanvas><FlameBlend/></SquareCanvas> <SquareCanvas name={"flame_blend"}><FlameBlend/></SquareCanvas>
## Post transforms ## Post transforms
Next, we'll introduce a second affine transform, known as a post transform, that is applied _after_ variation blending. Next, we'll introduce a second affine transform, applied _after_ variation blending. This is called a "post transform."
We'll use some new variables, but the post transform function should look familiar: We'll use some new variables, but the post transform function should look familiar:
$$ $$
@ -163,19 +163,18 @@ import postSource from '!!raw-loader!./post'
<CodeBlock language="typescript">{postSource}</CodeBlock> <CodeBlock language="typescript">{postSource}</CodeBlock>
The image below uses the same transforms/variations as the previous fractal flame, The image below uses the same transforms/variations as the previous fractal flame,
but allows modifying the post-transform coefficients. but allows modifying the post-transform coefficients:
<details> <details>
<summary>If you want to test your understanding...</summary> <summary>If you want to test your understanding...</summary>
Challenge 1: What post-transform coefficients will give us the previous image? - What post-transform coefficients will give us the previous image?
- What post-transform coefficients will give us a _mirrored_ image?
Challenge 2: What post-transform coefficients will give us a _mirrored_ image?
</details> </details>
import FlamePost from "./FlamePost"; import FlamePost from "./FlamePost";
<SquareCanvas><FlamePost/></SquareCanvas> <SquareCanvas name={"flame_post"}><FlamePost/></SquareCanvas>
## Final transforms ## Final transforms
@ -204,12 +203,12 @@ import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal"
import FlameFinal from "./FlameFinal"; import FlameFinal from "./FlameFinal";
<SquareCanvas><FlameFinal/></SquareCanvas> <SquareCanvas name={"flame_final"}><FlameFinal/></SquareCanvas>
## Summary ## Summary
Variations are the fractal flame algorithm's first major innovation over previous IFS's. Variations are the fractal flame algorithm's first major innovation.
Blending variation functions and post/final transforms allows us to generate interesting images. By blending variation functions, and post/final transforms, we generate unique images.
However, the images themselves are grainy and unappealing. In the next post, we'll clean up However, the images themselves are grainy and unappealing. In the next post, we'll clean up
the image quality and add some color. the image quality and add some color.

View File

@ -6,9 +6,9 @@ authors: [bspeice]
tags: [] tags: []
--- ---
So far, our `plot()` function has been fairly simple; map an input coordinate to a specific pixel, So far, our `plot()` function has been fairly simple; map a fractal flame coordinate to a specific pixel,
and color in that pixel. This works well for simple function systems (like Sierpinski's Gasket), and color in that pixel. This works well for simple function systems (like Sierpinski's Gasket),
but more complex systems (like our reference parameters) produce grainy images. but more complex systems (like the reference parameters) produce grainy images.
In this post, we'll refine the image quality and add color to really make things shine. In this post, we'll refine the image quality and add color to really make things shine.
@ -20,20 +20,21 @@ In this post, we'll refine the image quality and add color to really make things
This post covers sections 4 and 5 of the Fractal Flame Algorithm paper This post covers sections 4 and 5 of the Fractal Flame Algorithm paper
::: :::
To start, it's worth demonstrating how much work is actually "wasted" One problem with the existing chaos game is that we waste work
when we treat pixels as a binary "on" (opaque) or "off" (transparent). by treating pixels as a binary "on" (opaque) or "off" (transparent).
If the chaos game encounters the same location twice, nothing actually changes.
We'll render the reference image again, but this time, count each time To demonstrate how much work is wasted, we'll render the reference image again.
we encounter a pixel during the chaos game. This gives us a kind of "histogram" However, we'll also count each time the chaos game encounters a pixel.
of the image: This gives us a kind of image "histogram":
import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram" import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram"
<CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock> <CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock>
When the chaos game finishes, find the pixel we encountered most frequently. When the chaos game finishes, we find the pixel encountered most frequently.
Finally, "paint" the image by setting each pixel's alpha value (transparency) Finally, we "paint" the image by setting each pixel's alpha value (transparency)
to the ratio of times encountered, divided by the maximum value: to the ratio of times encountered divided by the maximum:
import CodeBlock from "@theme/CodeBlock"; import CodeBlock from "@theme/CodeBlock";
@ -49,27 +50,27 @@ import {paintLinear} from "./paintLinear";
## Tone mapping ## Tone mapping
While using a histogram to paint the image improves the quality, it also leads to some parts vanishing entirely. While using a histogram reduces the "graininess" of the image, it also leads to some parts vanishing entirely.
In the reference parameters, the outer circle is preserved, but the interior appears to be missing! In the reference parameters, the outer circle is preserved, but the interior appears to be missing!
To fix this, we'll introduce the second major innovation of the fractal flame algorithm: [tone mapping](https://en.wikipedia.org/wiki/Tone_mapping). To fix this, we'll introduce the second major innovation of the fractal flame algorithm: [tone mapping](https://en.wikipedia.org/wiki/Tone_mapping).
This is a technique used in computer graphics to compensate for differences in how This is a technique used in computer graphics to compensate for differences in how
computers represent color, and how people see color. computers represent brightness, and how people see brightness.
As a concrete example, high dynamic range (HDR) photography uses this technique to capture As a concrete example, high dynamic range (HDR) photography uses this technique to capture
nice images of scenes with wide brightness ranges. To take a picture of something dark, nice images of scenes with a wide range of brightnesses. To take a picture of something dark,
you need a long exposure time. However, long exposures can lead to "hot spots" in images that are pure white. you need a long exposure time. However, long exposures lead to "hot spots" (sections that are pure white).
By taking multiple pictures using different exposure times, we can combine them to create By taking multiple pictures with different exposure times, we can combine them to create
a final image where everything is visible. a final image where everything is visible.
In fractal flames, this "tone map" is accomplished by scaling brightness according to the _logarithm_ In fractal flames, this "tone map" is accomplished by scaling brightness according to the _logarithm_
of how many times we encounter a pixel. This way, "dark spots" (pixels the chaos game visits infrequently) of how many times we encounter a pixel. This way, "cold spots" (pixels the chaos game visits infrequently)
will still be visible, and "bright spots" (pixels the chaos game visits frequently) won't wash out. will still be visible, and "hot spots" (pixels the chaos game visits frequently) won't wash out.
<details> <details>
<summary>Log-scale vibrancy is also why fractal flames appear to be 3D...</summary> <summary>Log-scale vibrancy also explains fractal flames appear to be 3D...</summary>
As explained in the Fractal Flame paper: As mentioned in the paper:
> Where one branch of the fractal crosses another, one may appear to occlude the other > Where one branch of the fractal crosses another, one may appear to occlude the other
> if their densities are different enough because the lesser density is inconsequential in sum. > if their densities are different enough because the lesser density is inconsequential in sum.
@ -89,15 +90,17 @@ import {paintLogarithmic} from './paintLogarithmic'
## Color ## Color
Finally, we'll introduce the last innovation of the fractal flame algorithm: color. Finally, we'll introduce the last innovation of the fractal flame algorithm: color.
By including a color coordinate ($c$) in the chaos game, we can illustrate the transforms By including a third coordinate ($c$) in the chaos game, we can illustrate the transforms
responsible for each part of the image. responsible for the image.
### Color coordinate ### Color coordinate
Color in a fractal flame uses a range of $[0, 1]$. This is important for two reasons: Color in a fractal flame is continuous on the range $[0, 1]$. This is important for two reasons:
- It helps blend colors together in the final image - It helps blend colors together in the final image. Slight changes in the color value lead to
- It allows us to swap in new color palettes easily slight changes in the actual color
- It allows us to swap in new color palettes easily. We're free to choose what actual colors
each color value represents
We'll give each transform a color value ($c_i$) in the $[0, 1]$ range. We'll give each transform a color value ($c_i$) in the $[0, 1]$ range.
Then, at each step in the chaos game, we'll set the current color Then, at each step in the chaos game, we'll set the current color
@ -139,14 +142,14 @@ import mixColorSource from "!!raw-loader!./mixColor"
Color speed values work just like transform weights. A value of 1 Color speed values work just like transform weights. A value of 1
means we take the transform color and ignore the previous color state. means we take the transform color and ignore the previous color state.
Similarly, a value of 0 means we keep the current color state and ignore the A value of 0 means we keep the current color state and ignore the
transform color. transform color.
### Palette ### Palette
Now, we need to map the color coordinate to a pixel color. Fractal flames typically use Now, we need to map the color coordinate to a pixel color. Fractal flames typically use
256 colors (each color has 3 values - red, green, blue) to define a palette. 256 colors (each color has 3 values - red, green, blue) to define a palette.
Then, the color coordinate becomes an index into the palette. The color coordinate then becomes an index into the palette.
There's one small complication: the color coordinate is continuous, but the palette There's one small complication: the color coordinate is continuous, but the palette
uses discrete colors. How do we handle situations where the color coordinate is uses discrete colors. How do we handle situations where the color coordinate is
@ -163,11 +166,11 @@ import colorFromPaletteSource from "!!raw-loader!./colorFromPalette";
<summary>As an alternative...</summary> <summary>As an alternative...</summary>
...you could also interpolate between colors in the palette. ...you could also interpolate between colors in the palette.
For example: [`flam3` code](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486) For example, `flam3` uses [linear interpolation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486)
</details> </details>
In the diagram below, each color in our palette is plotted on a small vertical strip. In the diagram below, each color in our palette is plotted on a small vertical strip.
Putting the strips side by side shows the palette used by our reference image: Putting the strips side by side shows the full palette used by the reference parameters:
import * as params from "../src/params" import * as params from "../src/params"
import {PaletteBar} from "./FlameColor" import {PaletteBar} from "./FlameColor"
@ -177,14 +180,15 @@ import {PaletteBar} from "./FlameColor"
### Plotting ### Plotting
We're now ready to plot our $(x_f,y_f,c_f)$ coordinates. After translating from color coordinate ($c_f$) We're now ready to plot our $(x_f,y_f,c_f)$ coordinates. After translating from color coordinate ($c_f$)
to RGB value, add that value to the image histogram: to RGB value, add that to the image histogram:
import chaosGameColorSource from "!!raw-loader!./chaosGameColor" import chaosGameColorSource from "!!raw-loader!./chaosGameColor"
<CodeBlock language="typescript">{chaosGameColorSource}</CodeBlock> <CodeBlock language="typescript">{chaosGameColorSource}</CodeBlock>
Finally, painting the image. With tone mapping, logarithms scale the image brightness to match Finally, painting the image. With tone mapping, logarithms scale the image brightness to match
how it is perceived. When using color, we scale each color channel by the alpha channel: how it is perceived. With color, we use a similar method, but scale each color channel
by the alpha channel:
import paintColorSource from "!!raw-loader!./paintColor" import paintColorSource from "!!raw-loader!./paintColor"
@ -206,10 +210,10 @@ Next, introducing a third coordinate to the chaos game makes color images possib
the third major innovation of the fractal flame algorithm. Using a continuous the third major innovation of the fractal flame algorithm. Using a continuous
color scale and color palette adds a splash of color to our transforms. color scale and color palette adds a splash of color to our transforms.
The Fractal Flame Algorithm paper goes on to describe more techniques The Fractal Flame Algorithm paper does go on to describe more techniques
not covered here. Image quality can be improved with density estimation not covered here. For example, Image quality can be improved with density estimation
and filtering. New parameters can be generated by "mutating" existing and filtering. New parameters can be generated by "mutating" existing
fractal flames. Fractal flames can even be animated to produce videos! fractal flames. And fractal flames can even be animated to produce videos!
That said, I think this is a good place to wrap up. We were able to go from That said, I think this is a good place to wrap up. We were able to go from
an introduction to the mathematics of fractal systems all the way to an introduction to the mathematics of fractal systems all the way to

View File

@ -8,7 +8,8 @@ type PainterProps = {
} }
export const PainterContext = createContext<PainterProps>(null) export const PainterContext = createContext<PainterProps>(null)
const downloadImage = (e: MouseEvent) => { const downloadImage = (name: string) =>
(e: MouseEvent) => {
const link = document.createElement("a"); const link = document.createElement("a");
link.download = "flame.png"; link.download = "flame.png";
link.href = (e.target as HTMLCanvasElement).toDataURL("image/png"); link.href = (e.target as HTMLCanvasElement).toDataURL("image/png");
@ -16,10 +17,11 @@ const downloadImage = (e: MouseEvent) => {
} }
type CanvasProps = { type CanvasProps = {
name: string;
style?: any; style?: any;
children?: React.ReactElement children?: React.ReactElement
} }
export const Canvas: React.FC<CanvasProps> = ({style, children}) => { export const Canvas: React.FC<CanvasProps> = ({name, style, children}) => {
const sizingRef = useRef<HTMLDivElement>(null); const sizingRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0); const [height, setHeight] = useState(0);
@ -93,7 +95,7 @@ export const Canvas: React.FC<CanvasProps> = ({style, children}) => {
<> <>
<center> <center>
<div ref={sizingRef} style={style}> <div ref={sizingRef} style={style}>
{width > 0 ? <canvas {...canvasProps} onDoubleClick={downloadImage}/> : null} {width > 0 ? <canvas {...canvasProps} onDoubleClick={downloadImage(name)}/> : null}
</div> </div>
</center> </center>
<PainterContext.Provider value={{width, height, setPainter}}> <PainterContext.Provider value={{width, height, setPainter}}>
@ -103,6 +105,6 @@ export const Canvas: React.FC<CanvasProps> = ({style, children}) => {
) )
} }
export const SquareCanvas: React.FC<CanvasProps> = ({style, children}) => { export const SquareCanvas: React.FC<CanvasProps> = ({name, style, children}) => {
return <center><Canvas style={{width: '75%', aspectRatio: '1/1', ...style}} children={children}/></center> return <center><Canvas name={name} style={{width: '75%', aspectRatio: '1/1', ...style}} children={children}/></center>
} }

View File

@ -1,7 +1,7 @@
.inputGroup { .inputGroup {
padding: .2em; padding: .2em;
margin-top: .2em; margin-top: .5em;
margin-bottom: .2em; margin-bottom: .5em;
border: 1px solid; border: 1px solid;
border-radius: var(--ifm-global-radius); border-radius: var(--ifm-global-radius);
border-color: var(--ifm-color-emphasis-500); border-color: var(--ifm-color-emphasis-500);
@ -25,6 +25,10 @@
.inputElement > input[type=range] { .inputElement > input[type=range] {
width: 100%; width: 100%;
height: 4px;
appearance: none;
background: var(--ifm-color-emphasis-400);
border-radius: var(--ifm-global-radius);
} }
.inputReset { .inputReset {