diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx b/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx new file mode 100644 index 0000000..d909aeb --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx @@ -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 ( +
+ {children} + setValue(Number(e.currentTarget.value))}/> +
+ ) +} + +export default function GasketWeighted() { + const image = new ImageData(600, 600); + const iterations = 100_000; + const step = 1000; + + const [f0Weight, setF0Weight] = useState(1); + const [f1Weight, setF1Weight] = useState(1); + const [f2Weight, setF2Weight] = useState(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>(null); + useEffect(() => { + const params: Params = { + transforms: [ + [f0Weight, f0], + [f1Weight, f1], + [f2Weight, f2] + ], + image, + iterations, + step + } + setGame(chaosGameWeighted(params)) + }, [f0Weight, f1Weight, f2Weight]); + + return ( + <> + +
+ +

F_0 weight:{f0Weight}

+
+ +

F_1 weight:{f1Weight}

+
+ +

F_2 weight:{f2Weight}

+
+
+ + ) +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/biunit.ts b/blog/2024-11-15-playing-with-fire/1-introduction/biunit.ts deleted file mode 100644 index 21bf035..0000000 --- a/blog/2024-11-15-playing-with-fire/1-introduction/biunit.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function randomBiUnit() { - return Math.random() * 2 - 1; -} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js index ca42723..8d02cc4 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js +++ b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGame.js @@ -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); diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts new file mode 100644 index 0000000..4f39635 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts @@ -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; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx index 7cdeb02..a8c1347 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx @@ -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' {biunitSource} 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' {randintSource} @@ -185,8 +185,22 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
-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. -TODO: Explanation of function weights $w_i$ \ No newline at end of file +## 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' + +{randomChoiceSource} + +import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted"; + +{chaosGameWeightedSource} + +import GasketWeighted from "./GasketWeighted" + + \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts b/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts index 3bf9f03..e18a5b0 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts +++ b/blog/2024-11-15-playing-with-fire/1-introduction/plot.ts @@ -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: diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/randint.ts b/blog/2024-11-15-playing-with-fire/1-introduction/randint.ts deleted file mode 100644 index 07d79a3..0000000 --- a/blog/2024-11-15-playing-with-fire/1-introduction/randint.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function randomInteger(min: number, max: number) { - return Math.floor(Math.random() * (max - min)) + min; -} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/scope.tsx b/blog/2024-11-15-playing-with-fire/1-introduction/scope.tsx index d5d2d99..5b2dd7b 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/scope.tsx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/scope.tsx @@ -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 = { diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/BaselineRender.ts b/blog/2024-11-15-playing-with-fire/2-transforms/BaselineRender.ts deleted file mode 100644 index 315aacb..0000000 --- a/blog/2024-11-15-playing-with-fire/2-transforms/BaselineRender.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {useColorMode, useThemeConfig} from "@docusaurus/theme-common"; - -export default function BaselineRender({children}) { - const {colorMode, setColorMode} = useColorMode(); -} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/baseline.ts b/blog/2024-11-15-playing-with-fire/2-transforms/baseline.ts deleted file mode 100644 index 1708e81..0000000 --- a/blog/2024-11-15-playing-with-fire/2-transforms/baseline.ts +++ /dev/null @@ -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]; -} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx b/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx index ddccc50..87e4e53 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx +++ b/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx @@ -66,9 +66,6 @@ import linearSrc from '!!raw-loader!../src/linear' {linearSrc} -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' - -{baselineSrc} - -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: - diff --git a/blog/2024-11-15-playing-with-fire/src/Canvas.tsx b/blog/2024-11-15-playing-with-fire/src/Canvas.tsx index 35f3c7a..4b9b4d4 100644 --- a/blog/2024-11-15-playing-with-fire/src/Canvas.tsx +++ b/blog/2024-11-15-playing-with-fire/src/Canvas.tsx @@ -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 ( <> diff --git a/blog/2024-11-15-playing-with-fire/src/julia.ts b/blog/2024-11-15-playing-with-fire/src/julia.ts index 5d21045..19c5040 100644 --- a/blog/2024-11-15-playing-with-fire/src/julia.ts +++ b/blog/2024-11-15-playing-with-fire/src/julia.ts @@ -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)); diff --git a/blog/2024-11-15-playing-with-fire/src/linear.ts b/blog/2024-11-15-playing-with-fire/src/linear.ts index cbea1e7..36b234a 100644 --- a/blog/2024-11-15-playing-with-fire/src/linear.ts +++ b/blog/2024-11-15-playing-with-fire/src/linear.ts @@ -1,4 +1,4 @@ // hidden-start -import {Variation} from "./variations" +import {Variation} from "./variation" //hidden-end export const linear: Variation = (x, y) => [x, y]; \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/params.ts b/blog/2024-11-15-playing-with-fire/src/params.ts index 485347d..3ec90cd 100644 --- a/blog/2024-11-15-playing-with-fire/src/params.ts +++ b/blog/2024-11-15-playing-with-fire/src/params.ts @@ -4,7 +4,6 @@ */ import { Coefs } from './coefs'; -import { Transform } from './transform'; import { linear } from './linear' import { julia } from './julia' import { popcorn } from './popcorn' diff --git a/blog/2024-11-15-playing-with-fire/src/pdj.ts b/blog/2024-11-15-playing-with-fire/src/pdj.ts index 5360ced..8456692 100644 --- a/blog/2024-11-15-playing-with-fire/src/pdj.ts +++ b/blog/2024-11-15-playing-with-fire/src/pdj.ts @@ -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) => [ diff --git a/blog/2024-11-15-playing-with-fire/src/popcorn.ts b/blog/2024-11-15-playing-with-fire/src/popcorn.ts index 4652960..bcb2741 100644 --- a/blog/2024-11-15-playing-with-fire/src/popcorn.ts +++ b/blog/2024-11-15-playing-with-fire/src/popcorn.ts @@ -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) => [ diff --git a/blog/2024-11-15-playing-with-fire/src/randomBiUnit.ts b/blog/2024-11-15-playing-with-fire/src/randomBiUnit.ts new file mode 100644 index 0000000..7e5f389 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/randomBiUnit.ts @@ -0,0 +1,3 @@ +export function randomBiUnit() { + return Math.random() * 2 - 1; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/randomChoice.ts b/blog/2024-11-15-playing-with-fire/src/randomChoice.ts new file mode 100644 index 0000000..0494924 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/randomChoice.ts @@ -0,0 +1,15 @@ +export function randomChoice(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]]; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/randomInteger.ts b/blog/2024-11-15-playing-with-fire/src/randomInteger.ts new file mode 100644 index 0000000..c3a6719 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/randomInteger.ts @@ -0,0 +1,3 @@ +export function randomInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min)) + min; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/transform.ts b/blog/2024-11-15-playing-with-fire/src/transform.ts index e719c57..4829133 100644 --- a/blog/2024-11-15-playing-with-fire/src/transform.ts +++ b/blog/2024-11-15-playing-with-fire/src/transform.ts @@ -1,5 +1,5 @@ import { Coefs } from './coefs' -import { Variation } from './variations' +import { Variation } from './variation' export interface Transform { coefs: Coefs, diff --git a/blog/2024-11-15-playing-with-fire/src/variations.ts b/blog/2024-11-15-playing-with-fire/src/variation.ts similarity index 100% rename from blog/2024-11-15-playing-with-fire/src/variations.ts rename to blog/2024-11-15-playing-with-fire/src/variation.ts diff --git a/package-lock.json b/package-lock.json index 4878ca7..7eb6dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 34cb296..47f0fda 100644 --- a/package.json +++ b/package.json @@ -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",