mirror of
				https://github.com/bspeice/speice.io
				synced 2025-10-31 09:30:32 -04:00 
			
		
		
		
	First pass proof-reading
This commit is contained in:
		| @ -14,7 +14,7 @@ export function Render({f}) { | ||||
|  | ||||
| export default function Gasket({f}) { | ||||
|     return ( | ||||
|         <SquareCanvas> | ||||
|         <SquareCanvas name={"gasket"}> | ||||
|             <Render f={f}/> | ||||
|         </SquareCanvas> | ||||
|     ) | ||||
|  | ||||
| @ -31,7 +31,7 @@ export default function GasketWeighted() { | ||||
|         <> | ||||
|             <div className={styles.inputElement}> | ||||
|                 <p><TeX>{title}</TeX>: {weight}</p> | ||||
|                 <input type={'range'} min={0} max={1} step={.01} style={{width: '100%', background: 'transparent'}} value={weight} | ||||
|                 <input type={'range'} min={0} max={1} step={.01} value={weight} | ||||
|                     onInput={e => setWeight(Number(e.currentTarget.value))}/> | ||||
|             </div> | ||||
|         </> | ||||
|  | ||||
| @ -7,11 +7,11 @@ tags: [] | ||||
| --- | ||||
|  | ||||
|  | ||||
| Wikipedia describes fractal flames fractal flames as: | ||||
| Wikipedia describes [fractal flames](https://en.wikipedia.org/wiki/Fractal_flame) as: | ||||
|  | ||||
| > a member of the iterated function system class of fractals | ||||
|  | ||||
| It's a bit tedious, but technically correct. I choose to think of them a different way: beauty in mathematics. | ||||
| 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' | ||||
| @ -22,14 +22,15 @@ import banner from '../banner.png' | ||||
|  | ||||
| <!-- truncate --> | ||||
|  | ||||
| I don't remember exactly when I first learned about 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 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 original [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, maybe I can make some progress. | ||||
| But the desire to understand it stuck around. Now, with a graduate degree under my belt, I want to revisit it | ||||
| and try to make some progress. | ||||
|  | ||||
| This guide is my attempt to explain fractal flames in a way that younger me — and others interested in the art — | ||||
| This guide is my attempt to explain how fractal flames work in a way that younger me — and others interested in the art — | ||||
| can understand without too much prior knowledge. | ||||
|  | ||||
| --- | ||||
| @ -41,7 +42,7 @@ 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 will take some time to unpack: | ||||
| or IFS. The formula for an IFS is short, but takes some time to unpack: | ||||
|  | ||||
| $$ | ||||
| S = \bigcup_{i=0}^{n-1} F_i(S) | ||||
| @ -69,15 +70,15 @@ 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. 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 change the functions, our solution changes, and we'll get a new picture. | ||||
| And if the functions change, the solution also changes, and we get a new picture. | ||||
|  | ||||
| ### Transform 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)." | ||||
| While you could theoretically use any function, we'll start with a specific kind of function | ||||
| called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)." | ||||
|  | ||||
| The general form of an affine transformation is: | ||||
|  | ||||
| @ -102,7 +103,7 @@ 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) | ||||
| F_{shift}(x, y) &= (x + 0.5, y + 1.5) | ||||
| \end{align*} | ||||
| $$ | ||||
|  | ||||
| @ -139,7 +140,7 @@ $$ | ||||
| S = \bigcup_{i=0}^{n-1} F_i(S) | ||||
| $$ | ||||
|  | ||||
| Or, to put it in English, we would get something like this: | ||||
| Or, to put it 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. | ||||
| @ -154,40 +155,41 @@ explaining the mathematics of iterated function systems: | ||||
| > of finite compositions $F_{i_1...i_p}$ of members of $F$. | ||||
|  | ||||
| :::note | ||||
| I've tweaked the wording slightly to match the conventions in the Fractal Flame paper | ||||
| I've tweaked the wording slightly to match the conventions of the Fractal Flame paper | ||||
| ::: | ||||
|  | ||||
| Before your eyes glaze over, let's unpack this explanation: | ||||
| Before your eyes glaze over, let's unpack this: | ||||
|  | ||||
| - **$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 these points does not change them | ||||
|   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 function), we will arrive at the points we care about | ||||
|   using the output of one function as input to the next function), we will arrive at the points in the solution | ||||
|  | ||||
| Thus, by applying the functions in our system to "fixed points," we will find the other points we care about. | ||||
|  | ||||
| However, this is all a bit vague, so let's work through an example. | ||||
| Thus, by applying the functions to fixed points of our system, we will find the other points we care about. | ||||
|  | ||||
| <details> | ||||
|     <summary>If you want a bit more math first...</summary> | ||||
|     <summary>If you want a bit more math...</summary> | ||||
|  | ||||
|     ...then there are some details worth mentioning that I've glossed over so far. | ||||
|     ...then there are some extra details I've glossed over so far. | ||||
|  | ||||
|     First, the Hutchinson paper requires that the functions $F_i$ be _contractive_ tor the solution set to exist. | ||||
|     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 $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper | ||||
|     allows for arbitrary dimensions - which means you could also have 3-dimensional fractal flames. | ||||
|     allows for arbitrary dimensions; you could also have 3-dimensional fractal flames. | ||||
|  | ||||
|     TODO: Mention attractors? https://en.wikipedia.org/wiki/Chaos_game | ||||
|     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 in detail below). | ||||
| </details> | ||||
|  | ||||
| ## Sierpinski's gasket | ||||
| This is still a bit vague, so let's work through an example. | ||||
|  | ||||
| The Fractal Flame paper gives us three functions we can use for our function system: | ||||
| ## [Sierpinski's gasket](https://www.britannica.com/biography/Waclaw-Sierpinski) | ||||
|  | ||||
| The Fractal Flame paper gives us three functions to use for our first IFS: | ||||
|  | ||||
| $$ | ||||
| F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\ | ||||
| @ -200,7 +202,7 @@ $$ | ||||
| ### The chaos game | ||||
|  | ||||
| Next, how do we find the "fixed points" we mentioned earlier? The paper lays out an algorithm called the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)" | ||||
| that will give us points in the solution set. | ||||
| that gives us points in the solution set: | ||||
|  | ||||
| $$ | ||||
| \begin{align*} | ||||
| @ -214,12 +216,13 @@ $$ | ||||
| $$ | ||||
|  | ||||
| :::note | ||||
| In effect, the chaos game algorithm implements the "finite compositions of $F_{i_1..i_p}$ mentioned earlier. | ||||
| The chaos game algorithm is effectively the "finite compositions of $F_{i_1..i_p}$" mentioned earlier. | ||||
| ::: | ||||
|  | ||||
| Now, let's turn this into code, one piece at a time. | ||||
|  | ||||
| First, the "bi-unit square" is the range $[-1, 1]$. We can : | ||||
| First, we need to generate some random numbers. The "bi-unit square" is the range $[-1, 1]$, | ||||
| so we generate a random point using an existing API: | ||||
|  | ||||
| import biunitSource from '!!raw-loader!../src/randomBiUnit' | ||||
|  | ||||
| @ -233,14 +236,12 @@ import randintSource from '!!raw-loader!../src/randomInteger' | ||||
|  | ||||
| ### 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. | ||||
| Finally, implementing the `plot` function. This blog series is designed to be 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 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. | ||||
| To show 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 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 | ||||
| @ -250,10 +251,9 @@ import cameraSource from "!!raw-loader!./cameraGasket" | ||||
|  | ||||
| <CodeBlock language="typescript">{cameraSource}</CodeBlock> | ||||
|  | ||||
| Next, we'll use an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) | ||||
| to store the pixel data. | ||||
| Next, we'll store the pixel data in an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData). | ||||
| 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: | ||||
| To plot a point, we set that pixel to be black: | ||||
|  | ||||
| import plotSource from '!!raw-loader!./plot' | ||||
|  | ||||
| @ -271,18 +271,16 @@ import chaosGameSource from '!!raw-loader!./chaosGame' | ||||
| <hr/> | ||||
|  | ||||
| <small> | ||||
| 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). | ||||
|     The image here is slightly different than in the paper. | ||||
|     I think the paper has an error, so I'm choosing to plot the image | ||||
|     the same way as the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441). | ||||
| </small> | ||||
|  | ||||
| ### Weights | ||||
|  | ||||
| Finally, we'll introduce a "weight" ($w_i$) for each function that controls how often we choose | ||||
| that function during the chaos game relative to each other function. | ||||
|  | ||||
| For Sierpinski's Gasket, we start with equal weighting, | ||||
| but you can see how changing the function weights affects the image below: | ||||
| There's one last step before we finish the introduction. So far, each function $F_i$ has | ||||
| the same chance of being chosen. We can change that by introducing a "weight" ($w_i$) | ||||
| to each transform in the chaos game: | ||||
|  | ||||
| import randomChoiceSource from '!!raw-loader!../src/randomChoice' | ||||
|  | ||||
| @ -292,16 +290,23 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted"; | ||||
|  | ||||
| <CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock> | ||||
|  | ||||
| For Sierpinski's Gasket, we start with equal weighting, | ||||
| but changing the transform weights affects how often | ||||
| the chaos game "visits" parts of the image: | ||||
|  | ||||
| :::tip | ||||
| Double-click the image if you want to save a copy! | ||||
| ::: | ||||
|  | ||||
| import GasketWeighted from "./GasketWeighted"; | ||||
| import {SquareCanvas} from "../src/Canvas"; | ||||
|  | ||||
| <SquareCanvas><GasketWeighted/></SquareCanvas> | ||||
| <SquareCanvas name={"gasket_weighted"}><GasketWeighted/></SquareCanvas> | ||||
|  | ||||
| ## Summary | ||||
|  | ||||
| Studying the foundations of fractal flames is challenging, | ||||
| but we now have an understanding of both the mathematics | ||||
| and implementation of iterated function systems. | ||||
| and implementations of iterated function systems. | ||||
|  | ||||
| In the next post, we'll study the first innovation that fractal flames | ||||
| bring: variations. | ||||
| In the next post, we'll study the first innovation of fractal flames: variations. | ||||
| @ -12,9 +12,9 @@ shapes and patterns that fractal flames are known for. | ||||
| <!-- truncate --> | ||||
|  | ||||
| :::info | ||||
| This post uses a set of [reference parameters](../params.flame) to demonstrate the fractal flame algorithm. | ||||
| This post uses [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! | ||||
| can load that file to play around with! | ||||
| ::: | ||||
|  | ||||
| ## Variations | ||||
| @ -30,8 +30,8 @@ playing the chaos game leads to an image of Sierpinski's Gasket. Even though we | ||||
| the image it generates is exciting. But it's still not nearly as exciting as the images the Fractal Flame | ||||
| algorithm is known for. | ||||
|  | ||||
| This leads us to the first big innovation of the Fractal Flame algorithm: applying non-linear functions | ||||
| after the affine transform has happened. These functions are called "variations": | ||||
| 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 \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i) | ||||
| @ -41,8 +41,9 @@ import variationSource from '!!raw-loader!../src/variation' | ||||
|  | ||||
| <CodeBlock language="typescript">{variationSource}</CodeBlock> | ||||
|  | ||||
| Just like transforms, variations ($V_j$) are functions that map $(x, y)$ coordinates to new coordinates. | ||||
| However, the sky is the limit for what we can do in between input and output. | ||||
| 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 different variation functions, | ||||
| and the official `flam3` implementation supports [98 different variations](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c). | ||||
|  | ||||
| @ -70,8 +71,6 @@ apply the affine coefficients to the input point, and use that as the output poi | ||||
| This variation is a good example of the non-linear functions we can use. It uses both trigonometry | ||||
| and probability to produce interesting shapes: | ||||
|  | ||||
| TODO: Connection with the julia set? | ||||
|  | ||||
| $$ | ||||
| \begin{align*} | ||||
| r &= \sqrt{x^2 + y^2} \\ | ||||
| @ -93,7 +92,7 @@ import juliaSrc from '!!raw-loader!../src/julia' | ||||
|  | ||||
| ### Popcorn (variation 17) | ||||
|  | ||||
| Some variations rely on knowing their transform's affine coefficients; they're known as "dependent variations." | ||||
| Some variations rely on knowing the transform's affine coefficients; they're called "dependent variations." | ||||
| For the popcorn variation, we use $c$ and $f$: | ||||
|  | ||||
| $$ | ||||
| @ -106,7 +105,7 @@ import popcornSrc from '!!raw-loader!../src/popcorn' | ||||
|  | ||||
| ### PDJ (variation 24) | ||||
|  | ||||
| Some variations have extra parameters we can choose; these are known as "parametric variations." | ||||
| Some variations have extra parameters; they're called "parametric variations." | ||||
| For the PDJ variation, there are four extra parameters we can choose: | ||||
|  | ||||
| $$ | ||||
| @ -122,7 +121,7 @@ import pdjSrc from '!!raw-loader!../src/pdj' | ||||
|  | ||||
| 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 (called $v_{ij}$) that changes how much it contributes to the transform: | ||||
| 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(a_i \cdot x + b_i \cdot y + c_i, \hspace{0.2cm} d_i \cdot x + e_i \cdot y + f_i) | ||||
| @ -144,11 +143,12 @@ Try using the variation weight sliders to figure out which parts of the image ea | ||||
| import {SquareCanvas} from "../src/Canvas"; | ||||
| import FlameBlend from "./FlameBlend"; | ||||
|  | ||||
| <SquareCanvas><FlameBlend/></SquareCanvas> | ||||
| <SquareCanvas name={"flame_blend"}><FlameBlend/></SquareCanvas> | ||||
|  | ||||
| ## Post transforms | ||||
|  | ||||
| Next, we'll introduce a second affine transform, known as a post transform, that is applied _after_ variation blending. | ||||
| 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 function should look familiar: | ||||
|  | ||||
| $$ | ||||
| @ -163,19 +163,18 @@ import postSource from '!!raw-loader!./post' | ||||
| <CodeBlock language="typescript">{postSource}</CodeBlock> | ||||
|  | ||||
| The image below uses the same transforms/variations as the previous fractal flame, | ||||
| but allows modifying the post-transform coefficients. | ||||
| but allows modifying the post-transform coefficients: | ||||
|  | ||||
| <details> | ||||
|     <summary>If you want to test your understanding...</summary> | ||||
|  | ||||
|     Challenge 1: What post-transform coefficients will give us the previous image? | ||||
|  | ||||
|     Challenge 2: What post-transform coefficients will give us a _mirrored_ image? | ||||
|     - What post-transform coefficients will give us the previous image? | ||||
|     - What post-transform coefficients will give us a _mirrored_ image? | ||||
| </details> | ||||
|  | ||||
| import FlamePost from "./FlamePost"; | ||||
|  | ||||
| <SquareCanvas><FlamePost/></SquareCanvas> | ||||
| <SquareCanvas name={"flame_post"}><FlamePost/></SquareCanvas> | ||||
|  | ||||
| ## Final transforms | ||||
|  | ||||
| @ -204,12 +203,12 @@ import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal" | ||||
|  | ||||
| import FlameFinal from "./FlameFinal"; | ||||
|  | ||||
| <SquareCanvas><FlameFinal/></SquareCanvas> | ||||
| <SquareCanvas name={"flame_final"}><FlameFinal/></SquareCanvas> | ||||
|  | ||||
| ## Summary | ||||
|  | ||||
| Variations are the fractal flame algorithm's first major innovation over previous IFS's. | ||||
| Blending variation functions and post/final transforms allows us to generate interesting images. | ||||
| Variations are the fractal flame algorithm's first major innovation. | ||||
| By blending variation functions, and post/final transforms, we generate unique images. | ||||
|  | ||||
| However, the images themselves are grainy and unappealing. In the next post, we'll clean up | ||||
| the image quality and add some color. | ||||
| @ -6,9 +6,9 @@ authors: [bspeice] | ||||
| tags: [] | ||||
| --- | ||||
|  | ||||
| So far, our `plot()` function has been fairly simple; map an input coordinate to a specific pixel, | ||||
| So far, our `plot()` function has been fairly simple; map a fractal flame coordinate to a specific pixel, | ||||
| and color in that pixel. This works well for simple function systems (like Sierpinski's Gasket), | ||||
| but more complex systems (like our reference parameters) produce grainy images. | ||||
| but more complex systems (like the reference parameters) produce grainy images. | ||||
|  | ||||
| In this post, we'll refine the image quality and add color to really make things shine. | ||||
|  | ||||
| @ -20,20 +20,21 @@ In this post, we'll refine the image quality and add color to really make things | ||||
| This post covers sections 4 and 5 of the Fractal Flame Algorithm paper | ||||
| ::: | ||||
|  | ||||
| To start, it's worth demonstrating how much work is actually "wasted" | ||||
| when we treat pixels as a binary "on" (opaque) or "off" (transparent). | ||||
| One problem with the existing chaos game is that we waste work | ||||
| by treating pixels as a binary "on" (opaque) or "off" (transparent). | ||||
| If the chaos game encounters the same location twice, nothing actually changes. | ||||
|  | ||||
| We'll render the reference image again, but this time, count each time | ||||
| we encounter a pixel during the chaos game. This gives us a kind of "histogram" | ||||
| of the image: | ||||
| To demonstrate how much work is wasted, we'll render the reference image again. | ||||
| However, we'll also count each time the chaos game encounters a pixel. | ||||
| This gives us a kind of image "histogram": | ||||
|  | ||||
| import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram" | ||||
|  | ||||
| <CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock> | ||||
|  | ||||
| When the chaos game finishes, find the pixel we encountered most frequently. | ||||
| Finally, "paint" the image by setting each pixel's alpha value (transparency) | ||||
| to the ratio of times encountered, divided by the maximum value: | ||||
| When the chaos game finishes, we find the pixel encountered most frequently. | ||||
| Finally, we "paint" the image by setting each pixel's alpha value (transparency) | ||||
| to the ratio of times encountered divided by the maximum: | ||||
|  | ||||
| import CodeBlock from "@theme/CodeBlock"; | ||||
|  | ||||
| @ -49,27 +50,27 @@ import {paintLinear} from "./paintLinear"; | ||||
|  | ||||
| ## Tone mapping | ||||
|  | ||||
| While using a histogram to paint the image improves the quality, it also leads to some parts vanishing entirely. | ||||
| While using a histogram reduces the "graininess" of the image, it also leads to some parts vanishing entirely. | ||||
| In the reference parameters, the outer circle is preserved, but the interior appears to be missing! | ||||
|  | ||||
| To fix this, we'll introduce the second major innovation of the fractal flame algorithm: [tone mapping](https://en.wikipedia.org/wiki/Tone_mapping). | ||||
| This is a technique used in computer graphics to compensate for differences in how | ||||
| computers represent color, and how people see color. | ||||
| computers represent brightness, and how people see brightness. | ||||
|  | ||||
| As a concrete example, high dynamic range (HDR) photography uses this technique to capture | ||||
| nice images of scenes with wide brightness ranges. To take a picture of something dark, | ||||
| you need a long exposure time. However, long exposures can lead to "hot spots" in images that are pure white. | ||||
| By taking multiple pictures using different exposure times, we can combine them to create | ||||
| nice images of scenes with a wide range of brightnesses. To take a picture of something dark, | ||||
| you need a long exposure time. However, long exposures lead to "hot spots" (sections that are pure white). | ||||
| By taking multiple pictures with different exposure times, we can combine them to create | ||||
| a final image where everything is visible. | ||||
|  | ||||
| In fractal flames, this "tone map" is accomplished by scaling brightness according to the _logarithm_ | ||||
| of how many times we encounter a pixel. This way, "dark spots" (pixels the chaos game visits infrequently) | ||||
| will still be visible, and "bright spots" (pixels the chaos game visits frequently) won't wash out. | ||||
| of how many times we encounter a pixel. This way, "cold spots" (pixels the chaos game visits infrequently) | ||||
| will still be visible, and "hot spots" (pixels the chaos game visits frequently) won't wash out. | ||||
|  | ||||
| <details> | ||||
|     <summary>Log-scale vibrancy is also why fractal flames appear to be 3D...</summary> | ||||
|     <summary>Log-scale vibrancy also explains fractal flames appear to be 3D...</summary> | ||||
|  | ||||
|     As explained in the Fractal Flame paper: | ||||
|     As mentioned in the paper: | ||||
|  | ||||
|     > Where one branch of the fractal crosses another, one may appear to occlude the other | ||||
|     > if their densities are different enough because the lesser density is inconsequential in sum. | ||||
| @ -89,15 +90,17 @@ import {paintLogarithmic} from './paintLogarithmic' | ||||
| ## Color | ||||
|  | ||||
| Finally, we'll introduce the last innovation of the fractal flame algorithm: color. | ||||
| By including a color coordinate ($c$) in the chaos game, we can illustrate the transforms | ||||
| responsible for each part of the image. | ||||
| By including a third coordinate ($c$) in the chaos game, we can illustrate the transforms | ||||
| responsible for the image. | ||||
|  | ||||
| ### Color coordinate | ||||
|  | ||||
| Color in a fractal flame uses a range of $[0, 1]$. This is important for two reasons: | ||||
| Color in a fractal flame is continuous on the range $[0, 1]$. This is important for two reasons: | ||||
|  | ||||
| - It helps blend colors together in the final image | ||||
| - It allows us to swap in new color palettes easily | ||||
| - It helps blend colors together in the final image. Slight changes in the color value lead to | ||||
|   slight changes in the actual color | ||||
| - It allows us to swap in new color palettes easily. We're free to choose what actual colors | ||||
|   each color value represents | ||||
|  | ||||
| We'll give each transform a color value ($c_i$) in the $[0, 1]$ range. | ||||
| Then, at each step in the chaos game, we'll set the current color | ||||
| @ -139,14 +142,14 @@ import mixColorSource from "!!raw-loader!./mixColor" | ||||
|  | ||||
| Color speed values work just like transform weights. A value of 1 | ||||
| means we take the transform color and ignore the previous color state. | ||||
| Similarly, a value of 0 means we keep the current color state and ignore the | ||||
| A value of 0 means we keep the current color state and ignore the | ||||
| transform color. | ||||
|  | ||||
| ### Palette | ||||
|  | ||||
| Now, we need to map the color coordinate to a pixel color. Fractal flames typically use | ||||
| 256 colors (each color has 3 values - red, green, blue) to define a palette. | ||||
| Then, the color coordinate becomes an index into the palette. | ||||
| The color coordinate then becomes an index into the palette. | ||||
|  | ||||
| There's one small complication: the color coordinate is continuous, but the palette | ||||
| uses discrete colors. How do we handle situations where the color coordinate is | ||||
| @ -163,11 +166,11 @@ import colorFromPaletteSource from "!!raw-loader!./colorFromPalette"; | ||||
|     <summary>As an alternative...</summary> | ||||
|  | ||||
|     ...you could also interpolate between colors in the palette. | ||||
|     For example: [`flam3` code](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486) | ||||
|     For example, `flam3` uses [linear interpolation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486) | ||||
| </details> | ||||
|  | ||||
| In the diagram below, each color in our palette is plotted on a small vertical strip. | ||||
| Putting the strips side by side shows the palette used by our reference image: | ||||
| Putting the strips side by side shows the full palette used by the reference parameters: | ||||
|  | ||||
| import * as params from "../src/params" | ||||
| import {PaletteBar} from "./FlameColor" | ||||
| @ -177,14 +180,15 @@ import {PaletteBar} from "./FlameColor" | ||||
| ### Plotting | ||||
|  | ||||
| We're now ready to plot our $(x_f,y_f,c_f)$ coordinates. After translating from color coordinate ($c_f$) | ||||
| to RGB value, add that value to the image histogram: | ||||
| to RGB value, add that to the image histogram: | ||||
|  | ||||
| import chaosGameColorSource from "!!raw-loader!./chaosGameColor" | ||||
|  | ||||
| <CodeBlock language="typescript">{chaosGameColorSource}</CodeBlock> | ||||
|  | ||||
| Finally, painting the image. With tone mapping, logarithms scale the image brightness to match | ||||
| how it is perceived. When using color, we scale each color channel by the alpha channel: | ||||
| how it is perceived. With color, we use a similar method, but scale each color channel | ||||
| by the alpha channel: | ||||
|  | ||||
| import paintColorSource from "!!raw-loader!./paintColor" | ||||
|  | ||||
| @ -206,10 +210,10 @@ Next, introducing a third coordinate to the chaos game makes color images possib | ||||
| the third major innovation of the fractal flame algorithm. Using a continuous | ||||
| color scale and color palette adds a splash of color to our transforms. | ||||
|  | ||||
| The Fractal Flame Algorithm paper goes on to describe more techniques | ||||
| not covered here. Image quality can be improved with density estimation | ||||
| The Fractal Flame Algorithm paper does go on to describe more techniques | ||||
| not covered here. For example, Image quality can be improved with density estimation | ||||
| and filtering. New parameters can be generated by "mutating" existing | ||||
| fractal flames. Fractal flames can even be animated to produce videos! | ||||
| fractal flames. And fractal flames can even be animated to produce videos! | ||||
|  | ||||
| That said, I think this is a good place to wrap up. We were able to go from | ||||
| an introduction to the mathematics of fractal systems all the way to | ||||
|  | ||||
| @ -8,18 +8,20 @@ type PainterProps = { | ||||
| } | ||||
| export const PainterContext = createContext<PainterProps>(null) | ||||
|  | ||||
| const downloadImage = (e: MouseEvent) => { | ||||
|     const link = document.createElement("a"); | ||||
|     link.download = "flame.png"; | ||||
|     link.href = (e.target as HTMLCanvasElement).toDataURL("image/png"); | ||||
|     link.click(); | ||||
| } | ||||
| const downloadImage = (name: string) => | ||||
|     (e: MouseEvent) => { | ||||
|         const link = document.createElement("a"); | ||||
|         link.download = "flame.png"; | ||||
|         link.href = (e.target as HTMLCanvasElement).toDataURL("image/png"); | ||||
|         link.click(); | ||||
|     } | ||||
|  | ||||
| type CanvasProps = { | ||||
|     name: string; | ||||
|     style?: any; | ||||
|     children?: React.ReactElement | ||||
| } | ||||
| export const Canvas: React.FC<CanvasProps> = ({style, children}) => { | ||||
| export const Canvas: React.FC<CanvasProps> = ({name, style, children}) => { | ||||
|     const sizingRef = useRef<HTMLDivElement>(null); | ||||
|     const [width, setWidth] = useState(0); | ||||
|     const [height, setHeight] = useState(0); | ||||
| @ -93,7 +95,7 @@ export const Canvas: React.FC<CanvasProps> = ({style, children}) => { | ||||
|         <> | ||||
|             <center> | ||||
|                 <div ref={sizingRef} style={style}> | ||||
|                     {width > 0 ? <canvas {...canvasProps} onDoubleClick={downloadImage}/> : null} | ||||
|                     {width > 0 ? <canvas {...canvasProps} onDoubleClick={downloadImage(name)}/> : null} | ||||
|                 </div> | ||||
|             </center> | ||||
|             <PainterContext.Provider value={{width, height, setPainter}}> | ||||
| @ -103,6 +105,6 @@ export const Canvas: React.FC<CanvasProps> = ({style, children}) => { | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export const SquareCanvas: React.FC<CanvasProps> = ({style, children}) => { | ||||
|     return <center><Canvas style={{width: '75%', aspectRatio: '1/1', ...style}} children={children}/></center> | ||||
| export const SquareCanvas: React.FC<CanvasProps> = ({name, style, children}) => { | ||||
|     return <center><Canvas name={name} style={{width: '75%', aspectRatio: '1/1', ...style}} children={children}/></center> | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| .inputGroup { | ||||
|     padding: .2em; | ||||
|     margin-top: .2em; | ||||
|     margin-bottom: .2em; | ||||
|     margin-top: .5em; | ||||
|     margin-bottom: .5em; | ||||
|     border: 1px solid; | ||||
|     border-radius: var(--ifm-global-radius); | ||||
|     border-color: var(--ifm-color-emphasis-500); | ||||
| @ -25,6 +25,10 @@ | ||||
|  | ||||
| .inputElement > input[type=range] { | ||||
|     width: 100%; | ||||
|     height: 4px; | ||||
|     appearance: none; | ||||
|     background: var(--ifm-color-emphasis-400); | ||||
|     border-radius: var(--ifm-global-radius); | ||||
| } | ||||
|  | ||||
| .inputReset { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user