mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
More writing for the math and browser APIs
This commit is contained in:
parent
192286a86a
commit
538cc2eb47
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
export function camera(
|
||||||
|
size: number,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
): [number, number] {
|
||||||
|
return [
|
||||||
|
Math.floor(x * size),
|
||||||
|
Math.floor(y * size)
|
||||||
|
];
|
||||||
|
}
|
@ -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}/>)
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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 =>
|
||||||
|
@ -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)
|
|
||||||
]
|
|
||||||
}
|
|
@ -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)
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user