Flame blending example

This commit is contained in:
Bradlee Speice 2024-11-29 19:25:29 -05:00
parent 137bd74d4d
commit ce5a28b7bd
16 changed files with 269 additions and 54 deletions

View File

@ -3,16 +3,9 @@ import Canvas from "../src/Canvas";
import { Params, chaosGameWeighted } from "./chaosGameWeighted"; import { Params, chaosGameWeighted } from "./chaosGameWeighted";
import TeX from '@matejmazur/react-katex'; import TeX from '@matejmazur/react-katex';
type Transform = (x: number, y: number) => [number, number]; import styles from "../src/css/styles.module.css"
function WeightInput({value, setValue, children}) { type Transform = (x: number, y: number) => [number, number];
return (
<div style={{paddingLeft: '1.5em', paddingRight: '1.5em'}}>
{children}
<input type={'range'} min={0} max={1} step={.01} style={{width: '100%'}} value={value} onInput={e => setValue(Number(e.currentTarget.value))}/>
</div>
)
}
export default function GasketWeighted() { export default function GasketWeighted() {
const image = new ImageData(600, 600); const image = new ImageData(600, 600);
@ -42,19 +35,23 @@ export default function GasketWeighted() {
setGame(chaosGameWeighted(params)) setGame(chaosGameWeighted(params))
}, [f0Weight, f1Weight, f2Weight]); }, [f0Weight, f1Weight, f2Weight]);
const weightInput = (title, weight, setWeight) => (
<>
<div className={styles.inputDiv}>
<p><TeX>{title}</TeX> weight:<span style={{float: 'right'}}>{weight}</span></p>
<input type={'range'} min={0} max={1} step={.01} style={{width: '100%'}} value={weight}
onInput={e => setWeight(Number(e.currentTarget.value))}/>
</div>
</>
)
return ( return (
<> <>
<Canvas width={image.width} height={image.height} painter={game}/> <Canvas width={image.width} height={image.height} painter={game}/>
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto'}}> <div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
<WeightInput value={f0Weight} setValue={setF0Weight}> {weightInput("F_0", f0Weight, setF0Weight)}
<p><TeX>F_0</TeX> weight:<span style={{float: 'right'}}>{f0Weight}</span></p> {weightInput("F_1", f1Weight, setF1Weight)}
</WeightInput> {weightInput("F_2", f2Weight, setF2Weight)}
<WeightInput value={f1Weight} setValue={setF1Weight}>
<p><TeX>F_1</TeX> weight:<span style={{float: 'right'}}>{f1Weight}</span></p>
</WeightInput>
<WeightInput value={f2Weight} setValue={setF2Weight}>
<p><TeX>F_2</TeX> weight:<span style={{float: 'right'}}>{f2Weight}</span></p>
</WeightInput>
</div> </div>
</> </>
) )

View File

@ -2,9 +2,6 @@ function Gasket() {
// Hint: try increasing the iteration count // Hint: try increasing the iteration count
const iterations = 10000; const iterations = 10000;
// Display the progress every `step` iterations
const step = 1000;
// Hint: negating `x` and `y` creates some interesting images // Hint: negating `x` and `y` creates some interesting images
const transforms = [ const transforms = [
(x, y) => [x / 2, y / 2], (x, y) => [x / 2, y / 2],
@ -21,16 +18,12 @@ function Gasket() {
const i = randomInteger(0, transforms.length); const i = randomInteger(0, transforms.length);
[x, y] = transforms[i](x, y); [x, y] = transforms[i](x, y);
if (count > 20) { if (count > 20)
plot(x, y, image); plot(x, y, image);
}
if (count % 1000 === 0) { if (count % 1000 === 0)
yield image; yield image;
}
} }
yield image;
} }
return ( return (
@ -40,5 +33,4 @@ function Gasket() {
painter={chaosGame()}/> painter={chaosGame()}/>
) )
} }
render(<Gasket/>) render(<Gasket/>)

View File

@ -19,13 +19,11 @@ export function* chaosGameWeighted({transforms, image, iterations, step}: Params
// highlight-end // highlight-end
[x, y] = transform(x, y); [x, y] = transform(x, y);
if (i > 20) { if (i > 20)
plot(x, y, image); plot(x, y, image);
}
if (i % step === 0) { if (i % step === 0)
yield image; yield image;
}
} }
yield image; yield image;

