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.
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,
and we'll focus on the range $[0, 1]$ for both $x$ and $y$:
To simplify things, we'll assume that we're plotting a square image
with range $[0, 1]$ for both $x$ and $y$:
import cameraSource from "!!raw-loader!./cameraGasket"

View File

@ -13,7 +13,7 @@ type PaletteBarProps = {
palette: number[];
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 [width, setWidth] = useState(0);
useEffect(() => {
@ -51,10 +51,12 @@ const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
}
}, [canvasRef, paletteImage]);
const canvasStyle = {filter: useColorMode().colorMode === 'dark' ? 'invert(1)' : ''};
return (
<>
<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>
{children}
</>

View File

@ -6,13 +6,11 @@ authors: [bspeice]
tags: []
---
So far, our `plot()` function has been fairly simple; map an input coordinate
to a specific pixel, and color in that pixel.
This works well for simple function systems (like Sierpinski's Gasket),
So far, our `plot()` function has been fairly simple; map an input 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.
Every additional time we plot a pixel, we're wasting work.
Can we do something more intelligent instead?
In this post, we'll refine the image quality and add color to really make things shine.
<!-- 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
:::
To start with, it's worth demonstrating how much work is actually "wasted."
Previously, pixels would be either transparent or opaque depending on whether
we encountered them while running the chaos game.
To start, it's worth demonstrating how much work is actually "wasted"
when we treat pixels as a binary "on" (opaque) or "off" (transparent).
We'll render the reference image again, but this time, keep track of how many times
we encounter each pixel during the chaos game. At the end, we'll "paint" our final image
by setting each pixel's transparency based on how frequently we encounter it:
We'll render the reference image again, but this time, track each time
we encounter each pixel during the chaos game. When the chaos game finishes,
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";
@ -40,27 +39,40 @@ import {SquareCanvas} from "../src/Canvas";
import FlameHistogram from "./FlameHistogram";
import {paintLinear} from "./paintLinear";
<SquareCanvas><FlameHistogram quality={5} paint={paintLinear}/></SquareCanvas>
<SquareCanvas><FlameHistogram quality={15} paint={paintLinear}/></SquareCanvas>
## Log display
Using a histogram to paint our image is definitely a quality improvement,
but it produces "ghostly" images. In our reference parameters, the outer circle
While using a histogram to paint the image improves the quality,
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.
This technique is used in computer graphics to adjust for the fact that
people perceive brightness on a logarithmic scale.
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 color is perceived by people.
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,
you need a long exposure time. However, long exposures lead to bright spots that "wash out" and become nothing but white.
If we take multiple images using different exposure times, we can blend them together to create
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 images that "wash out" and become pure white.
By taking multiple pictures using different exposure times, we can combine them to create
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"
@ -68,10 +80,29 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic"
import {paintLogarithmic} from './paintLogarithmic'
<SquareCanvas><FlameHistogram quality={10} paint={paintLogarithmic}/></SquareCanvas>
<SquareCanvas><FlameHistogram quality={15} paint={paintLogarithmic}/></SquareCanvas>
## 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"
<CodeBlock language="typescript">{paintColorSource}</CodeBlock>

View File

@ -1,7 +1,7 @@
export function paintLogarithmic(width: number, height: number, histogram: number[]): ImageData {
const image = new ImageData(width, height);
const histogramLog = new Array<number>();
const histogramLog: number[] = [];
histogram.forEach(value => histogramLog.push(Math.log(value)));
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";
type PainterProps = {
@ -8,6 +8,13 @@ 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();
}
type CanvasProps = {
style?: any;
children?: React.ReactElement
@ -86,7 +93,7 @@ export const Canvas: React.FC<CanvasProps> = ({style, children}) => {
<>
<center>
<div ref={sizingRef} style={style}>
{width > 0 ? <canvas {...canvasProps}/> : null}
{width > 0 ? <canvas {...canvasProps} onDoubleClick={downloadImage}/> : null}
</div>
</center>
<PainterContext.Provider value={{width, height, setPainter}}>

View File

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