More writing for the math and browser APIs

This commit is contained in:
Bradlee Speice 2024-12-11 19:56:39 -05:00
parent 192286a86a
commit 538cc2eb47
9 changed files with 138 additions and 102 deletions

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%'}} value={weight} <input type={'range'} min={0} max={1} step={.01} style={{width: '100%', background: 'transparent'}} value={weight}
onInput={e => setWeight(Number(e.currentTarget.value))}/> onInput={e => setWeight(Number(e.currentTarget.value))}/>
</div> </div>
</> </>

View File

@ -0,0 +1,10 @@
export function camera(
size: number,
x: number,
y: number
): [number, number] {
return [
Math.floor(x * size),
Math.floor(y * size)
];
}

View File

@ -30,5 +30,4 @@ function* chaosGame({width, height}) {
yield img; yield img;
} }
// Wiring so the code above displays properly
render(<Gasket f={chaosGame}/>) render(<Gasket f={chaosGame}/>)

View File

@ -36,9 +36,7 @@ can understand without too much prior knowledge.
## Iterated function systems ## Iterated function systems
:::note :::note
This post covers section 2 of the Fractal Flame Algorithm paper 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),"
@ -50,15 +48,18 @@ $$
S = \bigcup_{i=0}^{n-1} F_i(S) S = \bigcup_{i=0}^{n-1} F_i(S)
$$ $$
### Fixed set TODO: I'm not sure what the intuitive explanation here is. Is the idea that the solution is all points
produced by applying each function to all points in the solution? And the purpose of the chaos game is
that if we find one point in the solution set, we can effectively discover all the other points?
First, $S$. $S$ is the set of points in two dimensions (in math terms, $S \in \mathbb{R}^2$) that represent ### Solution set
a "solution" of some kind. Our goal is to find all points in the set $S$, plot them, and display that image.
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.
Our goal is to find all points in $S$, plot them, and display that image.
For example, if we say $S = \{(0,0), (1, 1), (2, 2)\}$, there are three points to plot: For example, if we say $S = \{(0,0), (1, 1), (2, 2)\}$, there are three points to plot:
<!-- TODO: What is a stationary point? How does it relate to the chaos game? Why does the chaos game work? -->
import {VictoryChart, VictoryTheme, VictoryScatter, VictoryLegend} from "victory"; import {VictoryChart, VictoryTheme, VictoryScatter, VictoryLegend} from "victory";
export const simpleData = [ export const simpleData = [
{x: 0, y: 0}, {x: 0, y: 0},
@ -71,35 +72,52 @@ export const simpleData = [
</VictoryChart> </VictoryChart>
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. we use functions to describe which points are part of the solution. This means there are an infinite
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. 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 And if we change the functions, our solution changes, and we'll get a new picture.
with a new picture.
However, it's not clear which points belong in the solution just by staring at the functions. ### Transform functions
We'll need a computer to figure it out.
### Transformation functions 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
(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
known as an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)."
Second, $F_i(S)$. At their most basic, each $F_i$ is a function that takes in a 2-dimensional point and transforms The general form of an affine transformation is:
it into a new 2-dimensional point: $F_i \in \mathbb{R}^2 \rightarrow \mathbb{R}^2$. It's worth discussing
these functions, but not critical, so **this section is optional**.
In mathematical terms, each $F_i$ is a special kind of function called an [affine transformation](https://en.wikipedia.org/wiki/Affine_transformation).
We can think of them like mapping from one coordinate system to another. For example, we can define a coordinate system
where everything is shifted over:
$$ $$
F_{shift}(x, y) = (x + 1, y) F_i(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i)
$$ $$
That is, for an input point $(x, y)$, the output point will be $(x + 1, y)$: import transformSource from "!!raw-loader!../src/transform"
export const shiftData = simpleData.map(({x, y}) => { return {x: x + 1, y} }) <CodeBlock language="typescript">{transformSource}</CodeBlock>
The parameters ($a_i$, $b_i$, etc.) are values we get to choose.
For example, we can represent a "shift" function like this:
$$
\begin{align*}
a &= 1 \\
b &= 0 \\
c &= 0.5 \\
d &= 0 \\
e &= 1 \\
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) &= (x + 0.5, y + 0.5)
\end{align*}
$$
Applying this function to our original points will give us a new set of points:
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)))
<VictoryChart theme={VictoryTheme.clean}> <VictoryChart theme={VictoryTheme.clean}>
<VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/> <VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/>
@ -115,32 +133,37 @@ export const shiftData = simpleData.map(({x, y}) => { return {x: x + 1, y} })
/> />
</VictoryChart> </VictoryChart>
This is a simple example designed to illustrate the principle. In general, $F_i$ functions have the form: Fractal flames use more complex functions, but they all start with this structure.
<details>
<summary>If you're interested in more math...</summary>
TODO: Contractive functions, attractors, etc.?
</details>
### Fixed set
With those definitions in place, we can try stating the original problem in
a more natural way:
$$ $$
F_i(x,y) = (a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i) S = \bigcup_{i=0}^{n-1} F_i(S)
$$ $$
The parameters ($a_i$, $b_i$, etc.) are values we get to choose. In the example above, we can represent our shift > The solution, $S$, is the union of all sets produced by applying each function, $F_i$,
function using these parameters: > to points in the solution.
$$ There's just one small problem: to solve the equation, we must already know what the solution is?
a_i = 1 \hspace{0.5cm} b_i = 0 \hspace{0.5cm} c_i = 1 \\
d_i = 0 \hspace{0.5cm} e_i = 1 \hspace{0.5cm} f_i = 0 \\
$$
$$ TODO: Phrase it another way?
\begin{align*} A point is in the solution if it can be reached by applying
F_{shift}(x,y) &= (1 \cdot x + 0 \cdot y + 1, 0 \cdot x + 1 \cdot y + 0) \\ one of the functions to another point in the solution?
F_{shift}(x,y) &= (x + 1, y) Is that the definition of a fixed set?
\end{align*}
$$
Fractal flames use more complex functions to produce a wide variety of images, but all follow this same format.
## Sierpinski's gasket ## Sierpinski's gasket
Using these definitions, we can build the first image. The paper defines a function system for us: With the math out of the way, we're ready to build our first IFS.
The Fractal Flame paper provides us three functions we can use for our system:
$$ $$
F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\ F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\
@ -169,7 +192,7 @@ $$
Let's turn this into code, one piece at a time. Let's turn this into code, one piece at a time.
First, the "bi-unit square" is the range $[-1, 1]$. We can pick a random point like this: First, the "bi-unit square" is the range $[-1, 1]$. We can :
import biunitSource from '!!raw-loader!../src/randomBiUnit' import biunitSource from '!!raw-loader!../src/randomBiUnit'
@ -181,14 +204,36 @@ import randintSource from '!!raw-loader!../src/randomInteger'
<CodeBlock language="typescript">{randintSource}</CodeBlock> <CodeBlock language="typescript">{randintSource}</CodeBlock>
Finally, implementing the `plot` function. Web browsers have a [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) ### Plotting
we can use for 2D graphics. In our case, the plot function will take an $(x,y)$ coordinate and plot it by
coloring the corresponding pixel in an [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData): Finally, implementing the `plot` function. This blog series
is designed to be interactive, so everything shows
real-time directly in the browser. As an alternative,
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).
This allows us to manipulate individual pixels an image,
and display it on screen.
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,
and we'll focus on the range $[0, 1]$ for both $x$ and $y$:
import cameraSource from "!!raw-loader!./cameraGasket"
<CodeBlock language="typescript">{cameraSource}</CodeBlock>
Next, we'll use 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.
To plot our image, we set that pixel to be black:
import plotSource from '!!raw-loader!./plot' import plotSource from '!!raw-loader!./plot'
<CodeBlock language="typescript">{plotSource}</CodeBlock> <CodeBlock language="typescript">{plotSource}</CodeBlock>
Putting it all together, we have our first image:
import Playground from '@theme/Playground' import Playground from '@theme/Playground'
import Scope from './scope' import Scope from './scope'
@ -199,8 +244,8 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
<hr/> <hr/>
<small> <small>
Note: The image here is slightly different than the fractal flame paper; I think the paper has an error, Note: The image here is slightly different than the one in the paper.
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). 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).
</small> </small>
## Weights ## Weights

