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 index d909aeb..fc3f6bb 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/GasketWeighted.tsx @@ -3,16 +3,9 @@ import Canvas from "../src/Canvas"; import { Params, chaosGameWeighted } from "./chaosGameWeighted"; 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}) { - return ( -
- {children} - setValue(Number(e.currentTarget.value))}/> -
- ) -} +type Transform = (x: number, y: number) => [number, number]; export default function GasketWeighted() { const image = new ImageData(600, 600); @@ -42,19 +35,23 @@ export default function GasketWeighted() { setGame(chaosGameWeighted(params)) }, [f0Weight, f1Weight, f2Weight]); + const weightInput = (title, weight, setWeight) => ( + <> +
+

{title} weight:{weight}

+ setWeight(Number(e.currentTarget.value))}/> +
+ + ) + return ( <>
- -

F_0 weight:{f0Weight}

-
- -

F_1 weight:{f1Weight}

-
- -

F_2 weight:{f2Weight}

-
+ {weightInput("F_0", f0Weight, setF0Weight)} + {weightInput("F_1", f1Weight, setF1Weight)} + {weightInput("F_2", f2Weight, setF2Weight)}
) 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 8d02cc4..e51648c 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,9 +2,6 @@ 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 transforms = [ (x, y) => [x / 2, y / 2], @@ -21,16 +18,12 @@ function Gasket() { const i = randomInteger(0, transforms.length); [x, y] = transforms[i](x, y); - if (count > 20) { + if (count > 20) plot(x, y, image); - } - if (count % 1000 === 0) { + if (count % 1000 === 0) yield image; - } } - - yield image; } return ( @@ -40,5 +33,4 @@ function Gasket() { painter={chaosGame()}/> ) } - render() 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 index 4f39635..5001ff3 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts +++ b/blog/2024-11-15-playing-with-fire/1-introduction/chaosGameWeighted.ts @@ -19,13 +19,11 @@ export function* chaosGameWeighted({transforms, image, iterations, step}: Params // highlight-end [x, y] = transform(x, y); - if (i > 20) { + if (i > 20) plot(x, y, image); - } - if (i % step === 0) { + if (i % step === 0) yield image; - } } yield image; 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 df6d974..22b022b 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 @@ -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. import isDarkMode from '@site/src/isDarkMode' -import bannerDark from '../banner-dark.png' -import bannerLight from '../banner-light.png' +import banner from '../banner.png'
- - - +
@@ -204,6 +201,4 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted"; import BrowserOnly from "@docusaurus/BrowserOnly"; import GasketWeighted from "./GasketWeighted" - - - \ No newline at end of file +{() => } \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/FlameBlend.tsx b/blog/2024-11-15-playing-with-fire/2-transforms/FlameBlend.tsx new file mode 100644 index 0000000..cc49d53 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/2-transforms/FlameBlend.tsx @@ -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 ( + <> +

{title}:

+
+

Linear: {variations.linear}

+ setVariations({...variations, linear: Number(e.currentTarget.value)})}/> +
+
+

Julia: {variations.julia}

+ setVariations({...variations, julia: Number(e.currentTarget.value)})}/> +
+
+

Popcorn: {variations.popcorn}

+ setVariations({...variations, popcorn: Number(e.currentTarget.value)})}/> +
+
+

PDJ: {variations.pdj}

