Basic function weights

This commit is contained in:
Bradlee Speice 2024-11-24 22:37:53 -05:00
parent 4c3f4246a4
commit 7759b58dbe
24 changed files with 172 additions and 62 deletions

View File

@ -0,0 +1,61 @@
import {useEffect, useState} from "react";
import Canvas from "../src/Canvas";
import { Params, chaosGameWeighted } from "./chaosGameWeighted";
import TeX from '@matejmazur/react-katex';
type Transform = (x: number, y: number) => [number, number];
function WeightInput({value, setValue, children}) {
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() {
const image = new ImageData(600, 600);
const iterations = 100_000;
const step = 1000;
const [f0Weight, setF0Weight] = useState<number>(1);
const [f1Weight, setF1Weight] = useState<number>(1);
const [f2Weight, setF2Weight] = useState<number>(1);
const f0: Transform = (x, y) => [x / 2, y / 2];
const f1: Transform = (x, y) => [(x + 1) / 2, y / 2];
const f2: Transform = (x, y) => [x / 2, (y + 1) / 2];
const [game, setGame] = useState<Generator<ImageData>>(null);
useEffect(() => {
const params: Params = {
transforms: [
[f0Weight, f0],
[f1Weight, f1],
[f2Weight, f2]
],
image,
iterations,
step
}
setGame(chaosGameWeighted(params))
}, [f0Weight, f1Weight, f2Weight]);
return (
<>
<Canvas width={image.width} height={image.height} painter={game}/>
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
<WeightInput value={f0Weight} setValue={setF0Weight}>
<p><TeX>F_0</TeX> weight:<span style={{float: 'right'}}>{f0Weight}</span></p>
</WeightInput>
<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>
</>
)
}

View File

@ -1,3 +0,0 @@
export default function randomBiUnit() {
return Math.random() * 2 - 1;
}

View File

@ -2,20 +2,24 @@ function Gasket() {
// Hint: try increasing the iteration count
const iterations = 10000;
// Display the progress every `step` iterations
const step = 1000;
// Hint: negating `x` and `y` creates some interesting images
const functions = [
const transforms = [
(x, y) => [x / 2, y / 2],
(x, y) => [(x + 1) / 2, y / 2],
(x, y) => [x / 2, (y + 1) / 2]
]
const image = new ImageData(600, 600);
function* chaosGame() {
var [x, y] = [randomBiUnit(), randomBiUnit()];
for (var count = 0; count < iterations; count++) {
const i = randomInteger(0, functions.length);
[x, y] = functions[i](x, y);
const i = randomInteger(0, transforms.length);
[x, y] = transforms[i](x, y);
if (count > 20) {
plot(x, y, image);

View File

@ -0,0 +1,32 @@
// hidden-start
import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { plot } from "./plot"
export type Transform = (x: number, y: number) => [number, number];
export type Params = {
transforms: [number, Transform][],
image: ImageData,
iterations: number,
step: number
}
// hidden-end
export function* chaosGameWeighted({transforms, image, iterations, step}: Params) {
var [x, y] = [randomBiUnit(), randomBiUnit()];
for (let i = 0; i < iterations; i++) {
// highlight-start
const [_, transform] = randomChoice(transforms);
// highlight-end
[x, y] = transform(x, y);
if (i > 20) {
plot(x, y, image);
}
if (i % step === 0) {
yield image;
}
}
yield image;
}

View File

@ -126,7 +126,7 @@ Fractal flames use more complex functions to produce a wide variety of images, b
## Sierpinski's gasket
Using these definitions, we can build the first image. The paper defines a function system we can use as-is:
Using these definitions, we can build the first image. The paper defines a function system for us:
$$
F_0(x, y) = \left({x \over 2}, {y \over 2} \right)
@ -157,13 +157,13 @@ Let's turn this into code, one piece at a time.
First, the "bi-unit square" is the range $[-1, 1]$. We can pick a random point like this:
import biunitSource from '!!raw-loader!./biunit'
import biunitSource from '!!raw-loader!../src/randomBiUnit'
<CodeBlock language="typescript">{biunitSource}</CodeBlock>
Next, we need to choose a random integer from $0$ to $n - 1$:
import randintSource from '!!raw-loader!./randint'
import randintSource from '!!raw-loader!../src/randomInteger'
<CodeBlock language="typescript">{randintSource}</CodeBlock>
@ -185,8 +185,22 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
<hr/>
<small>
Note: The image our chaos game generates is different than the fractal flame paper, but I think the version displayed
here is correct. As confirmation, the next post will re-create the same image using a different method.
Note: The image here is different than the fractal flame paper, but I think the paper has an error.
</small>
TODO: Explanation of function weights $w_i$
## Weights
Finally, we'll introduce a "weight" parameter ($w_i$) assigned to each function, which controls
how often that function is used:
import randomChoiceSource from '!!raw-loader!../src/randomChoice'
<CodeBlock language={'typescript'}>{randomChoiceSource}</CodeBlock>
import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
import GasketWeighted from "./GasketWeighted"
<GasketWeighted/>

View File

@ -1,4 +1,4 @@
export default function plot(x: number, y: number, image: ImageData) {
export function plot(x: number, y: number, image: ImageData) {
// Translate (x,y) coordinates to pixel coordinates.
// The display range we care about is x=[0, 1], y=[0, 1],
// so our pixelX and pixelY coordinates are easy to calculate:

View File

@ -1,3 +0,0 @@
export default function randomInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}

View File

@ -1,6 +1,6 @@
import randomBiUnit from './biunit';
import plot from './plot';
import randomInteger from './randint';
import { plot } from './plot';
import { randomBiUnit } from '../src/randomBiUnit';
import { randomInteger } from '../src/randomInteger';
import Canvas from "../src/Canvas";
const Scope = {

View File

@ -1,5 +0,0 @@
import {useColorMode, useThemeConfig} from "@docusaurus/theme-common";
export default function BaselineRender({children}) {
const {colorMode, setColorMode} = useColorMode();
}

View File

@ -1,22 +0,0 @@
// hidden-start
import { Coefs } from './coefs'
import { Variation } from './variations'
// hidden-end
export function applyTransform(
x: number,
y: number,
coefs: Coefs,
variations: [number, Variation][])
{
const transformX = coefs.a * x + coefs.b * y + coefs.c;
const transformY = coefs.d * x + coefs.e * y + coefs.f;
var finalX = 0;
var finalY = 0;
for (const [blend, variation] of variations) {
const [variationX, variationY] = variation(transformX, transformY);
finalX += blend * variationX;
finalY += blend * variationY;
}
return [finalX, finalY];
}

View File

@ -66,9 +66,6 @@ import linearSrc from '!!raw-loader!../src/linear'
<CodeBlock language={'typescript'}>{linearSrc}</CodeBlock>
Before we move on, it's worth mentioning the relationship between this variation and the Sierpinski Gasket.
Specifically, we can think of the Gasket as a fractal flame that uses only the linear variation.
### Julia (variation 13)
This variation still uses just the $x$ and $y$ coordinates, but does crazy things with them:
@ -132,11 +129,6 @@ $$
The formula looks intimidating, but it's not hard to implement:
import baselineSrc from '!!raw-loader!./baseline'
<CodeBlock language={'typescript'}>{baselineSrc}</CodeBlock>
TODO: Mention that the Sierpinski Gasket is just a blend with linear weight 1, all others 0. Maybe replace comment above about Sierpinski Gasket and linear transform?
TODO: Blending implementation?
And with that in place, we have enough to render a first full fractal flame:

View File

@ -39,13 +39,18 @@ export default function Canvas({width, height, painter, children}: Props) {
useEffect(paint, [colorMode, image]);
const animate = () => {
if (!painter) {
return;
}
console.log("Animating");
const nextImage = painter.next().value;
if (nextImage) {
setImage([nextImage])
requestAnimationFrame(animate);
}
}
useEffect(animate, [canvasCtx]);
useEffect(animate, [painter, canvasCtx]);
return (
<>

View File

@ -1,5 +1,5 @@
// hidden-start
import { Variation } from './variations'
import { Variation } from './variation'
// hidden-end
export const julia: Variation = (x, y) => {
const r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));

View File

@ -1,4 +1,4 @@
// hidden-start
import {Variation} from "./variations"
import {Variation} from "./variation"
//hidden-end
export const linear: Variation = (x, y) => [x, y];

View File

@ -4,7 +4,6 @@
*/
import { Coefs } from './coefs';
import { Transform } from './transform';
import { linear } from './linear'
import { julia } from './julia'
import { popcorn } from './popcorn'

View File

@ -1,5 +1,5 @@
// hidden-start
import { Variation } from './variations'
import { Variation } from './variation'
//hidden-end
export function pdj(a: number, b: number, c: number, d: number): Variation {
return (x, y) => [

View File

@ -1,6 +1,6 @@
// hidden-start
import {Coefs} from './coefs'
import {Variation} from './variations'
import {Variation} from './variation'
// hidden-end
export function popcorn({c, f}: Coefs): Variation {
return (x, y) => [

View File

@ -0,0 +1,3 @@
export function randomBiUnit() {
return Math.random() * 2 - 1;
}

View File

@ -0,0 +1,15 @@
export function randomChoice<T>(choices: [number, T][]): [number, T] {
const weightSum = choices.reduce((sum, [weight, _]) => sum + weight, 0);
let choice = Math.random() * weightSum;
for (const [index, element] of choices.entries()) {
const [weight, t] = element;
if (choice < weight) {
return [index, t];
}
choice -= weight;
}
const index = choices.length - 1;
return [index, choices[index][1]];
}

View File

@ -0,0 +1,3 @@
export function randomInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}

View File

@ -1,5 +1,5 @@
import { Coefs } from './coefs'
import { Variation } from './variations'
import { Variation } from './variation'
export interface Transform {
coefs: Coefs,

14
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@docusaurus/faster": "^3.6.1",
"@docusaurus/preset-classic": "^3.6.1",
"@docusaurus/theme-live-codeblock": "^3.6.1",
"@matejmazur/react-katex": "^3.1.3",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.5.0",
@ -3031,6 +3032,19 @@
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
},
"node_modules/@matejmazur/react-katex": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@matejmazur/react-katex/-/react-katex-3.1.3.tgz",
"integrity": "sha512-rBp7mJ9An7ktNoU653BWOYdO4FoR4YNwofHZi+vaytX/nWbIlmHVIF+X8VFOn6c3WYmrLT5FFBjKqCZ1sjR5uQ==",
"engines": {
"node": ">=12",
"yarn": ">=1.1"
},
"peerDependencies": {
"katex": ">=0.9",
"react": ">=16"
}
},
"node_modules/@mdx-js/mdx": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz",

View File

@ -19,6 +19,7 @@
"@docusaurus/faster": "^3.6.1",
"@docusaurus/preset-classic": "^3.6.1",
"@docusaurus/theme-live-codeblock": "^3.6.1",
"@matejmazur/react-katex": "^3.1.3",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.5.0",