View File

@ -13,13 +13,10 @@ Wikipedia [describes](https://en.wikipedia.org/wiki/Fractal_flame) fractal flame
I think of them a different way: beauty in mathematics. I think of them a different way: beauty in mathematics.
import isDarkMode from '@site/src/isDarkMode' import isDarkMode from '@site/src/isDarkMode'
import bannerDark from '../banner-dark.png' import banner from '../banner.png'
import bannerLight from '../banner-light.png'
<center> <center>
<!-- Why are these backwards? --> <img src={banner} style={{filter: isDarkMode() ? '' : 'invert(1)'}}/>
<img src={bannerLight} hidden={isDarkMode()}/>
<img src={bannerDark} hidden={!isDarkMode()}/>
</center> </center>
<!-- truncate --> <!-- truncate -->
@ -204,6 +201,4 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
import BrowserOnly from "@docusaurus/BrowserOnly"; import BrowserOnly from "@docusaurus/BrowserOnly";
import GasketWeighted from "./GasketWeighted" import GasketWeighted from "./GasketWeighted"
<BrowserOnly> <BrowserOnly>{() => <GasketWeighted/>}</BrowserOnly>
<GasketWeighted/>
</BrowserOnly>

View File

@ -0,0 +1,142 @@
import {useState} from "react";
import { blend } from "./blend";
import { applyCoefs, Coefs } from "../src/coefs"
import {randomBiUnit} from "../src/randomBiUnit";
import {linear} from "../src/linear";
import {julia} from "../src/julia";
import {popcorn} from "../src/popcorn";
import {pdj} from "../src/pdj";
import {Variation} from "../src/variation";
import {Transform} from "../src/transform";
import {
pdjParams,
xform1Coefs,
xform1Weight,
xform2Coefs,
xform2Weight,
xform3Coefs,
xform3Weight
} from "../src/params";
import {randomChoice} from "../src/randomChoice";
import {plotBinary} from "../src/plotBinary"
import Canvas from "../src/Canvas"
import styles from "../src/css/styles.module.css"
type VariationBlend = {
linear: number,
julia: number,
popcorn: number,
pdj: number
}
export default function FlameBlend() {
const image = new ImageData(400, 400);
const quality = 2;
const step = 5000;
const xform1Default: VariationBlend = {
linear: 0,
julia: 1,
popcorn: 0,
pdj: 0,
}
const [xform1Variations, setXform1Variations] = useState(xform1Default)
const xform2Default: VariationBlend = {
linear: 1,
julia: 0,
popcorn: 1,
pdj: 0
}
const [xform2Variations, setXform2Variations] = useState(xform2Default)
const xform3Default: VariationBlend = {
linear: 0,
julia: 0,
popcorn: 0,
pdj: 1
}
const [xform3Variations, setXform3Variations] = useState(xform3Default)
function buildTransform(coefs: Coefs, variations: VariationBlend): Transform {
return (x: number, y: number) => {
const [varX, varY] = applyCoefs(x, y, coefs);
const varFunctions: [number, Variation][] = [
[variations.linear, linear],
[variations.julia, julia],
[variations.popcorn, popcorn(coefs)],
[variations.pdj, pdj(pdjParams)]
]
return blend(varX, varY, varFunctions);
}
}
function* chaosGame() {
let [x, y] = [randomBiUnit(), randomBiUnit()];
const transforms: [number, Transform][] = [
[xform1Weight, buildTransform(xform1Coefs, xform1Variations)],
[xform2Weight, buildTransform(xform2Coefs, xform2Variations)],
[xform3Weight, buildTransform(xform3Coefs, xform3Variations)]
]
const iterations = quality * image.width * image.height;
for (let i = 0; i < iterations; i++) {
let [_, transform] = randomChoice(transforms);
[x, y] = transform(x, y);
if (i > 20)
plotBinary(x, y, image);
if (i % step === 0) {
console.log(`Checking in; iterations=${i}`)
yield image;
}
}
yield image;
}
const variationEditor = (title, variations, setVariations) => {
return (
<>
<p style={{gridColumn: '1/-1'}}>{title}:</p>
<div className={styles.inputDiv}>
<p>Linear: {variations.linear}</p>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.linear}
onInput={e => setVariations({...variations, linear: Number(e.currentTarget.value)})}/>
</div>
<div className={styles.inputDiv}>
<p>Julia: {variations.julia}</p>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.julia}
onInput={e => setVariations({...variations, julia: Number(e.currentTarget.value)})}/>
</div>
<div className={styles.inputDiv}>
<p>Popcorn: {variations.popcorn}</p>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.popcorn}
onInput={e => setVariations({...variations, popcorn: Number(e.currentTarget.value)})}/>
</div>
<div className={styles.inputDiv}>
<p>PDJ: {variations.pdj}</p>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.pdj}
onInput={e => setVariations({...variations, pdj: Number(e.currentTarget.value)})}/>
</div>
</>
)
}
return (
<>
<Canvas
width={image.width}
height={image.height}
painter={chaosGame()}/>
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto auto'}}>
{variationEditor("Transform 1", xform1Variations, setXform1Variations)}
{variationEditor("Transform 2", xform2Variations, setXform2Variations)}
{variationEditor("Transform 3", xform3Variations, setXform3Variations)}
</div>
</>
)
}

