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 42ff413..75f0d5f 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 @@ -9,6 +9,7 @@ const xforms = [ ] function* chaosGame({width, height}) { + const step = 1000; let img = new ImageData(width, height); let [x, y] = [ randomBiUnit(), @@ -22,7 +23,7 @@ function* chaosGame({width, height}) { if (c > 20) plot(x, y, img); - if (c % 1000 === 0) + if (c % step === 0) yield img; } 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 index a8d4c7e..e219ed9 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts +++ b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts @@ -6,7 +6,7 @@ import {Transform} from "../src/transform"; const iterations = 50_000; const step = 1000; // hidden-end -type Props = { +export type Props = { width: number, height: number, transforms: [number, Transform][] @@ -20,6 +20,7 @@ export function* chaosGameWeighted( randomBiUnit() ]; + // TODO: Explain quality const iterations = width * height * 0.5; for (let c = 0; c < iterations; c++) { // highlight-start 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 e9c8ec9..de3897b 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 @@ -6,11 +6,11 @@ authors: [bspeice] tags: [] --- -Wikipedia [describes](https://en.wikipedia.org/wiki/Fractal_flame) fractal flames as: +Wikipedia describes fractal flames fractal flames as: > a member of the iterated function system class of fractals -I think of them a different way: beauty in mathematics. +It's a bit 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' @@ -21,37 +21,38 @@ import banner from '../banner.png' -I don't remember exactly when or how I originally came across 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 actively participate in. +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 [paper](https://flam3.com/flame_draves.pdf) describing their mathematical structure was too much -for me to handle at the time (I was ~12 years old), and I was content to play around and enjoy the pictures. -But the desire to understand it stuck with me, so I wanted to try again. With a graduate degree in Financial Engineering under my belt, -maybe it would be easier this time. +The original [Fractal Flame](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. + +This guide is my attempt to explain fractal flames in a way that younger me — and others interested in the art — +can understand without too much prior knowledge. --- ## Iterated function systems -Let's begin by defining an "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system)" (IFS). -We'll start at the end and work backwards to build a practical understanding. In mathematical notation, an IFS is: +As mentioned above, 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: $$ -S = \bigcup_{i=0}^{n-1} F_i(S) \\[0.6cm] -S \in \mathbb{R}^2 \\ -F_i(S) \in \mathbb{R}^2 \rightarrow \mathbb{R}^2 +S = \bigcup_{i=0}^{n-1} F_i(S) $$ -### Stationary point +### Fixed set -First, $S$. We're generating images, so everything is in two dimensions: $S \in \mathbb{R}^2$. The set $S$ is -all points that are "in the system." To generate our final image, we just plot every point in the system -like a coordinate chart. - -TODO: What is a stationary point? How does it relate to the chaos game? Why does the chaos game work? +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. 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}, @@ -63,8 +64,18 @@ export const simpleData = [ -For fractal flames, we just need to figure out which points are in $S$ and plot them. While there are -technically an infinite number of points, if we find _enough_ points and plot them, we end up with a nice picture. +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 +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. + +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 @@ -182,7 +193,8 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
-Note: The image here is different than the fractal flame paper, but I think the paper has an error. +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). ## 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 a30eaf3..2364330 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 @@ -29,17 +29,17 @@ export function plot( let pixelY = Math.floor(y * img.height); const index = imageIndex( - img.width, - pixelX, - pixelY + img.width, + pixelX, + pixelY ); // Skip pixels outside the display range if ( - index < 0 || - index > img.data.length + index < 0 || + index > img.data.length ) { - return; + return; } // Set the pixel to black by writing 0 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 index 53080c3..fc4d782 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/CoefEditor.tsx +++ b/blog/2024-11-15-playing-with-fire/2-transforms/CoefEditor.tsx @@ -14,7 +14,7 @@ export const CoefEditor = ({title, isPost, coefs, setCoefs, resetCoefs}: Props) const resetButton = return ( -
+

{title} {resetButton}

{isPost ? \alpha : 'a'}: {coefs.a}

diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/FlameBlend.tsx b/blog/2024-11-15-playing-with-fire/2-transforms/FlameBlend.tsx index b4792d4..a05dcf8 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/FlameBlend.tsx +++ b/blog/2024-11-15-playing-with-fire/2-transforms/FlameBlend.tsx @@ -4,9 +4,8 @@ import * as params from "../src/params" import {PainterContext} from "../src/Canvas" import {chaosGameFinal} from "./chaosGameFinal" import {VariationEditor, VariationProps} from "./VariationEditor" -import {xform1Weight} from "../src/params"; -import {applyTransform} from "@site/blog/2024-11-15-playing-with-fire/src/applyTransform"; -import {buildBlend} from "@site/blog/2024-11-15-playing-with-fire/2-transforms/buildBlend"; +import {applyTransform} from "../src/applyTransform"; +import {buildBlend} from "./buildBlend"; export default function FlameBlend() { const {width, height, setPainter} = useContext(PainterContext); diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/FlamePost.tsx b/blog/2024-11-15-playing-with-fire/2-transforms/FlamePost.tsx index 33e0664..539114e 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/FlamePost.tsx +++ b/blog/2024-11-15-playing-with-fire/2-transforms/FlamePost.tsx @@ -3,7 +3,7 @@ import {Coefs} from "../src/coefs" import {Transform} from "../src/transform"; import * as params from "../src/params"; import {PainterContext} from "../src/Canvas" -import {chaosGameFinal, ChaosGameFinalProps} from "./chaosGameFinal" +import {chaosGameFinal, Props as ChaosGameFinalProps} from "./chaosGameFinal" import {CoefEditor} from "./CoefEditor" import {applyPost, applyTransform} from "@site/blog/2024-11-15-playing-with-fire/src/applyTransform"; diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/VariationEditor.tsx b/blog/2024-11-15-playing-with-fire/2-transforms/VariationEditor.tsx index 1785ea7..57a3159 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/VariationEditor.tsx +++ b/blog/2024-11-15-playing-with-fire/2-transforms/VariationEditor.tsx @@ -18,7 +18,7 @@ export const VariationEditor = ({title, variations, setVariations, resetVariatio const resetButton = return ( -
+

{title} {resetButton}

Linear: {variations.linear} 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 index 638846d..147b02b 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/chaosGameFinal.ts +++ b/blog/2024-11-15-playing-with-fire/2-transforms/chaosGameFinal.ts @@ -3,20 +3,19 @@ import { randomBiUnit } from "../src/randomBiUnit"; import { randomChoice } from "../src/randomChoice"; import { plotBinary as plot } from "../src/plotBinary" import {Transform} from "../src/transform"; -import {ChaosGameWeightedProps} from "../1-introduction/chaosGameWeighted"; +import {Props as ChaosGameWeightedProps} from "../1-introduction/chaosGameWeighted"; + +const quality = 0.5; +const step = 1000; // hidden-end -export type ChaosGameFinalProps = ChaosGameWeightedProps & { +export type Props = ChaosGameWeightedProps & { final: Transform, - quality?: number, - step?: number, } -export function* chaosGameFinal({width, height, transforms, final, quality, step}: ChaosGameFinalProps) { +export function* chaosGameFinal({width, height, transforms, final}: Props) { let image = new ImageData(width, height); let [x, y] = [randomBiUnit(), randomBiUnit()]; - const iterations = (quality ?? 0.5) * width * height; - step = step ?? 1000; - + const iterations = width * height * quality; for (let i = 0; i < iterations; i++) { const [_, transform] = randomChoice(transforms); [x, y] = transform(x, y); 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 64f778f..47c60c2 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 @@ -142,10 +142,10 @@ The sliders below change the variation weights for each transform (the $v_{ij}$ try changing them around to see which parts of the image are controlled by each transform. -import Canvas from "../src/Canvas"; +import {SquareCanvas} from "../src/Canvas"; import FlameBlend from "./FlameBlend"; - + ## Post transforms @@ -160,10 +160,10 @@ $$ import FlamePost from "./FlamePost"; - + ## Final transform import FlameFinal from "./FlameFinal"; - \ No newline at end of file + \ 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 index bf75f31..84a0db1 100644 --- 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 @@ -1,35 +1,12 @@ import React, {useContext, useEffect, useMemo, useRef, useState} from "react"; import * as params from "../src/params"; -import {InvertibleCanvas, PainterContext} from "../src/Canvas"; +import {PainterContext} from "../src/Canvas"; import {colorFromPalette} from "./paintColor"; import {chaosGameColor, ChaosGameColorProps, TransformColor} from "./chaosGameColor"; import styles from "../src/css/styles.module.css"; import {histIndex} from "../src/camera"; - -type AutoSizingCanvasProps = { - painter: (width: number, height: number) => ImageData; -} -const AutoSizingCanvas: React.FC = ({painter}) => { - const sizingRef = useRef(null); - const [width, setWidth] = useState(0); - const [height, setHeight] = useState(0); - - useEffect(() => { - if (sizingRef) { - setWidth(sizingRef.current.offsetWidth); - setHeight(sizingRef.current.offsetHeight) - } - }, [sizingRef]); - - const image: [ImageData] = useMemo(() => (width && height) ? [painter(width, height)] : null, [painter, width, height]); - - return ( -
- -
- ) -} +import {useColorMode} from "@docusaurus/theme-common"; const paletteBarPainter = (palette: number[]) => (width: number, height: number) => { @@ -60,7 +37,7 @@ const PaletteBar: React.FC = ({height, palette, children}) => { return ( <>
- + {/**/}
{children} @@ -90,9 +67,13 @@ type ColorEditorProps = { children?: React.ReactNode; } const ColorEditor: React.FC = ({title, palette, transformColor, setTransformColor, resetTransformColor, children}) => { - const painter = useMemo(() => colorSwatchPainter(palette, transformColor.color), [palette, transformColor]); const resetButton = + const [r, g, b] = colorFromPalette(palette, transformColor.color); + const colorCss = `rgb(${Math.floor(r * 0xff)},${Math.floor(g * 0xff)},${Math.floor(b * 0xff)})`; + + const {colorMode} = useColorMode(); + return ( <>
@@ -107,9 +88,12 @@ const ColorEditor: React.FC = ({title, palette, transformColor setTransformColor({...transformColor, colorSpeed: Number(e.currentTarget.value)})}/>
-
- -
+
{children} diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx index 69dbca5..7c96033 100644 --- a/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx +++ b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx @@ -28,11 +28,11 @@ import paintLinearSource from "!!raw-loader!./paintLinear" {paintLinearSource} -import Canvas from "../src/Canvas"; +import {SquareCanvas} from "../src/Canvas"; import FlameHistogram from "./FlameHistogram"; import {paintLinear} from "./paintLinear"; - + ## Log display @@ -42,7 +42,7 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic" import {paintLogarithmic} from './paintLogarithmic' - + ## Color @@ -52,4 +52,4 @@ import paintColorSource from "!!raw-loader!./paintColor" import FlameColor from "./FlameColor"; - \ No newline at end of file + \ No newline at end of file