Actually doing some writing

This commit is contained in:
Bradlee Speice 2024-12-08 22:50:46 -05:00
parent 5ae6b82d26
commit b608a25146
12 changed files with 78 additions and 82 deletions

View File

@ -9,6 +9,7 @@ const xforms = [
] ]
function* chaosGame({width, height}) { function* chaosGame({width, height}) {
const step = 1000;
let img = new ImageData(width, height); let img = new ImageData(width, height);
let [x, y] = [ let [x, y] = [
randomBiUnit(), randomBiUnit(),
@ -22,7 +23,7 @@ function* chaosGame({width, height}) {
if (c > 20) if (c > 20)
plot(x, y, img); plot(x, y, img);
if (c % 1000 === 0) if (c % step === 0)
yield img; yield img;
} }

View File

@ -6,7 +6,7 @@ import {Transform} from "../src/transform";
const iterations = 50_000; const iterations = 50_000;
const step = 1000; const step = 1000;
// hidden-end // hidden-end
type Props = { export type Props = {
width: number, width: number,
height: number, height: number,
transforms: [number, Transform][] transforms: [number, Transform][]
@ -20,6 +20,7 @@ export function* chaosGameWeighted(
randomBiUnit() randomBiUnit()
]; ];
// TODO: Explain quality
const iterations = width * height * 0.5; const iterations = width * height * 0.5;
for (let c = 0; c < iterations; c++) { for (let c = 0; c < iterations; c++) {
// highlight-start // highlight-start

View File

@ -6,11 +6,11 @@ authors: [bspeice]
tags: [] 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 > 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 isDarkMode from '@site/src/isDarkMode'
import banner from '../banner.png' import banner from '../banner.png'
@ -21,37 +21,38 @@ import banner from '../banner.png'
<!-- truncate --> <!-- truncate -->
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 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 actively participate in. 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 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), and I was content to play around and enjoy the pictures. 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 with me, so I wanted to try again. With a graduate degree in Financial Engineering under my belt, But the desire to understand it stuck around. Now, with a graduate degree under my belt, maybe I can make some progress.
maybe it would be easier this time.
This guide is my attempt to explain fractal flames in a way that younger me &mdash; and others interested in the art &mdash;
can understand without too much prior knowledge.
--- ---
## Iterated function systems ## Iterated function systems
Let's begin by defining an "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system)" (IFS). As mentioned above, fractal flames are a type of "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system),"
We'll start at the end and work backwards to build a practical understanding. In mathematical notation, an IFS is: 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 = \bigcup_{i=0}^{n-1} F_i(S)
S \in \mathbb{R}^2 \\
F_i(S) \in \mathbb{R}^2 \rightarrow \mathbb{R}^2
$$ $$
### 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 First, $S$. $S$ is the set of points in two dimensions (in math terms, $S \in \mathbb{R}^2$) that represent
all points that are "in the system." To generate our final image, we just plot every point in the system a "solution" of some kind. Our goal is to find all points in the set $S$, plot them, and display that image.
like a coordinate chart.
TODO: What is a stationary point? How does it relate to the chaos game? Why does the chaos game work?
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},
@ -63,8 +64,18 @@ export const simpleData = [
<VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/> <VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/>
</VictoryChart> </VictoryChart>
For fractal flames, we just need to figure out which points are in $S$ and plot them. While there are However, this is a pretty boring image. With fractal flames, rather than listing individual points,
technically an infinite number of points, if we find _enough_ points and plot them, we end up with a nice picture. 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 ### Transformation functions
@ -182,7 +193,8 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
<hr/> <hr/>
<small> <small>
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).
</small> </small>
## Weights ## Weights

View File

@ -29,17 +29,17 @@ export function plot(
let pixelY = Math.floor(y * img.height); let pixelY = Math.floor(y * img.height);
const index = imageIndex( const index = imageIndex(
img.width, img.width,
pixelX, pixelX,
pixelY pixelY
); );
// Skip pixels outside the display range // Skip pixels outside the display range
if ( if (
index < 0 || index < 0 ||
index > img.data.length index > img.data.length
) { ) {
return; return;
} }
// Set the pixel to black by writing 0 // Set the pixel to black by writing 0

View File

@ -14,7 +14,7 @@ export const CoefEditor = ({title, isPost, coefs, setCoefs, resetCoefs}: Props)
const resetButton = <button className={styles.inputReset} onClick={resetCoefs}>Reset</button> const resetButton = <button className={styles.inputReset} onClick={resetCoefs}>Reset</button>
return ( return (
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: 'auto auto auto'}}> <div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr'}}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p> <p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<p>{isPost ? <TeX>\alpha</TeX> : 'a'}: {coefs.a}</p> <p>{isPost ? <TeX>\alpha</TeX> : 'a'}: {coefs.a}</p>

View File

@ -4,9 +4,8 @@ import * as params from "../src/params"
import {PainterContext} from "../src/Canvas" import {PainterContext} from "../src/Canvas"
import {chaosGameFinal} from "./chaosGameFinal" import {chaosGameFinal} from "./chaosGameFinal"
import {VariationEditor, VariationProps} from "./VariationEditor" import {VariationEditor, VariationProps} from "./VariationEditor"
import {xform1Weight} from "../src/params"; import {applyTransform} from "../src/applyTransform";
import {applyTransform} from "@site/blog/2024-11-15-playing-with-fire/src/applyTransform"; import {buildBlend} from "./buildBlend";
import {buildBlend} from "@site/blog/2024-11-15-playing-with-fire/2-transforms/buildBlend";
export default function FlameBlend() { export default function FlameBlend() {
const {width, height, setPainter} = useContext(PainterContext); const {width, height, setPainter} = useContext(PainterContext);

View File

@ -3,7 +3,7 @@ import {Coefs} from "../src/coefs"
import {Transform} from "../src/transform"; import {Transform} from "../src/transform";
import * as params from "../src/params"; import * as params from "../src/params";
import {PainterContext} from "../src/Canvas" import {PainterContext} from "../src/Canvas"
import {chaosGameFinal, ChaosGameFinalProps} from "./chaosGameFinal" import {chaosGameFinal, Props as ChaosGameFinalProps} from "./chaosGameFinal"
import {CoefEditor} from "./CoefEditor" import {CoefEditor} from "./CoefEditor"
import {applyPost, applyTransform} from "@site/blog/2024-11-15-playing-with-fire/src/applyTransform"; import {applyPost, applyTransform} from "@site/blog/2024-11-15-playing-with-fire/src/applyTransform";

View File

@ -18,7 +18,7 @@ export const VariationEditor = ({title, variations, setVariations, resetVariatio
const resetButton = <button className={styles.inputReset} onClick={resetVariations}>Reset</button> const resetButton = <button className={styles.inputReset} onClick={resetVariations}>Reset</button>
return ( return (
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: 'auto auto auto auto'}}> <div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr'}}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p> <p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<span>Linear: {variations.linear}</span> <span>Linear: {variations.linear}</span>

View File

@ -3,20 +3,19 @@ import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice"; import { randomChoice } from "../src/randomChoice";
import { plotBinary as plot } from "../src/plotBinary" import { plotBinary as plot } from "../src/plotBinary"
import {Transform} from "../src/transform"; 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 // hidden-end
export type ChaosGameFinalProps = ChaosGameWeightedProps & { export type Props = ChaosGameWeightedProps & {
final: Transform, 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 image = new ImageData(width, height);
let [x, y] = [randomBiUnit(), randomBiUnit()]; let [x, y] = [randomBiUnit(), randomBiUnit()];
const iterations = (quality ?? 0.5) * width * height; const iterations = width * height * quality;
step = step ?? 1000;
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
const [_, transform] = randomChoice(transforms); const [_, transform] = randomChoice(transforms);
[x, y] = transform(x, y); [x, y] = transform(x, y);

View File

@ -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 try changing them around to see which parts of the image are controlled by
each transform. each transform.
import Canvas from "../src/Canvas"; import {SquareCanvas} from "../src/Canvas";
import FlameBlend from "./FlameBlend"; import FlameBlend from "./FlameBlend";
<!-- <Canvas><FlameBlend/></Canvas> --> <SquareCanvas><FlameBlend/></SquareCanvas>
## Post transforms ## Post transforms
@ -160,10 +160,10 @@ $$
import FlamePost from "./FlamePost"; import FlamePost from "./FlamePost";
<!-- <Canvas><FlamePost/></Canvas> --> <SquareCanvas><FlamePost/></SquareCanvas>
## Final transform ## Final transform
import FlameFinal from "./FlameFinal"; import FlameFinal from "./FlameFinal";
<!-- <Canvas><FlameFinal/></Canvas> --> <SquareCanvas><FlameFinal/></SquareCanvas>

View File

@ -1,35 +1,12 @@
import React, {useContext, useEffect, useMemo, useRef, useState} from "react"; import React, {useContext, useEffect, useMemo, useRef, useState} from "react";
import * as params from "../src/params"; import * as params from "../src/params";
import {InvertibleCanvas, PainterContext} from "../src/Canvas"; import {PainterContext} from "../src/Canvas";
import {colorFromPalette} from "./paintColor"; import {colorFromPalette} from "./paintColor";
import {chaosGameColor, ChaosGameColorProps, TransformColor} from "./chaosGameColor"; import {chaosGameColor, ChaosGameColorProps, TransformColor} from "./chaosGameColor";
import styles from "../src/css/styles.module.css"; import styles from "../src/css/styles.module.css";
import {histIndex} from "../src/camera"; import {histIndex} from "../src/camera";
import {useColorMode} from "@docusaurus/theme-common";
type AutoSizingCanvasProps = {
painter: (width: number, height: number) => ImageData;
}
const AutoSizingCanvas: React.FC<AutoSizingCanvasProps> = ({painter}) => {
const sizingRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);
const [height, setHeight] = useState<number>(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 (
<div ref={sizingRef} style={{width: '100%', height: '100%'}}>
<InvertibleCanvas width={width} height={height} image={image}/>
</div>
)
}
const paletteBarPainter = (palette: number[]) => const paletteBarPainter = (palette: number[]) =>
(width: number, height: number) => { (width: number, height: number) => {
@ -60,7 +37,7 @@ const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
return ( return (
<> <>
<div style={{width: '100%', height, marginTop: '1em', marginBottom: '1em'}}> <div style={{width: '100%', height, marginTop: '1em', marginBottom: '1em'}}>
<AutoSizingCanvas painter={painter}/> {/*<AutoSizingCanvas painter={painter}/>*/}
</div> </div>
{children} {children}
</> </>
@ -90,9 +67,13 @@ type ColorEditorProps = {
children?: React.ReactNode; children?: React.ReactNode;
} }
const ColorEditor: React.FC<ColorEditorProps> = ({title, palette, transformColor, setTransformColor, resetTransformColor, children}) => { const ColorEditor: React.FC<ColorEditorProps> = ({title, palette, transformColor, setTransformColor, resetTransformColor, children}) => {
const painter = useMemo(() => colorSwatchPainter(palette, transformColor.color), [palette, transformColor]);
const resetButton = <button className={styles.inputReset} onClick={resetTransformColor}>Reset</button> const resetButton = <button className={styles.inputReset} onClick={resetTransformColor}>Reset</button>
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 ( return (
<> <>
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '2fr 2fr 1fr'}}> <div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '2fr 2fr 1fr'}}>
@ -107,9 +88,12 @@ const ColorEditor: React.FC<ColorEditorProps> = ({title, palette, transformColor
<input type={'range'} min={0} max={1} step={.001} value={transformColor.colorSpeed} <input type={'range'} min={0} max={1} step={.001} value={transformColor.colorSpeed}
onInput={e => setTransformColor({...transformColor, colorSpeed: Number(e.currentTarget.value)})}/> onInput={e => setTransformColor({...transformColor, colorSpeed: Number(e.currentTarget.value)})}/>
</div> </div>
<div className={styles.inputElement}> <div className={styles.inputElement} style={{
<AutoSizingCanvas painter={painter}/> width: '100%',
</div> height: '100%',
backgroundColor: colorCss,
filter: colorMode === 'dark' ? 'invert(1)' : ''
}}/>
</div> </div>
{children} {children}
</> </>

View File

@ -28,11 +28,11 @@ import paintLinearSource from "!!raw-loader!./paintLinear"
<CodeBlock language="typescript">{paintLinearSource}</CodeBlock> <CodeBlock language="typescript">{paintLinearSource}</CodeBlock>
import Canvas from "../src/Canvas"; import {SquareCanvas} from "../src/Canvas";
import FlameHistogram from "./FlameHistogram"; import FlameHistogram from "./FlameHistogram";
import {paintLinear} from "./paintLinear"; import {paintLinear} from "./paintLinear";
<!-- <Canvas><FlameHistogram quality={5} paint={paintLinear}/></Canvas> --> <SquareCanvas><FlameHistogram quality={5} paint={paintLinear}/></SquareCanvas>
## Log display ## Log display
@ -42,7 +42,7 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic"
import {paintLogarithmic} from './paintLogarithmic' import {paintLogarithmic} from './paintLogarithmic'
<!-- <Canvas><FlameHistogram quality={5} paint={paintLogarithmic}/></Canvas> --> <SquareCanvas><FlameHistogram quality={10} paint={paintLogarithmic}/></SquareCanvas>
## Color ## Color
@ -52,4 +52,4 @@ import paintColorSource from "!!raw-loader!./paintColor"
import FlameColor from "./FlameColor"; import FlameColor from "./FlameColor";
<!-- <Canvas><FlameColor quality={15}/></Canvas> --> <SquareCanvas><FlameColor quality={15}/></SquareCanvas>