From 538cc2eb47bc5a206e79013c733ee82a42a682ba Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Wed, 11 Dec 2024 19:56:39 -0500 Subject: [PATCH] More writing for the math and browser APIs --- .../1-introduction/GasketWeighted.tsx | 2 +- .../1-introduction/cameraGasket.ts | 10 ++ .../1-introduction/chaosGame.js | 1 - .../1-introduction/index.mdx | 145 ++++++++++++------ .../1-introduction/plot.ts | 39 ++--- .../2-transforms/index.mdx | 15 +- .../src/applyTransform.ts | 3 +- .../2024-11-15-playing-with-fire/src/coefs.ts | 11 -- .../src/transform.ts | 14 +- 9 files changed, 138 insertions(+), 102 deletions(-) create mode 100644 blog/2024-11-15-playing-with-fire/1-introduction/cameraGasket.ts delete mode 100644 blog/2024-11-15-playing-with-fire/src/coefs.ts diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx b/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx index 326b7f1..1d0776c 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx @@ -31,7 +31,7 @@ export default function GasketWeighted() { <>

{title}: {weight}

- setWeight(Number(e.currentTarget.value))}/>
diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/cameraGasket.ts b/blog/2024-11-15-playing-with-fire/1-introduction/cameraGasket.ts new file mode 100644 index 0000000..8f729cc --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/1-introduction/cameraGasket.ts @@ -0,0 +1,10 @@ +export function camera( + size: number, + x: number, + y: number +): [number, number] { + return [ + Math.floor(x * size), + Math.floor(y * size) + ]; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js index 75f0d5f..17f6962 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js +++ b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js @@ -30,5 +30,4 @@ function* chaosGame({width, height}) { yield img; } -// Wiring so the code above displays properly render() diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx index befe48d..eeb18f9 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx @@ -36,9 +36,7 @@ can understand without too much prior knowledge. ## Iterated function systems :::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)," @@ -50,15 +48,18 @@ $$ 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 -a "solution" of some kind. Our goal is to find all points in the set $S$, plot them, and display that image. +### Solution set + +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: - - import {VictoryChart, VictoryTheme, VictoryScatter, VictoryLegend} from "victory"; export const simpleData = [ {x: 0, y: 0}, @@ -71,35 +72,52 @@ export const simpleData = [ 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. - -TODO: Explain characteristics of the solution - fixed set - -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. -And if we choose different functions to start with, our solution set changes, and we'll end up -with a new picture. +And if we change the functions, our solution changes, and we'll get 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. +### Transform functions -### 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 -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: +The general form of an affine transformation is: $$ -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} }) +{transformSource} + +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))) @@ -115,32 +133,37 @@ export const shiftData = simpleData.map(({x, y}) => { return {x: x + 1, y} }) /> -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. + +
+ If you're interested in more math... + + TODO: Contractive functions, attractors, etc.? +
+ +### 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 -function using these parameters: +> The solution, $S$, is the union of all sets produced by applying each function, $F_i$, +> to points in the solution. -$$ -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 \\ -$$ +There's just one small problem: to solve the equation, we must already know what the solution is? -$$ -\begin{align*} -F_{shift}(x,y) &= (1 \cdot x + 0 \cdot y + 1, 0 \cdot x + 1 \cdot y + 0) \\ -F_{shift}(x,y) &= (x + 1, y) -\end{align*} -$$ - -Fractal flames use more complex functions to produce a wide variety of images, but all follow this same format. +TODO: Phrase it another way? +A point is in the solution if it can be reached by applying +one of the functions to another point in the solution? +Is that the definition of a fixed set? ## 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) \\ @@ -169,7 +192,7 @@ $$ 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' @@ -181,14 +204,36 @@ import randintSource from '!!raw-loader!../src/randomInteger' {randintSource} -Finally, implementing the `plot` function. Web browsers have a [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) -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): +### Plotting + +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" + +{cameraSource} + +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' {plotSource} +Putting it all together, we have our first image: + import Playground from '@theme/Playground' import Scope from './scope' @@ -199,8 +244,8 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
-Note: The image here is slightly different than the fractal flame paper; 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). +Note: The image here is slightly different than the one in the paper. +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). ## Weights diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts b/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts index 2364330..ef386f5 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts +++ b/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts @@ -1,10 +1,6 @@ -/** - * ImageData is an array that contains - * four elements per pixel (one for each - * red, green, blue, and alpha value). - * This maps from pixel coordinates - * to the array index - */ +// hidden-start +import {camera} from "./cameraGasket" +// hidden-end function imageIndex( width: number, x: number, @@ -18,17 +14,9 @@ export function plot( y: number, img: ImageData ) { - // Translate (x,y) coordinates - // 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); + let [pixelX, pixelY] = camera(img.width, x, y); - const index = imageIndex( + const i = imageIndex( img.width, pixelX, pixelY @@ -36,17 +24,18 @@ export function plot( // Skip pixels outside the display range if ( - index < 0 || - index > img.data.length + i < 0 || + i > img.data.length ) { return; } // Set the pixel to black by writing 0 - // to the first three elements, - // and 255 to the last element - img.data[index] = 0; - img.data[index + 1] = 0; - img.data[index + 2] = 0; - img.data[index + 3] = 0xff; + // to the first three elements at the index + // (red, green, and blue, respectively), + // and 255 to the last element (alpha) + img.data[i] = 0; + img.data[i + 1] = 0; + img.data[i + 2] = 0; + img.data[i + 3] = 0xff; } \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx b/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx index 475b36e..1979e4f 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx +++ b/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx @@ -12,22 +12,15 @@ shapes and patterns that fractal flames are known for. :::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 -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. - +This post covers section 3 of the Fractal Flame Algorithm paper ::: ## 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," and showed how diff --git a/blog/2024-11-15-playing-with-fire/src/applyTransform.ts b/blog/2024-11-15-playing-with-fire/src/applyTransform.ts index 0b28ae0..92b2165 100644 --- a/blog/2024-11-15-playing-with-fire/src/applyTransform.ts +++ b/blog/2024-11-15-playing-with-fire/src/applyTransform.ts @@ -1,5 +1,4 @@ -import {Transform} from "./transform"; -import {applyCoefs, Coefs} from "./coefs"; +import {Transform, Coefs, applyCoefs} from "./transform"; import {blend, VariationBlend} from "./blend"; export const applyTransform = (coefs: Coefs, variations: VariationBlend): Transform => diff --git a/blog/2024-11-15-playing-with-fire/src/coefs.ts b/blog/2024-11-15-playing-with-fire/src/coefs.ts deleted file mode 100644 index da7e129..0000000 --- a/blog/2024-11-15-playing-with-fire/src/coefs.ts +++ /dev/null @@ -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) - ] -} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/transform.ts b/blog/2024-11-15-playing-with-fire/src/transform.ts index ce099a0..9b7e783 100644 --- a/blog/2024-11-15-playing-with-fire/src/transform.ts +++ b/blog/2024-11-15-playing-with-fire/src/transform.ts @@ -1 +1,13 @@ -export type Transform = (x: number, y: number) => [number, number]; \ No newline at end of file +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) + ] +} \ No newline at end of file