View File

@ -0,0 +1,14 @@
// hidden-start
import { Variation } from "../src/variation"
// hidden-end
export function blend(x: number, y: number, variations: [number, Variation][]): [number, number] {
let [finalX, finalY] = [0, 0];
for (const [weight, variation] of variations) {
const [varX, varY] = variation(x, y);
finalX += weight * varX;
finalY += weight * varY;
}
return [finalX, finalY];
}

View File

@ -15,6 +15,8 @@ This blog post uses a set of reference parameters ([available here](../params.fl
implementation of the fractal flame algorithm. If you're interested in tweaking the parameters, or generating implementation of the fractal flame algorithm. If you're interested in tweaking the parameters, or generating
your own art, [Apophysis](https://sourceforge.net/projects/apophysis/) is a good introductory tool. your own art, [Apophysis](https://sourceforge.net/projects/apophysis/) is a good introductory tool.
TODO: Include the reference image here
## Transforms and variations ## Transforms and variations
import CodeBlock from '@theme/CodeBlock' import CodeBlock from '@theme/CodeBlock'
@ -120,8 +122,9 @@ import pdjSrc from '!!raw-loader!../src/pdj'
### Blending ### Blending
Now, one variation is fun, but we can also combine variations in a single transform by "blending." Now, one variation is fun, but we can also combine variations in a single transform by "blending."
First, each variation is assigned a value that describes how much it affects the transform function ($v_j$). Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs.
Afterward, sum up the $x$ and $y$ values respectively: We'll also give each variation a weight ($v_j$) that scales the output, which allows us to control
how much each variation contributes to the transform:
$$ $$
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) 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)
@ -132,3 +135,7 @@ The formula looks intimidating, but it's not hard to implement:
TODO: Blending implementation? TODO: Blending implementation?
And with that in place, we have enough to render a first full fractal flame: And with that in place, we have enough to render a first full fractal flame:
import FlameBlend from "./FlameBlend";
<FlameBlend/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

View File

Before

Width:  |  Height:  |  Size: 557 KiB

After

Width:  |  Height:  |  Size: 557 KiB

View File

@ -0,0 +1,44 @@
/**
* Translate values in the flame coordinate system to pixel coordinates
*
* The way `flam3` actually calculates the "camera" for mapping a point
* to its pixel coordinate is fairly involved - it also needs to calculate
* zoom and rotation (see the bucket accumulator code in rect.c).
* We'll make some simplifying assumptions:
* - The final image is square
* - We want to plot the range [-2, 2]
*
* The reference parameters were designed in Apophysis, which uses the
* range [-2, 2] by default (the `scale` parameter in XML defines the
* "pixels per unit", and with the default zoom, is chosen to give a
* range of [-2, 2]).
*
* @param x point in the range [-2, 2]
* @param y point in the range [-2, 2]
* @param size image size
* @returns pair of pixel coordinates
*/
export function camera(x: number, y: number, size: number): [number, number] {
return [Math.floor(((x + 2) * size) / 4), Math.floor(((y + 2) * size) / 4)];
}
/**
* Translate values in pixel coordinates to a 1-dimensional array index
*
* Unlike the camera function, this mapping doesn't assume a square image,
* and only requires knowing the image width.
*
* The stride parameter is used to calculate indices that take into account
* how many "values" each pixel has. For example, in an ImageData, each pixel
* has a red, green, blue, and alpha component per pixel, so a stride of 4
* is appropriate. For situations where there are separate red/green/blue/alpha
* arrays per pixel, a stride of 1 is appropriate
*
* @param x point in the range [0, size)
* @param y point in the range [0, size)
* @param width width of image in pixel units
* @param stride values per pixel coordinate
*/
export function histIndex(x: number, y: number, width: number, stride: number): number {
return y * (width * stride) + x * stride;
}

View File

@ -2,3 +2,10 @@ export interface Coefs {
a: number, b: number, c: number, a: number, b: number, c: number,
d: number, e: number, f: number d: number, e: number, f: number
} }
export function applyCoefs(x: number, y: number, coefs: Coefs) {
return [
(x * coefs.a + y * coefs.b + coefs.c),
(x * coefs.d + y * coefs.e + coefs.f)
]
}