View File

@ -1,10 +1,6 @@
/** // hidden-start
* ImageData is an array that contains import {camera} from "./cameraGasket"
* four elements per pixel (one for each // hidden-end
* red, green, blue, and alpha value).
* This maps from pixel coordinates
* to the array index
*/
function imageIndex( function imageIndex(
width: number, width: number,
x: number, x: number,
@ -18,17 +14,9 @@ export function plot(
y: number, y: number,
img: ImageData img: ImageData
) { ) {
// Translate (x,y) coordinates let [pixelX, pixelY] = camera(img.width, x, y);
// to pixel coordinates.
// Also known as a "camera" function.
//
// The display range is:
// x=[0, 1]
// y=[0, 1]
let pixelX = Math.floor(x * img.width);
let pixelY = Math.floor(y * img.height);
const index = imageIndex( const i = imageIndex(
img.width, img.width,
pixelX, pixelX,
pixelY pixelY
@ -36,17 +24,18 @@ export function plot(
// Skip pixels outside the display range // Skip pixels outside the display range
if ( if (
index < 0 || i < 0 ||
index > img.data.length i > img.data.length
) { ) {
return; return;
} }
// Set the pixel to black by writing 0 // Set the pixel to black by writing 0
// to the first three elements, // to the first three elements at the index
// and 255 to the last element // (red, green, and blue, respectively),
img.data[index] = 0; // and 255 to the last element (alpha)
img.data[index + 1] = 0; img.data[i] = 0;
img.data[index + 2] = 0; img.data[i + 1] = 0;
img.data[index + 3] = 0xff; img.data[i + 2] = 0;
img.data[i + 3] = 0xff;
} }

View File

@ -12,22 +12,15 @@ shapes and patterns that fractal flames are known for.
<!-- truncate --> <!-- truncate -->
:::note :::note
This post uses a set of [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/)
can load that file and you can try tweaking things yourself!
This post uses a set of [reference parameters](../params.flame) to demonstrate a working This post covers section 3 of the Fractal Flame Algorithm paper
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 ## Transforms and variations
:::note
This post covers section 3 of the Fractal Flame Algorithm paper
:::
import CodeBlock from '@theme/CodeBlock' import CodeBlock from '@theme/CodeBlock'
We previously introduced transforms as the "functions" of an "iterated function system," and showed how We previously introduced transforms as the "functions" of an "iterated function system," and showed how

View File

@ -1,5 +1,4 @@
import {Transform} from "./transform"; import {Transform, Coefs, applyCoefs} from "./transform";
import {applyCoefs, Coefs} from "./coefs";
import {blend, VariationBlend} from "./blend"; import {blend, VariationBlend} from "./blend";
export const applyTransform = (coefs: Coefs, variations: VariationBlend): Transform => export const applyTransform = (coefs: Coefs, variations: VariationBlend): Transform =>

View File

@ -1,11 +0,0 @@
export interface Coefs {
a: number, b: number, c: number,
d: number, e: number, f: number
}
export function applyCoefs(x: number, y: number, coefs: Coefs): [number, number] {
return [
(x * coefs.a + y * coefs.b + coefs.c),
(x * coefs.d + y * coefs.e + coefs.f)
]
}

View File

@ -1 +1,13 @@
export type Transform = (x: number, y: number) => [number, number]; export type Transform = (x: number, y: number) => [number, number];
export interface Coefs {
a: number, b: number, c: number,
d: number, e: number, f: number
}
export function applyCoefs(x: number, y: number, coefs: Coefs): [number, number] {
return [
(x * coefs.a + y * coefs.b + coefs.c),
(x * coefs.d + y * coefs.e + coefs.f)
]
}