More writing for the main posts

This commit is contained in:
Bradlee Speice 2024-12-09 22:18:13 -05:00
parent b608a25146
commit 0983558659
16 changed files with 155 additions and 125 deletions

View File

@ -3,7 +3,8 @@ import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { plot } from "./plot"
import {Transform} from "../src/transform";
const iterations = 50_000;
const quality = 0.5;
const step = 1000;
// hidden-end
export type Props = {
@ -20,8 +21,7 @@ export function* chaosGameWeighted(
randomBiUnit()
];
// TODO: Explain quality
const iterations = width * height * 0.5;
const iterations = width * height * quality;
for (let c = 0; c < iterations; c++) {
// highlight-start
const [_, xform] = randomChoice(transforms);

View File

@ -24,7 +24,7 @@ import banner from '../banner.png'
I don't remember exactly when I first learned about fractal flames, but I do remember becoming 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.
The original [Fractal Flame](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.
But the desire to understand it stuck around. Now, with a graduate degree under my belt, maybe I can make some progress.
@ -35,7 +35,13 @@ can understand without too much prior knowledge.
## Iterated function systems
As mentioned above, fractal flames are a type of "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system),"
:::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),"
or IFS. Their mathematical foundations come from a paper written by [John E. Hutchinson](https://maths-people.anu.edu.au/~john/Assets/Research%20Papers/fractals_self-similarity.pdf),
but reading that paper isn't critical for our purposes. Instead, we'll focus on building a practical understanding
of how they work. The formula for an IFS is short, but will take some time to unpack:
@ -65,7 +71,11 @@ export const simpleData = [
</VictoryChart>
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.
TODO: Explain characteristics of the solution - fixed set
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.
And if we choose different functions to start with, our solution set changes, and we'll end up
with a new picture.
@ -73,10 +83,6 @@ with a new picture.
However, it's not clear which points belong in the solution just by staring at the functions.
We'll need a computer to figure it out.
TODO: Other topics worth covering in this section? Maybe in a `details` block?:
- Fixed sets: https://en.wiktionary.org/wiki/fixed_set
- Compact sets
### Transformation functions
Second, $F_i(S)$. At their most basic, each $F_i$ is a function that takes in a 2-dimensional point and transforms

View File

@ -37,8 +37,6 @@ export default function FlameBlend() {
const [xform3Variations, setXform3Variations] = useState(xform3VariationsDefault)
const resetXform3Variations = () => setXform3Variations(xform3VariationsDefault);
// Cheating a bit here; for purposes of code re-use, use the post- and final-transform-enabled chaos game,
// and swap in identity components for each
const identityXform: Transform = (x, y) => [x, y];
useEffect(() => {

View File

@ -17,7 +17,7 @@ export default function FlamePost() {
const resetXform2CoefsPost = () => setXform2CoefsPost(params.xform2CoefsPost);
const [xform3CoefsPost, setXform3CoefsPost] = useState<Coefs>(params.xform3CoefsPost);
const resetXform3CoefsPost = () => setXform1CoefsPost(params.xform3CoefsPost);
const resetXform3CoefsPost = () => setXform3CoefsPost(params.xform3CoefsPost);
const identityXform: Transform = (x, y) => [x, y];

View File

@ -25,7 +25,9 @@ export function* chaosGameFinal({width, height, transforms, final}: Props) {
// highlight-end
if (i > 20)
// highlight-start
plot(finalX, finalY, image);
// highlight-end
if (i % step === 0)
yield image;

View File

@ -7,60 +7,55 @@ tags: []
---
Now that we have a basic chaos game in place, it's time to spice things up. Transforms and variations create the
interesting patterns that fractal flames are known for.
shapes and patterns that fractal flames are known for.
<!-- truncate -->
This blog post uses a set of reference parameters ([available here](../params.flame)) to demonstrate a practical
implementation of the fractal flame algorithm. If you're interested in tweaking the parameters, or generating
your own art, [Apophysis](https://sourceforge.net/projects/apophysis/) is a good introductory tool.
:::note
TODO: Include the reference image here
This post uses a set of [reference parameters](../params.flame) to demonstrate a working
implementation of the fractal flame algorithm. If you're interested in tweaking the parameters,
or generating your own art, [Apophysis](https://sourceforge.net/projects/apophysis/)
can load that file and gives full control over the image.
:::
## Transforms and variations
:::note
This post covers section 3 of the Fractal Flame Algorithm paper
:::
import CodeBlock from '@theme/CodeBlock'
We previously introduced "transforms" as the "functions" of an "iterated function system." Their general format is:
We previously introduced transforms as the "functions" of an "iterated function system," and showed how
playing the chaos game leads to an image of Sierpinski's Gasket. Even though we used simple functions,
the image it generates is exciting. But it's still not nearly as exciting as the images the Fractal Flame
algorithm is known for.
This leads us to the first big innovation of the Fractal Flame algorithm: using non-linear functions
for the transforms. These functions are known as "variations":
$$
F_i(x,y) = (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) = V_j(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i)
$$
import coefsSrc from '!!raw-loader!../src/coefs'
import variationSource from '!!raw-loader!../src/variation'
<CodeBlock language={'typescript'}>{coefsSrc}</CodeBlock>
<CodeBlock language="typescript">{variationSource}</CodeBlock>
We also introduced the Sierpinski Gasket functions ($F_0$, $F_1$, and $F_2$), demonstrating how they are related to
the general format. For example:
Variations, labeled $V_j$ above, are functions just like transforms (we use $j$ to indicate a specific variation).
They take an input point $(x,y)$, and give an output point. However, the sky is the limit for what variation functions do in between
input to output. The Fractal Flame paper lists 49 different variation functions,
and the official `flam3` implementation supports [98 different functions](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c).
$$
\begin{align*}
F_0(x,y) &= \left({x \over 2}, {y \over 2}\right) \\
&= (a_0 \cdot x + b_0 \cdot y + c_0, d_0 \cdot x + e_0 \cdot y + f_0) \\
& a_0 = 0.5 \hspace{0.2cm} b_0 = 0 \hspace{0.2cm} c_0 = 0 \\
& d_0 = 0 \hspace{0.2cm} e_0 = 0.5 \hspace{0.2cm} f_0 = 0
\end{align*}
$$
TODO: Explain the applyCoefs function
However, these transforms are pretty boring. We can build more exciting images by using additional functions
within the transform. These "sub-functions" are called "variations":
$$
F_i(x, y) = 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)
$$
The fractal flame paper lists 49 variation functions ($V_j$ above), but the sky's the limit here.
For example, the official `flam3` implementation supports
[98 variations](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c).
Our reference image will focus on just four variations:
To draw our reference image, we'll focus on four variations:
### Linear (variation 0)
This variation returns the $x$ and $y$ coordinates as-is:
This variation is dead simple: just return the $x$ and $y$ coordinates as-is.
$$
V_0(x,y) = (x,y)
@ -70,11 +65,17 @@ import linearSrc from '!!raw-loader!../src/linear'
<CodeBlock language={'typescript'}>{linearSrc}</CodeBlock>
:::tip
In a way, we've already been using this variation! The functions that define Sierpinski's Gasket
apply the affine coefficients to the input point, and use that as the output point.
:::
### Julia (variation 13)
This variation still uses just the $x$ and $y$ coordinates, but does crazy things with them:
<small>TODO: Is this related to the Julia set?</small>
This variation is a good example of the non-linear functions the Fractal Flame Algorithm introduces.
It still receives an input point $(x, y)$, but does some crazy things with it:
$$
\begin{align*}
@ -97,8 +98,8 @@ import juliaSrc from '!!raw-loader!../src/julia'
### Popcorn (variation 17)
This is known as a "dependent variation" because it depends on knowing the transform coefficients
(specifically, $c$ and $f$):
Some variations rely on knowing the transform's affine coefficients; these are known as "dependent variations."
For the popcorn variation, we use the $c$ and $f$ coefficients:
$$
V_{17}(x,y) = (x + c \cdot \text{sin}(\text{tan }3y), y + f \cdot \text{sin}(\text{tan }3x))
@ -110,7 +111,8 @@ import popcornSrc from '!!raw-loader!../src/popcorn'
### PDJ (variation 24)
This is known as a "parametric" variation because it has additional parameters given to it:
Some variations have extra parameters that the designer can choose; these are known as "parametric variations."
For the PDJ variation, there are four extra parameters we can choose:
$$
p_1 = \text{pdj.a} \hspace{0.2cm} p_2 = \text{pdj.b} \hspace{0.2cm} p_3 = \text{pdj.c} \hspace{0.2cm} p_4 = \text{pdj.d} \\
@ -123,9 +125,9 @@ import pdjSrc from '!!raw-loader!../src/pdj'
## Blending
Now, one variation is fun, but we can also combine variations in a single transform by "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.
We'll also give each variation a weight ($v_{ij}$) to control how much it contributes to the transform:
We'll also give each variation a weight (called $v_{ij}$) that changes how much it contributes to the transform:
$$
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)
@ -137,10 +139,15 @@ import blendSource from "!!raw-loader!../src/blend";
<CodeBlock language={'typescript'}>{blendSource}</CodeBlock>
And with that in place, we have enough to render a first full fractal flame.
The sliders below change the variation weights for each transform (the $v_{ij}$ parameters);
try changing them around to see which parts of the image are controlled by
each transform.
With that in place, we have enough to render a first full fractal flame. We'll use the same
chaos game as before, but use our new transforms and variations to produce a dramatically different image:
:::tip
This image is interactive! The sliders change the variation weights ($v_{ij}$ parameters)
so you can design your own image.
Try using the sliders to find which parts of the image each transform controls.
:::
import {SquareCanvas} from "../src/Canvas";
import FlameBlend from "./FlameBlend";
@ -149,20 +156,40 @@ import FlameBlend from "./FlameBlend";
## Post transforms
After variation blending, we apply a second set of transform coordinates.
Post transforms introduce a second affine transform, this time _after_ variation blending.
We'll use introduce some new variables, but the post transform function should look familiar by now:
The fractal flame below starts with the same initial transforms/variations as the previous fractal flame,
$$
\begin{align*}
P_i(x, y) &= (\alpha_i x + \beta_i y + \gamma_i, \delta_i x + \epsilon_i y + \zeta_i) \\
F_i(x, y) &= P_i\left(\sum_{j} v_{ij} V_j(x, y)\right)
\end{align*}
$$
import postSource from '!!raw-loader!./post'
<CodeBlock language="typescript">{postSource}</CodeBlock>
The image below starts with the same initial transforms/variations as the previous fractal flame,
but allows modifying the post-transform coefficients.
$$
P_i(x, y) = (\alpha_i x + \beta_i y + \gamma_i, \delta_i x + \epsilon_i y + \zeta_i)
$$
<details>
<summary>If you want a challenge...</summary>
Challenge 1: What post-transform coefficients will give us the previous image?
Challenge 2: What post-transform coefficients will give us a _mirrored_ image?
</details>
import FlamePost from "./FlamePost";
<SquareCanvas><FlamePost/></SquareCanvas>
## Final transform
## Final transforms
import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal"
<CodeBlock language="typescript">{chaosGameFinalSource}</CodeBlock>
import FlameFinal from "./FlameFinal";

View File

@ -4,4 +4,4 @@ import {Transform} from "../src/transform";
import {applyCoefs} from "../src/coefs";
// hidden-end
export const transformPost = (transform: Transform, coefs: Coefs): Transform =>
(x, y): [number, number] => applyCoefs(...transform(x, y), coefs)
(x, y) => applyCoefs(...transform(x, y), coefs)

View File

@ -44,20 +44,6 @@ const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
)
}
const colorSwatchPainter = (palette: number[], color: number) =>
(width: number, height: number) => {
const [r, g, b] = colorFromPalette(palette, color);
const image = new ImageData(width, height);
for (let i = 0; i < image.data.length; i += 4) {
image.data[i] = r * 0xff;
image.data[i + 1] = g * 0xff;
image.data[i + 2] = b * 0xff;
image.data[i + 3] = 0xff;
}
return image;
}
type ColorEditorProps = {
title: string;
palette: number[];

View File

@ -1,9 +1,13 @@
import {ChaosGameFinalProps} from "../2-transforms/chaosGameFinal";
// hidden-start
import {Props as ChaosGameFinalProps} from "../2-transforms/chaosGameFinal";
import {randomBiUnit} from "../src/randomBiUnit";
import {randomChoice} from "../src/randomChoice";
import {camera, histIndex} from "../src/camera";
import {colorFromPalette, paintColor} from "./paintColor";
const quality = 15;
const step = 100_000;
// hidden-end
export type TransformColor = {
color: number;
colorSpeed: number;
@ -13,23 +17,21 @@ function mixColor(color1: number, color2: number, colorSpeed: number) {
return color1 * (1 - colorSpeed) + color2 * colorSpeed;
}
export type ChaosGameColorProps = ChaosGameFinalProps & {
type Props = ChaosGameFinalProps & {
palette: number[];
colors: TransformColor[];
finalColor: TransformColor;
}
export function* chaosGameColor({width, height, transforms, final, palette, colors, finalColor, quality, step}: ChaosGameColorProps) {
let iterations = (quality ?? 1) * width * height;
step = step ?? 10_000;
export function* chaosGameColor({width, height, transforms, final, palette, colors, finalColor}: Props) {
let currentColor = Math.random();
const red = Array(width * height).fill(0);
const green = Array(width * height).fill(0);
const blue = Array(width * height).fill(0);
const alpha = Array(width * height).fill(0);
const red = Array<number>(width * height).fill(0);
const green = Array<number>(width * height).fill(0);
const blue = Array<number>(width * height).fill(0);
const alpha = Array<number>(width * height).fill(0);
let [x, y] = [randomBiUnit(), randomBiUnit()];
const iterations = width * height * quality;
for (let i = 0; i < iterations; i++) {
const [transformIndex, transform] = randomChoice(transforms);
[x, y] = transform(x, y);

View File

@ -1,17 +1,19 @@
// hidden-start
import {randomBiUnit} from "../src/randomBiUnit";
import {randomChoice} from "../src/randomChoice";
import {ChaosGameFinalProps} from "../2-transforms/chaosGameFinal";
import {Props as ChaosGameFinalProps} from "../2-transforms/chaosGameFinal";
import {camera, histIndex} from "../src/camera";
// hidden-end
export type ChaosGameHistogramProps = ChaosGameFinalProps & {
paint: (width: number, histogram: Uint32Array) => ImageData;
}
export function* chaosGameHistogram({width, height, transforms, final, quality, step, paint}: ChaosGameHistogramProps) {
let iterations = (quality ?? 1) * width * height;
step = step ?? 10_000;
const histogram = new Uint32Array(width * height);
const quality = 10;
const step = 100_000;
// hidden-end
export type Props = ChaosGameFinalProps & {
paint: (width: number, height: number, histogram: number[]) => ImageData;
}
export function* chaosGameHistogram({width, height, transforms, final, paint}: Props) {
let iterations = quality * width * height;
const histogram = Array<number>(width * height).fill(0);
let [x, y] = [randomBiUnit(), randomBiUnit()];
@ -22,13 +24,18 @@ export function* chaosGameHistogram({width, height, transforms, final, quality,
if (i > 20) {
const [pixelX, pixelY] = camera(finalX, finalY, width);
const pixelIndex = histIndex(pixelX, pixelY, width, 1);
histogram[pixelIndex] += 1;
const hIndex = histIndex(pixelX, pixelY, width, 1);
if (hIndex < 0 || hIndex >= histogram.length) {
continue;
}
histogram[hIndex] += 1;
}
if (i % step === 0)
yield paint(width, histogram);
yield paint(width, height, histogram);
}
yield paint(width, histogram);
yield paint(width, height, histogram);
}

View File

@ -18,6 +18,10 @@ Can we do something more intelligent with that information?
## Image histograms
:::note
This post covers sections 4 and 5 of the Fractal Flame Algorithm paper
:::
To start with, it's worth demonstrating how much work is actually "wasted."
We'll render the reference image again, but this time, set each pixel's transparency
based on how many times we encounter it in the chaos game:

View File

@ -1,17 +1,17 @@
export function paintLinear(width: number, histogram: Uint32Array): ImageData {
const image = new ImageData(width, histogram.length / width);
export function paintLinear(width: number, height: number, histogram: number[]): ImageData {
const image = new ImageData(width, height);
let countMax = 0;
let valueMax = 0;
for (let value of histogram) {
countMax = Math.max(countMax, value);
valueMax = Math.max(valueMax, value);
}
for (let i = 0; i < histogram.length; i++) {
const pixelIndex = i * 4;
image.data[pixelIndex] = 0; // red
image.data[pixelIndex + 1] = 0; // green
image.data[pixelIndex + 2] = 0; // blue
image.data[pixelIndex + 3] = Number(histogram[i]) / countMax * 0xff;
image.data[pixelIndex] = 0;
image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0;
image.data[pixelIndex + 3] = histogram[i] / valueMax * 0xff;
}
return image;

View File

@ -1,5 +1,5 @@
export function paintLogarithmic(width: number, histogram: Uint32Array): ImageData {
const image = new ImageData(width, histogram.length / width);
export function paintLogarithmic(width: number, height: number, histogram: number[]): ImageData {
const image = new ImageData(width, height);
const histogramLog = new Array<number>();
histogram.forEach(value => histogramLog.push(Math.log(value)));

View File

@ -5,14 +5,15 @@ export type VariationBlend = [number, Variation][];
export function blend(
x: number,
y: number,
variations: VariationBlend): [number, number] {
let [finalX, finalY] = [0, 0];
variations: VariationBlend
): [number, number] {
let [outX, outY] = [0, 0];
for (const [weight, variation] of variations) {
const [varX, varY] = variation(x, y);
finalX += weight * varX;
finalY += weight * varY;
outX += weight * varX;
outY += weight * varY;
}
return [finalX, finalY];
return [outX, outY];
}

View File

@ -2,14 +2,11 @@ import { camera, histIndex } from "./camera"
export function plotBinary(x: number, y: number, image: ImageData) {
const [pixelX, pixelY] = camera(x, y, image.width);
if (
pixelX < 0 || pixelX >= image.width || pixelY < 0 || pixelY > image.height
) {
const pixelIndex = histIndex(pixelX, pixelY, image.width, 4);
if (pixelIndex < 0 || pixelIndex > image.data.length) {
return;
}
const pixelIndex = histIndex(pixelX, pixelY, image.width, 4);
image.data[pixelIndex] = 0;
image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0;

View File

@ -8,7 +8,7 @@
--ifm-pre-padding: .6rem;
/* More readable code highlight background */
--docusaurus-highlighted-code-line-bg: var(--ifm-color-emphasis-300);
--docusaurus-highlighted-code-line-bg: var(--ifm-color-emphasis-200);
/*--ifm-code-font-size: 85%;*/
}