+ >
+ );
+}
\ No newline at end of file
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..47d104c
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/1-introduction/cameraGasket.ts
@@ -0,0 +1,10 @@
+export function camera(
+ x: number,
+ y: number,
+ size: 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
new file mode 100644
index 0000000..777ccfd
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js
@@ -0,0 +1,34 @@
+// Hint: try changing the iteration count
+const iterations = 100000;
+
+// Hint: negating `x` and `y` creates some cool images
+const xforms = [
+ (x, y) => [x / 2, y / 2],
+ (x, y) => [(x + 1) / 2, y / 2],
+ (x, y) => [x / 2, (y + 1) / 2]
+];
+
+function* chaosGame({ width, height }) {
+ let img =
+ new ImageData(width, height);
+ let [x, y] = [
+ randomBiUnit(),
+ randomBiUnit()
+ ];
+
+ for (let i = 0; i < iterations; i++) {
+ const index =
+ randomInteger(0, xforms.length);
+ [x, y] = xforms[index](x, y);
+
+ if (i > 20)
+ plot(x, y, img);
+
+ if (i % 1000 === 0)
+ yield img;
+ }
+
+ yield img;
+}
+
+render();
diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts
new file mode 100644
index 0000000..41a9922
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts
@@ -0,0 +1,43 @@
+// hidden-start
+import { randomBiUnit } from "../src/randomBiUnit";
+import { randomChoice } from "../src/randomChoice";
+import { plot } from "./plot";
+import { Transform } from "../src/transform";
+
+const quality = 0.5;
+const step = 1000;
+// hidden-end
+export type Props = {
+ width: number,
+ height: number,
+ transforms: [number, Transform][]
+}
+
+export function* chaosGameWeighted(
+ { width, height, transforms }: Props
+) {
+ let img =
+ new ImageData(width, height);
+ let [x, y] = [
+ randomBiUnit(),
+ randomBiUnit()
+ ];
+
+ const pixels = width * height;
+ const iterations = quality * pixels;
+ for (let i = 0; i < iterations; i++) {
+ // highlight-start
+ const [_, xform] =
+ randomChoice(transforms);
+ // highlight-end
+ [x, y] = xform(x, y);
+
+ if (i > 20)
+ plot(x, y, img);
+
+ if (i % step === 0)
+ yield img;
+ }
+
+ yield img;
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..aa77cf9
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx
@@ -0,0 +1,303 @@
+---
+slug: 2024/11/playing-with-fire
+title: "Playing with fire: The fractal flame algorithm"
+date: 2024-12-16 21:30:00
+authors: [bspeice]
+tags: []
+---
+
+
+Wikipedia describes [fractal flames](https://en.wikipedia.org/wiki/Fractal_flame) as:
+
+> a member of the iterated function system class of fractals
+
+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 banner from '../banner.png'
+
+
+
+
+
+
+
+I don't remember when exactly 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.
+
+The [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, I wanted to revisit it.
+
+This guide is my attempt to explain how fractal flames work so that younger me — and others interested in the art —
+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),"
+or IFS. The formula for an IFS is short, but takes some time to work through:
+
+$$
+S = \bigcup_{i=0}^{n-1} F_i(S)
+$$
+
+### 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 the 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},
+ {x: 1, y: 1},
+ {x: 2, y: 2}
+]
+
+
+
+
+
+With fractal flames, rather than listing individual points, we use functions to describe the solution.
+This means there are an infinite number of points, but if we find _enough_ points to plot, we get a nice picture.
+And if the functions change, the solution also changes, and we get something new.
+
+### Transform functions
+
+Second, the $F_i(S)$ functions, also known as "transforms."
+Each transform takes in a 2-dimensional point and gives a new point back
+(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
+called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)." Every transform uses the same formula:
+
+$$
+F_i(a_i x + b_i y + c_i, d_i x + e_i y + f_i)
+$$
+
+import transformSource from "!!raw-loader!../src/transform"
+import CodeBlock from '@theme/CodeBlock'
+
+{transformSource}
+
+The parameters ($a_i$, $b_i$, etc.) are values we choose.
+For example, we can define 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.5, 1 \cdot y + 1.5)
+\end{align*}
+$$
+
+Applying this transform to the original points gives 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)))
+
+
+
+
+
+
+
+Fractal flames use more complex functions, but they all start with this structure.
+
+### Fixed set
+
+With those definitions in place, let's revisit the initial problem:
+
+$$
+S = \bigcup_{i=0}^{n-1} F_i(S)
+$$
+
+Or, in English, we might say:
+
+> Our solution, $S$, is the union of all sets produced by applying each function, $F_i$,
+> to points in the solution.
+
+There's just one small problem: to find the solution, we must already know which points are in the solution.
+What?
+
+John E. Hutchinson provides an explanation in the [original paper](https://maths-people.anu.edu.au/~john/Assets/Research%20Papers/fractals_self-similarity.pdf)
+defining the mathematics of iterated function systems:
+
+> Furthermore, $S$ is compact and is the closure of the set of fixed points $s_{i_1...i_p}$
+> of finite compositions $F_{i_1...i_p}$ of members of $F$.
+
+Before your eyes glaze over, let's unpack this:
+
+- **Furthermore, $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))**:
+ 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,
+ using the output of one function as input to the next), we will arrive at the points in the solution
+
+Thus, by applying the functions to fixed points of our system, we will find the other points we care about.
+
+
+ If you want a bit more math...
+
+ ...then there are some extra details I've glossed over so far.
+
+ 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
+ algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will
+ degenerate and produce a bad image.
+
+ Second, we're focused on $\mathbb{R}^2$ because we're generating images, but the math
+ allows for arbitrary dimensions; you could also have 3-dimensional fractal flames.
+
+ 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 below).
+
+
+This is still a bit vague, so let's work through an example.
+
+## [Sierpinski's gasket](https://www.britannica.com/biography/Waclaw-Sierpinski)
+
+The Fractal Flame paper gives three functions to use for a first IFS:
+
+$$
+F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\
+~\\
+F_1(x, y) = \left({{x + 1} \over 2}, {y \over 2} \right) \\
+~\\
+F_2(x, y) = \left({x \over 2}, {{y + 1} \over 2} \right)
+$$
+
+### The chaos game
+
+Now, how do we find the "fixed points" mentioned earlier? The paper lays out an algorithm called the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)"
+that gives us points in the solution:
+
+$$
+\begin{align*}
+&(x, y) = \text{random point in the bi-unit square} \\
+&\text{iterate } \{ \\
+&\hspace{1cm} i = \text{random integer from 0 to } n - 1 \\
+&\hspace{1cm} (x,y) = F_i(x,y) \\
+&\hspace{1cm} \text{plot}(x,y) \text{ if iterations} > 20 \\
+\}
+\end{align*}
+$$
+
+:::note
+The chaos game algorithm is effectively the "finite compositions of $F_{i_1..i_p}$" mentioned earlier.
+:::
+
+Let's turn this into code, one piece at a time.
+
+To start, we need to generate some random numbers. The "bi-unit square" is the range $[-1, 1]$,
+and we can do this using an existing API:
+
+import biunitSource from '!!raw-loader!../src/randomBiUnit'
+
+{biunitSource}
+
+Next, we need to choose a random integer from $0$ to $n - 1$:
+
+import randintSource from '!!raw-loader!../src/randomInteger'
+
+{randintSource}
+
+### Plotting
+
+Finally, implementing the `plot` function. This blog series is interactive,
+so everything displays directly in the browser. As an alternative,
+software like `flam3` and Apophysis can "plot" by saving an image to disk.
+
+To see 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 in an image and show 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
+with range $[0, 1]$ for both $x$ and $y$:
+
+import cameraSource from "!!raw-loader!./cameraGasket"
+
+{cameraSource}
+
+Next, we'll store the pixel data in an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData).
+Each pixel on screen has a corresponding index in the `data` array.
+To plot a point, 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'
+
+import chaosGameSource from '!!raw-loader!./chaosGame'
+
+{chaosGameSource}
+
+
+
+
+ The image here is slightly different than in the paper.
+ I think the paper has an error, so I'm plotting the image
+ like the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
+
+
+### Weights
+
+There's one last step before we finish the introduction. So far, each transform has
+the same chance of being picked in the chaos game.
+We can change that by giving them a "weight" ($w_i$) instead:
+
+import randomChoiceSource from '!!raw-loader!../src/randomChoice'
+
+{randomChoiceSource}
+
+If we let the chaos game run forever, these weights wouldn't matter.
+But because the iteration count is limited, changing the weights
+means we don't plot some parts of the image:
+
+import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
+
+{chaosGameWeightedSource}
+
+:::tip
+Double-click the image if you want to save a copy!
+:::
+
+import GasketWeighted from "./GasketWeighted";
+import {SquareCanvas} from "../src/Canvas";
+
+
+
+## Summary
+
+Studying the foundations of fractal flames is challenging,
+but we now have an understanding of the mathematics
+and the implementation of iterated function systems.
+
+In the next post, we'll look at the first innovation of fractal flame algorithm: variations.
\ No newline at end of file
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
new file mode 100644
index 0000000..6db267d
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts
@@ -0,0 +1,44 @@
+// hidden-start
+import { camera } from "./cameraGasket";
+
+// hidden-end
+function imageIndex(
+ x: number,
+ y: number,
+ width: number
+) {
+ return y * (width * 4) + x * 4;
+}
+
+export function plot(
+ x: number,
+ y: number,
+ img: ImageData
+) {
+ let [pixelX, pixelY] =
+ camera(x, y, img.width);
+
+ // Skip coordinates outside the display
+ if (
+ pixelX < 0 ||
+ pixelX >= img.width ||
+ pixelY < 0 ||
+ pixelY >= img.height
+ )
+ return;
+
+ const i = imageIndex(
+ pixelX,
+ pixelY,
+ img.width
+ );
+
+ // Set the pixel to black by setting
+ // the first three elements to 0
+ // (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/1-introduction/scope.tsx b/blog/2024-11-15-playing-with-fire/1-introduction/scope.tsx
new file mode 100644
index 0000000..4adee1d
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/1-introduction/scope.tsx
@@ -0,0 +1,12 @@
+import Gasket from "./Gasket";
+import { plot } from "./plot";
+import { randomBiUnit } from "../src/randomBiUnit";
+import { randomInteger } from "../src/randomInteger";
+
+const Scope = {
+ Gasket,
+ plot,
+ randomBiUnit,
+ randomInteger
+};
+export default Scope;
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/CoefEditor.tsx b/blog/2024-11-15-playing-with-fire/2-transforms/CoefEditor.tsx
new file mode 100644
index 0000000..77e0abd
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/2-transforms/CoefEditor.tsx
@@ -0,0 +1,52 @@
+import TeX from "@matejmazur/react-katex";
+import { Coefs } from "../src/transform";
+
+import styles from "../src/css/styles.module.css";
+
+export interface Props {
+ title: String;
+ isPost: boolean;
+ coefs: Coefs;
+ setCoefs: (coefs: Coefs) => void;
+ resetCoefs: () => void;
+}
+
+export const CoefEditor = ({ title, isPost, coefs, setCoefs, resetCoefs }: Props) => {
+ const resetButton = ;
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/buildBlend.ts b/blog/2024-11-15-playing-with-fire/2-transforms/buildBlend.ts
new file mode 100644
index 0000000..155ddfb
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/2-transforms/buildBlend.ts
@@ -0,0 +1,17 @@
+import { Coefs } from "../src/transform";
+import { VariationProps } from "./VariationEditor";
+import { linear } from "../src/linear";
+import { julia } from "../src/julia";
+import { popcorn } from "../src/popcorn";
+import { pdj } from "../src/pdj";
+import { pdjParams } from "../src/params";
+import { Blend } from "../src/blend";
+
+export function buildBlend(coefs: Coefs, variations: VariationProps): Blend {
+ return [
+ [variations.linear, linear],
+ [variations.julia, julia],
+ [variations.popcorn, popcorn(coefs)],
+ [variations.pdj, pdj(pdjParams)]
+ ];
+}
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/chaosGameFinal.ts b/blog/2024-11-15-playing-with-fire/2-transforms/chaosGameFinal.ts
new file mode 100644
index 0000000..c653dee
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/2-transforms/chaosGameFinal.ts
@@ -0,0 +1,51 @@
+// hidden-start
+import { randomBiUnit } from "../src/randomBiUnit";
+import { randomChoice } from "../src/randomChoice";
+import { plotBinary as plot } from "../src/plotBinary";
+import { Transform } from "../src/transform";
+import { Props as WeightedProps } from "../1-introduction/chaosGameWeighted";
+
+const quality = 0.5;
+const step = 1000;
+// hidden-end
+export type Props = WeightedProps & {
+ final: Transform,
+}
+
+export function* chaosGameFinal(
+ {
+ width,
+ height,
+ transforms,
+ final
+ }: Props
+) {
+ let img =
+ new ImageData(width, height);
+ let [x, y] = [
+ randomBiUnit(),
+ randomBiUnit()
+ ];
+
+ const pixels = width * height;
+ const iterations = quality * pixels;
+ for (let i = 0; i < iterations; i++) {
+ const [_, transform] =
+ randomChoice(transforms);
+ [x, y] = transform(x, y);
+
+ // highlight-start
+ const [finalX, finalY] = final(x, y);
+ // highlight-end
+
+ if (i > 20)
+ // highlight-start
+ plot(finalX, finalY, img);
+ // highlight-end
+
+ if (i % step === 0)
+ yield img;
+ }
+
+ yield img;
+}
\ 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
new file mode 100644
index 0000000..55fd175
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx
@@ -0,0 +1,216 @@
+---
+slug: 2024/11/playing-with-fire-transforms
+title: "Playing with fire: Transforms and variations"
+date: 2024-12-16 21:31:00
+authors: [bspeice]
+tags: []
+---
+
+Now that we've learned about the chaos game, it's time to spice things up. Variations create the
+shapes and patterns that fractal flames are known for.
+
+
+
+:::info
+This post uses [reference parameters](../params.flame) to demonstrate the fractal flame algorithm.
+If you're interested in tweaking the parameters, or creating your own, [Apophysis](https://sourceforge.net/projects/apophysis/)
+can load that file.
+:::
+
+## 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
+playing the chaos game gives us an image of Sierpinski's Gasket. Even though we used simple functions,
+the image it generates is intriguing. But what would happen if we used something more complex?
+
+This leads us to the first big innovation of the fractal flame algorithm: adding non-linear functions
+after the affine transform. These functions are called "variations":
+
+$$
+F_i(x, y) = V_j(a_i x + b_i y + c_i, d_i x + e_i y + f_i)
+$$
+
+import variationSource from '!!raw-loader!../src/variation'
+
+{variationSource}
+
+Just like transforms, variations ($V_j$) are functions that take in $(x, y)$ coordinates
+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 variation functions,
+and the official `flam3` implementation supports [98 different variations](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c).
+
+To draw our reference image, we'll focus on just four:
+
+### Linear (variation 0)
+
+This variation is dead simple: return the $x$ and $y$ coordinates as-is.
+
+$$
+V_0(x,y) = (x,y)
+$$
+
+import linearSrc from '!!raw-loader!../src/linear'
+
+{linearSrc}
+
+:::tip
+In a way, we've already been using this variation! The transforms that define Sierpinski's Gasket
+apply the affine coefficients to the input point and use that as the output.
+:::
+
+### Julia (variation 13)
+
+This variation is a good example of a non-linear function. It uses both trigonometry
+and probability to produce interesting shapes:
+
+$$
+\begin{align*}
+r &= \sqrt{x^2 + y^2} \\
+\theta &= \text{arctan}(x / y) \\
+\Omega &= \left\{
+\begin{array}{lr}
+0 \hspace{0.4cm} \text{w.p. } 0.5 \\
+\pi \hspace{0.4cm} \text{w.p. } 0.5 \\
+\end{array}
+\right\} \\
+
+V_{13}(x, y) &= \sqrt{r} \cdot (\text{cos} ( \theta / 2 + \Omega ), \text{sin} ( \theta / 2 + \Omega ))
+\end{align*}
+$$
+
+import juliaSrc from '!!raw-loader!../src/julia'
+
+{juliaSrc}
+
+### Popcorn (variation 17)
+
+Some variations rely on knowing the transform's affine coefficients; they're called "dependent variations."
+For this variation, we use $c$ and $f$:
+
+$$
+V_{17}(x,y) = (x + c\ \text{sin}(\text{tan }3y), y + f\ \text{sin}(\text{tan }3x))
+$$
+
+import popcornSrc from '!!raw-loader!../src/popcorn'
+
+{popcornSrc}
+
+### PDJ (variation 24)
+
+Some variations have extra parameters we can choose; they're called "parametric variations."
+For the PDJ variation, there are four extra parameters:
+
+$$
+p_1 = \text{pdj.a} \hspace{0.1cm} p_2 = \text{pdj.b} \hspace{0.1cm} p_3 = \text{pdj.c} \hspace{0.1cm} p_4 = \text{pdj.d} \\
+V_{24} = (\text{sin}(p_1 y) - \text{cos}(p_2 x), \text{sin}(p_3 x) - \text{cos}(p_4 y))
+$$
+
+import pdjSrc from '!!raw-loader!../src/pdj'
+
+{pdjSrc}
+
+## 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}$) that changes how much it contributes to the result:
+
+$$
+F_i(x,y) = \sum_{j} v_{ij} V_j(x, y)
+$$
+
+The formula looks intimidating, but it's not hard to implement:
+
+import blendSource from "!!raw-loader!../src/blend";
+
+{blendSource}
+
+With that in place, we have enough to render a fractal flame. We'll use the same
+chaos game as before, but the new transforms and variations produce a dramatically different image:
+
+:::tip
+Try using the variation weights to figure out which parts of the image each transform controls.
+:::
+
+import {SquareCanvas} from "../src/Canvas";
+import FlameBlend from "./FlameBlend";
+
+
+
+## Post transforms
+
+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 should look familiar:
+
+$$
+\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'
+
+{postSource}
+
+The image below uses the same transforms/variations as the previous fractal flame,
+but allows changing the post-transform coefficients:
+
+
+ If you want to test your understanding...
+
+ - What post-transform coefficients will give us the previous image?
+ - What post-transform coefficients will give us a _mirrored_ image?
+
+
+import FlamePost from "./FlamePost";
+
+
+
+## Final transforms
+
+The last step is to introduce a "final transform" ($F_{final}$) that is applied
+regardless of which regular transform ($F_i$) the chaos game selects.
+It's just like a normal transform (composition of affine transform, variation blend, and post transform),
+but it doesn't affect the chaos game state.
+
+After adding the final transform, our chaos game algorithm looks like this:
+
+$$
+\begin{align*}
+&(x, y) = \text{random point in the bi-unit square} \\
+&\text{iterate } \{ \\
+&\hspace{1cm} i = \text{random integer from 0 to } n - 1 \\
+&\hspace{1cm} (x,y) = F_i(x,y) \\
+&\hspace{1cm} (x_f,y_f) = F_{final}(x,y) \\
+&\hspace{1cm} \text{plot}(x_f,y_f) \text{ if iterations} > 20 \\
+\}
+\end{align*}
+$$
+
+import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal"
+
+{chaosGameFinalSource}
+
+This image uses the same normal/post transforms as above, but allows modifying
+the coefficients and variations of the final transform:
+
+import FlameFinal from "./FlameFinal";
+
+
+
+## Summary
+
+Variations are the fractal flame algorithm's first major innovation.
+By blending variation functions and post/final transforms, we generate unique images.
+
+However, these images are grainy and unappealing. In the next post, we'll clean up
+the image quality and add some color.
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/post.ts b/blog/2024-11-15-playing-with-fire/2-transforms/post.ts
new file mode 100644
index 0000000..c70c3d6
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/2-transforms/post.ts
@@ -0,0 +1,11 @@
+// hidden-start
+import { applyCoefs, Coefs, Transform } from "../src/transform";
+// hidden-end
+export const transformPost = (
+ transform: Transform,
+ coefs: Coefs
+): Transform =>
+ (x, y) => {
+ [x, y] = transform(x, y);
+ return applyCoefs(x, y, coefs);
+ }
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/FlameColor.tsx b/blog/2024-11-15-playing-with-fire/3-log-density/FlameColor.tsx
new file mode 100644
index 0000000..2040b0b
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/3-log-density/FlameColor.tsx
@@ -0,0 +1,181 @@
+import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
+import * as params from "../src/params";
+import { PainterContext } from "../src/Canvas";
+import { colorFromPalette } from "./colorFromPalette";
+import { chaosGameColor, Props as ChaosGameColorProps, TransformColor } from "./chaosGameColor";
+
+import styles from "../src/css/styles.module.css";
+import { histIndex } from "../src/camera";
+import { useColorMode } from "@docusaurus/theme-common";
+
+type PaletteBarProps = {
+ height: number;
+ palette: number[];
+ children?: React.ReactNode;
+}
+export const PaletteBar: React.FC = ({ height, palette, children }) => {
+ const sizingRef = useRef(null);
+ const [width, setWidth] = useState(0);
+ useEffect(() => {
+ if (sizingRef) {
+ setWidth(sizingRef.current.offsetWidth);
+ }
+ }, [sizingRef]);
+
+ const canvasRef = useRef(null);
+ const paletteImage = useMemo(() => {
+ if (width === 0) {
+ return;
+ }
+
+ const image = new ImageData(width, height);
+ for (let x = 0; x < width; x++) {
+ const colorIndex = x / width;
+ const [r, g, b] = colorFromPalette(palette, colorIndex);
+
+ for (let y = 0; y < height; y++) {
+ const pixelIndex = histIndex(x, y, width, 4);
+ image.data[pixelIndex] = r * 0xff;
+ image.data[pixelIndex + 1] = g * 0xff;
+ image.data[pixelIndex + 2] = b * 0xff;
+ image.data[pixelIndex + 3] = 0xff;
+ }
+ }
+
+ return image;
+ }, [width, height, palette]);
+
+ useEffect(() => {
+ if (canvasRef && paletteImage) {
+ canvasRef.current.getContext("2d").putImageData(paletteImage, 0, 0);
+ }
+ }, [canvasRef, paletteImage]);
+
+ const canvasStyle = { filter: useColorMode().colorMode === "dark" ? "invert(1)" : "" };
+
+ return (
+ <>
+