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}) {
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;
}

View File

@ -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

View File

@ -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'
<!-- 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 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 &mdash; and others interested in the art &mdash;
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:
<!-- 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";
export const simpleData = [
{x: 0, y: 0},
@ -63,8 +64,18 @@ export const simpleData = [
<VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/>
</VictoryChart>
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'
<hr/>
<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>
## Weights

View File

@ -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

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>
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>
<div className={styles.inputElement}>
<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 {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);

View File

@ -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";

View File

@ -18,7 +18,7 @@ export const VariationEditor = ({title, variations, setVariations, resetVariatio
const resetButton = <button className={styles.inputReset} onClick={resetVariations}>Reset</button>
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>
<div className={styles.inputElement}>
<span>Linear: {variations.linear}</span>

View File

@ -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);

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
each transform.
import Canvas from "../src/Canvas";
import {SquareCanvas} from "../src/Canvas";
import FlameBlend from "./FlameBlend";
<!-- <Canvas><FlameBlend/></Canvas> -->
<SquareCanvas><FlameBlend/></SquareCanvas>
## Post transforms
@ -160,10 +160,10 @@ $$
import FlamePost from "./FlamePost";
<!-- <Canvas><FlamePost/></Canvas> -->
<SquareCanvas><FlamePost/></SquareCanvas>
## Final transform
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 * 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<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>
)
}
import {useColorMode} from "@docusaurus/theme-common";
const paletteBarPainter = (palette: number[]) =>
(width: number, height: number) => {
@ -60,7 +37,7 @@ const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
return (
<>
<div style={{width: '100%', height, marginTop: '1em', marginBottom: '1em'}}>
<AutoSizingCanvas painter={painter}/>
{/*<AutoSizingCanvas painter={painter}/>*/}
</div>
{children}
</>
@ -90,9 +67,13 @@ type ColorEditorProps = {
children?: React.ReactNode;
}
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 [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 (
<>
<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}
onInput={e => setTransformColor({...transformColor, colorSpeed: Number(e.currentTarget.value)})}/>
</div>
<div className={styles.inputElement}>
<AutoSizingCanvas painter={painter}/>
</div>
<div className={styles.inputElement} style={{
width: '100%',
height: '100%',
backgroundColor: colorCss,
filter: colorMode === 'dark' ? 'invert(1)' : ''
}}/>
</div>
{children}
</>

View File

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