More writing. Hopefully can finish the first draft soon

This commit is contained in:
Bradlee Speice 2024-12-13 23:28:35 -05:00
parent b526b02e7b
commit 9b1a3895d0
6 changed files with 71 additions and 34 deletions

View File

@ -243,8 +243,8 @@ This allows us to manipulate individual pixels an image,
and display it on screen. and display it on screen.
First, we need to convert from Fractal Flame coordinates to pixel coordinates. 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, To simplify things, we'll assume that we're plotting a square image
and we'll focus on the range $[0, 1]$ for both $x$ and $y$: with range $[0, 1]$ for both $x$ and $y$:
import cameraSource from "!!raw-loader!./cameraGasket" import cameraSource from "!!raw-loader!./cameraGasket"

View File

@ -13,7 +13,7 @@ type PaletteBarProps = {
palette: number[]; palette: number[];
children?: React.ReactNode; children?: React.ReactNode;
} }
const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => { export const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
const sizingRef = useRef<HTMLDivElement>(null); const sizingRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
useEffect(() => { useEffect(() => {
@ -51,10 +51,12 @@ const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
} }
}, [canvasRef, paletteImage]); }, [canvasRef, paletteImage]);
const canvasStyle = {filter: useColorMode().colorMode === 'dark' ? 'invert(1)' : ''};
return ( return (
<> <>
<div ref={sizingRef} style={{width: '100%', height}}> <div ref={sizingRef} style={{width: '100%', height}}>
{width > 0 ? <canvas ref={canvasRef} width={width} height={height}/> : null} {width > 0 ? <canvas ref={canvasRef} width={width} height={height} style={canvasStyle}/> : null}
</div> </div>
{children} {children}
</> </>

View File

@ -6,13 +6,11 @@ authors: [bspeice]
tags: [] tags: []
--- ---
So far, our `plot()` function has been fairly simple; map an input coordinate So far, our `plot()` function has been fairly simple; map an input coordinate to a specific pixel,
to a specific pixel, and color in that pixel. and color in that pixel. This works well for simple function systems (like Sierpinski's Gasket),
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 our reference parameters) produce grainy images.
Every additional time we plot a pixel, we're wasting work. In this post, we'll refine the image quality and add color to really make things shine.
Can we do something more intelligent instead?
<!-- truncate --> <!-- truncate -->
@ -22,13 +20,14 @@ Can we do something more intelligent instead?
This post covers sections 4 and 5 of the Fractal Flame Algorithm paper This post covers sections 4 and 5 of the Fractal Flame Algorithm paper
::: :::
To start with, it's worth demonstrating how much work is actually "wasted." To start, it's worth demonstrating how much work is actually "wasted"
Previously, pixels would be either transparent or opaque depending on whether when we treat pixels as a binary "on" (opaque) or "off" (transparent).
we encountered them while running the chaos game.
We'll render the reference image again, but this time, keep track of how many times We'll render the reference image again, but this time, track each time
we encounter each pixel during the chaos game. At the end, we'll "paint" our final image we encounter each pixel during the chaos game. When the chaos game finishes,
by setting each pixel's transparency based on how frequently we encounter it: find the pixel we encountered most frequently. Finally, "paint" the image
by setting each pixel's transparency to ratio of times encountered
divided by the maximum value:
import CodeBlock from "@theme/CodeBlock"; import CodeBlock from "@theme/CodeBlock";
@ -40,27 +39,40 @@ import {SquareCanvas} from "../src/Canvas";
import FlameHistogram from "./FlameHistogram"; import FlameHistogram from "./FlameHistogram";
import {paintLinear} from "./paintLinear"; import {paintLinear} from "./paintLinear";
<SquareCanvas><FlameHistogram quality={5} paint={paintLinear}/></SquareCanvas> <SquareCanvas><FlameHistogram quality={15} paint={paintLinear}/></SquareCanvas>
## Log display ## Log display
Using a histogram to paint our image is definitely a quality improvement, While using a histogram to paint the image improves the quality,
but it produces "ghostly" images. In our reference parameters, the outer circle it also leads to some parts vanishing entirely.
In the reference parameters, the outer circle
is preserved, but the interior appears to be missing! 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. 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 technique is used in computer graphics to adjust for the fact that This is a technique used in computer graphics to compensate for differences in how
people perceive brightness on a logarithmic scale. computers represent color, and how color is perceived by people.
As a concrete example, high dynamic range (HDR) photography uses this technique to capture As a concrete example, high dynamic range (HDR) photography uses this technique to capture
nice images of scenes with very different brightness levels. If you want to take a picture of something dark, nice images of scenes with wide brightness ranges. To take a picture of something dark,
you need a long exposure time. However, long exposures lead to bright spots that "wash out" and become nothing but white. you need a long exposure time. However, long exposures can lead to images that "wash out" and become pure white.
If we take multiple images using different exposure times, we can blend them together to create By taking multiple pictures using different exposure times, we can combine them to create
a final image where everything is visible. a final image where everything is visible.
TODO: HDR link? 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.
In fractal flames, this "tone map" is accomplished <details>
<summary>Log-scale vibrancy is also why fractal flames appear to be 3D...</summary>
As explained in the Fractal Flame 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.
> For example, branches of densities 1000 and 100 might have brightnesses of 30 and 20.
> Where they cross the density is 1100, whose brightness is 30.4, which is
> hardly distinguishable from 30.
</details>
import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic" import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic"
@ -68,10 +80,29 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic"
import {paintLogarithmic} from './paintLogarithmic' import {paintLogarithmic} from './paintLogarithmic'
<SquareCanvas><FlameHistogram quality={10} paint={paintLogarithmic}/></SquareCanvas> <SquareCanvas><FlameHistogram quality={15} paint={paintLogarithmic}/></SquareCanvas>
## Color ## Color
Finally, we'll spice things up with the last innovation introduced by
the fractal flame algorithm: color. By including a color coordinate
in the chaos game, we can illustrate the transforms that are responsible
for each part of an image.
### Palette
Our first step is to define a color palette for the image. Fractal flames
typically use a palette of 256 colors that transition smoothly
from one to another. In the diagram below, each color in our palette is plotted
on a small strip. Putting the strips side by side shows the palette for our image:
import * as params from "../src/params"
import {PaletteBar} from "./FlameColor"
<PaletteBar height="40" palette={params.palette}/>
### Color coordinate
import paintColorSource from "!!raw-loader!./paintColor" import paintColorSource from "!!raw-loader!./paintColor"
<CodeBlock language="typescript">{paintColorSource}</CodeBlock> <CodeBlock language="typescript">{paintColorSource}</CodeBlock>

View File

@ -1,7 +1,7 @@
export function paintLogarithmic(width: number, height: number, histogram: number[]): ImageData { export function paintLogarithmic(width: number, height: number, histogram: number[]): ImageData {
const image = new ImageData(width, height); const image = new ImageData(width, height);
const histogramLog = new Array<number>(); const histogramLog: number[] = [];
histogram.forEach(value => histogramLog.push(Math.log(value))); histogram.forEach(value => histogramLog.push(Math.log(value)));
let histogramLogMax = -Infinity; let histogramLogMax = -Infinity;

View File

@ -1,4 +1,4 @@
import React, {useEffect, useState, createContext, useRef} from "react"; import React, {useEffect, useState, createContext, useRef, MouseEvent} from "react";
import {useColorMode} from "@docusaurus/theme-common"; import {useColorMode} from "@docusaurus/theme-common";
type PainterProps = { type PainterProps = {
@ -8,6 +8,13 @@ type PainterProps = {
} }
export const PainterContext = createContext<PainterProps>(null) 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();
}
type CanvasProps = { type CanvasProps = {
style?: any; style?: any;
children?: React.ReactElement children?: React.ReactElement
@ -86,7 +93,7 @@ export const Canvas: React.FC<CanvasProps> = ({style, children}) => {
<> <>
<center> <center>
<div ref={sizingRef} style={style}> <div ref={sizingRef} style={style}>
{width > 0 ? <canvas {...canvasProps}/> : null} {width > 0 ? <canvas {...canvasProps} onDoubleClick={downloadImage}/> : null}
</div> </div>
</center> </center>
<PainterContext.Provider value={{width, height, setPainter}}> <PainterContext.Provider value={{width, height, setPainter}}>

View File

@ -99,11 +99,8 @@ const config: Config = {
plugins: [require.resolve('docusaurus-lunr-search')], plugins: [require.resolve('docusaurus-lunr-search')],
stylesheets: [ stylesheets: [
{ {
href: 'https://cdn.jsdelivr.net/npm/katex@0.13.24/dist/katex.min.css', href: '/katex/katex.min.css',
type: 'text/css', type: 'text/css',
integrity:
'sha384-odtC+0UGzzFL/6PNoE8rX/SPcQDXBJ+uRepguP4QkPCm2LBxH3FA3y+fKSiJ+AmM',
crossorigin: 'anonymous',
}, },
], ],
future: { future: {