+ setVariations({...variations, pdj: Number(e.currentTarget.value)})}/> +
+ + ) + } + + return ( + <> + +
+ {variationEditor("Transform 1", xform1Variations, setXform1Variations)} + {variationEditor("Transform 2", xform2Variations, setXform2Variations)} + {variationEditor("Transform 3", xform3Variations, setXform3Variations)} +
+ + ) +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/blend.ts b/blog/2024-11-15-playing-with-fire/2-transforms/blend.ts new file mode 100644 index 0000000..0f8778e --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/2-transforms/blend.ts @@ -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]; +} \ 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 87e4e53..5af277d 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 @@ -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 your own art, [Apophysis](https://sourceforge.net/projects/apophysis/) is a good introductory tool. +TODO: Include the reference image here + ## Transforms and variations import CodeBlock from '@theme/CodeBlock' @@ -120,8 +122,9 @@ import pdjSrc from '!!raw-loader!../src/pdj' ### 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$). -Afterward, sum up the $x$ and $y$ values respectively: +Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs. +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) @@ -132,3 +135,7 @@ The formula looks intimidating, but it's not hard to implement: TODO: Blending implementation? And with that in place, we have enough to render a first full fractal flame: + +import FlameBlend from "./FlameBlend"; + + \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/banner-light.png b/blog/2024-11-15-playing-with-fire/banner-light.png deleted file mode 100644 index 9ed2719..0000000 Binary files a/blog/2024-11-15-playing-with-fire/banner-light.png and /dev/null differ diff --git a/blog/2024-11-15-playing-with-fire/banner-dark.png b/blog/2024-11-15-playing-with-fire/banner.png similarity index 100% rename from blog/2024-11-15-playing-with-fire/banner-dark.png rename to blog/2024-11-15-playing-with-fire/banner.png diff --git a/blog/2024-11-15-playing-with-fire/src/camera.ts b/blog/2024-11-15-playing-with-fire/src/camera.ts new file mode 100644 index 0000000..28dd927 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/camera.ts @@ -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; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/coefs.ts b/blog/2024-11-15-playing-with-fire/src/coefs.ts index da5b1aa..121d451 100644 --- a/blog/2024-11-15-playing-with-fire/src/coefs.ts +++ b/blog/2024-11-15-playing-with-fire/src/coefs.ts @@ -1,4 +1,11 @@ export interface Coefs { a: number, b: number, c: 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) + ] } \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/css/styles.module.css b/blog/2024-11-15-playing-with-fire/src/css/styles.module.css new file mode 100644 index 0000000..c129a5c --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/css/styles.module.css @@ -0,0 +1,4 @@ +.inputDiv { + padding-left: .5em; + padding-right: 1em; +} \ 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 3ec90cd..0dee991 100644 --- a/blog/2024-11-15-playing-with-fire/src/params.ts +++ b/blog/2024-11-15-playing-with-fire/src/params.ts @@ -7,13 +7,18 @@ import { Coefs } from './coefs'; import { linear } from './linear' import { julia } from './julia' import { popcorn } from './popcorn' -import { pdj } from './pdj' +import {pdj, PdjParams} from './pdj' +import {Variation} from "./variation" export const identityCoefs: Coefs = { a: 1, b: 0, c: 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 xform1Coefs = { a: -1.381068, b: -1.381068, c: 0, @@ -47,7 +52,7 @@ export const xform3Coefs = { } export const xform3CoefsPost = identityCoefs; export const xform3Variations = [ - [1, pdj(1.09358, 2.13048, 2.54127, 2.37267)] + [1, pdj(pdjParams)] ]; export const xform3Color = 0.349; 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 8456692..5afd470 100644 --- a/blog/2024-11-15-playing-with-fire/src/pdj.ts +++ b/blog/2024-11-15-playing-with-fire/src/pdj.ts @@ -1,7 +1,8 @@ // hidden-start import { Variation } from './variation' //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) => [ Math.sin(a * y) - Math.cos(b * x), Math.sin(c * x) - Math.cos(d * y) diff --git a/blog/2024-11-15-playing-with-fire/src/plotBinary.ts b/blog/2024-11-15-playing-with-fire/src/plotBinary.ts new file mode 100644 index 0000000..5b9fb64 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/plotBinary.ts @@ -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; +} \ 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 4829133..ce099a0 100644 --- a/blog/2024-11-15-playing-with-fire/src/transform.ts +++ b/blog/2024-11-15-playing-with-fire/src/transform.ts @@ -1,9 +1 @@ -import { Coefs } from './coefs' -import { Variation } from './variation' - -export interface Transform { - coefs: Coefs, - variations: [number, Variation][], - coefsPost: Coefs, - color: number -} \ No newline at end of file +export type Transform = (x: number, y: number) => [number, number]; \ No newline at end of file