View File

@ -0,0 +1,4 @@
.inputDiv {
padding-left: .5em;
padding-right: 1em;
}

View File

@ -7,13 +7,18 @@ import { Coefs } from './coefs';
import { linear } from './linear' import { linear } from './linear'
import { julia } from './julia' import { julia } from './julia'
import { popcorn } from './popcorn' import { popcorn } from './popcorn'
import { pdj } from './pdj' import {pdj, PdjParams} from './pdj'
import {Variation} from "./variation"
export const identityCoefs: Coefs = { export const identityCoefs: Coefs = {
a: 1, b: 0, c: 0, a: 1, b: 0, c: 0,
d: 0, e: 1, f: 0, d: 0, e: 1, f: 0,
} }
export const pdjParams: PdjParams = {
a: 1.09358, b: 2.13048, c: 2.54127, d: 2.37267
}
export const xform1Weight = 0.56453495; export const xform1Weight = 0.56453495;
export const xform1Coefs = { export const xform1Coefs = {
a: -1.381068, b: -1.381068, c: 0, a: -1.381068, b: -1.381068, c: 0,
@ -47,7 +52,7 @@ export const xform3Coefs = {
} }
export const xform3CoefsPost = identityCoefs; export const xform3CoefsPost = identityCoefs;
export const xform3Variations = [ export const xform3Variations = [
[1, pdj(1.09358, 2.13048, 2.54127, 2.37267)] [1, pdj(pdjParams)]
]; ];
export const xform3Color = 0.349; export const xform3Color = 0.349;

View File

@ -1,7 +1,8 @@
// hidden-start // hidden-start
import { Variation } from './variation' import { Variation } from './variation'
//hidden-end //hidden-end
export function pdj(a: number, b: number, c: number, d: number): Variation { export type PdjParams = {a: number, b: number, c: number, d: number};
export function pdj({a, b, c, d}: PdjParams): Variation {
return (x, y) => [ return (x, y) => [
Math.sin(a * y) - Math.cos(b * x), Math.sin(a * y) - Math.cos(b * x),
Math.sin(c * x) - Math.cos(d * y) Math.sin(c * x) - Math.cos(d * y)

View File

@ -0,0 +1,17 @@
import { camera, histIndex } from "./camera"
export function plotBinary(x: number, y: number, image: ImageData) {
const [pixelX, pixelY] = camera(x, y, image.width);
if (
pixelX < 0 || pixelX >= image.width || pixelY < 0 || pixelY > image.height
) {
return;
}
const pixelIndex = histIndex(pixelX, pixelY, image.width, 4);
image.data[pixelIndex] = 0;
image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0;
image.data[pixelIndex + 3] = 0xff;
}

View File

@ -1,9 +1 @@
import { Coefs } from './coefs' export type Transform = (x: number, y: number) => [number, number];
import { Variation } from './variation'
export interface Transform {
coefs: Coefs,
variations: [number, Variation][],
coefsPost: Coefs,
color: number
}