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 ed60823..e4f1988 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
@@ -198,8 +198,7 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
{chaosGameWeightedSource}
-import BrowserOnly from "@docusaurus/BrowserOnly";
import GasketWeighted from "./GasketWeighted";
import Canvas from "../src/Canvas";
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/FlameFinal.tsx b/blog/2024-11-15-playing-with-fire/2-transforms/FlameFinal.tsx
index f9365b8..b11d9d4 100644
--- a/blog/2024-11-15-playing-with-fire/2-transforms/FlameFinal.tsx
+++ b/blog/2024-11-15-playing-with-fire/2-transforms/FlameFinal.tsx
@@ -24,6 +24,12 @@ import {VariationEditor, VariationProps} from "./VariationEditor";
import {CoefEditor} from "./CoefEditor";
import {Transform} from "../src/transform";
+export const transforms: [number, Transform][] = [
+ [xform1Weight, transformPost(buildTransform(xform1Coefs, xform1Variations), xform1CoefsPost)],
+ [xform2Weight, transformPost(buildTransform(xform2Coefs, xform2Variations), xform2CoefsPost)],
+ [xform3Weight, transformPost(buildTransform(xform3Coefs, xform3Variations), xform3CoefsPost)]
+];
+
export default function FlameFinal() {
const {width, height, setPainter} = useContext(PainterContext);
@@ -43,12 +49,6 @@ export default function FlameFinal() {
const resetXformFinalCoefsPost = () => setXformFinalCoefsPost(xformFinalCoefsPostDefault);
useEffect(() => {
- const transforms: [number, Transform][] = [
- [xform1Weight, transformPost(buildTransform(xform1Coefs, xform1Variations), xform1CoefsPost)],
- [xform2Weight, transformPost(buildTransform(xform2Coefs, xform2Variations), xform2CoefsPost)],
- [xform3Weight, transformPost(buildTransform(xform3Coefs, xform3Variations), xform3CoefsPost)]
- ];
-
const finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
const finalTransform = buildTransform(xformFinalCoefs, finalBlend);
const finalPost = transformPost(finalTransform, xformFinalCoefsPost);
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 59e24cd..481e4b6 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
@@ -142,11 +142,10 @@ The sliders below change the variation weights for each transform (the $v_{ij}$
try changing them around to see which parts of the image are controlled by
each transform.
-import BrowserOnly from "@docusaurus/BrowserOnly";
import Canvas from "../src/Canvas";
import FlameBlend from "./FlameBlend";
-
+
## Post transforms
@@ -161,10 +160,10 @@ $$
import FlamePost from "./FlamePost";
-
+
## Final transform
import FlameFinal from "./FlameFinal";
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/FlameHistogram.tsx b/blog/2024-11-15-playing-with-fire/3-log-density/FlameHistogram.tsx
index 3e134d7..976f6fb 100644
--- a/blog/2024-11-15-playing-with-fire/3-log-density/FlameHistogram.tsx
+++ b/blog/2024-11-15-playing-with-fire/3-log-density/FlameHistogram.tsx
@@ -1,5 +1,30 @@
-import {VictoryArea} from "victory";
+import {VictoryChart, VictoryLine, VictoryScatter, VictoryTheme} from "victory";
+import {useContext, useEffect, useState} from "react";
+import {PainterContext} from "../src/Canvas";
+import {chaosGameHistogram} from "./chaosGameHistogram";
+import {PlotData, plotHistogram} from "./plotHistogram";
-function F() {
- return void, setCdf: (data: PlotData) => void) {
+ const emptyImage = new ImageData(width, height);
+ for (let histogram of chaosGameHistogram(width, height)) {
+ const plotData = plotHistogram(histogram);
+ setPdf(plotData);
+ yield emptyImage;
+ }
+}
+
+export default function FlameHistogram() {
+ const {width, height, setPainter} = useContext(PainterContext);
+ const [pdfData, setPdfData] = useState<{ x: number, y: number }[]>(null);
+
+ useEffect(() => setPainter(plotChaosGame(width, height, setPdfData, null)), []);
+
+ return (
+
+
+
+ )
}
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/PlotHistogram.ts b/blog/2024-11-15-playing-with-fire/3-log-density/PlotHistogram.ts
deleted file mode 100644
index 7a527e8..0000000
--- a/blog/2024-11-15-playing-with-fire/3-log-density/PlotHistogram.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-// hidden-start
-import {VictoryChart} from "victory";
-import {camera, histIndex} from "../src/camera";
-// hidden-end
-export class PlotHistogram {
- public readonly pixels: Uint32Array;
-
- public constructor(private readonly width: number, height: number) {
- this.pixels = new Uint32Array(width * height);
- }
-
- public plot(x: number, y: number) {
- const [pixelX, pixelY] = camera(x, y, this.width);
- const pixelIndex = histIndex(pixelX, pixelY, this.width, 1);
- this.pixels[pixelIndex] += 1;
- }
-
- public getHistogram() {
- const data = new Map();
- this.pixels.forEach(value => {
- const bucket = 32 - Math.clz32(value);
-
- if (bucket in data) {
- data[bucket] += 1;
- } else {
- data[bucket] = 1;
- }
- })
-
- const output: {x: number, y: number}[] = [];
- data.forEach((bucket, value) =>
- output.push({x: Math.pow(bucket, 2), y: value}));
- return output;
- }
-}
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/chaosGameHistogram.ts b/blog/2024-11-15-playing-with-fire/3-log-density/chaosGameHistogram.ts
new file mode 100644
index 0000000..c831353
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/3-log-density/chaosGameHistogram.ts
@@ -0,0 +1,34 @@
+import {plot} from "./plotHistogram";
+import {randomBiUnit} from "../src/randomBiUnit";
+import {randomChoice} from "../src/randomChoice";
+import {buildTransform} from "../2-transforms/buildTransform";
+import {transformPost} from "../2-transforms/post";
+import {transforms} from "../2-transforms/FlameFinal";
+import * as params from "../src/params";
+
+const finalTransform = buildTransform(params.xformFinalCoefs, params.xformFinalVariations);
+const finalTransformPost = transformPost(finalTransform, params.xformFinalCoefsPost);
+
+const step = 1000;
+const quality = 1;
+
+export function* chaosGameHistogram(width: number, height: number) {
+ let iterations = quality * width * height;
+ let histogram = new Uint32Array(width * height);
+
+ let [x, y] = [randomBiUnit(), randomBiUnit()];
+
+ for (let i = 0; i < iterations; i++) {
+ const [_, transform] = randomChoice(transforms);
+ [x, y] = transform(x, y);
+ [x, y] = finalTransformPost(x, y);
+
+ if (i > 20)
+ plot(x, y, width, histogram);
+
+ if (i % step === 0)
+ yield histogram;
+ }
+
+ yield histogram;
+}
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx
index 37292ec..ce2168c 100644
--- a/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx
+++ b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx
@@ -23,6 +23,13 @@ We'll render the reference image again, but this time, counting the times
we tried to turn on a pixel.
import CodeBlock from "@theme/CodeBlock";
-import plotHistogramSource from "!!raw-loader!./PlotHistogram";
+import plotHistogramSource from "!!raw-loader!./plotHistogram";
{plotHistogramSource}
+
+import Canvas from "../src/Canvas";
+import FlameHistogram from "./FlameHistogram";
+
+
\ No newline at end of file
diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/plotHistogram.ts b/blog/2024-11-15-playing-with-fire/3-log-density/plotHistogram.ts
new file mode 100644
index 0000000..479d0ae
--- /dev/null
+++ b/blog/2024-11-15-playing-with-fire/3-log-density/plotHistogram.ts
@@ -0,0 +1,23 @@
+// hidden-start
+import {camera, histIndex} from "../src/camera";
+// hidden-end
+export function plot(x: number, y: number, width: number, hitCount: Uint32Array) {
+ const [pixelX, pixelY] = camera(x, y, width);
+ const pixelIndex = histIndex(pixelX, pixelY, width, 1);
+ hitCount[pixelIndex] += 1;
+}
+
+export type PlotData = {x: number, y: number}[];
+export function plotHistogram(hitCount: Uint32Array) {
+ const data = new Map();
+ hitCount.forEach(value => {
+ const bucket = 32 - Math.clz32(value);
+ const currentCount = data.get(bucket) ?? 0;
+ data.set(bucket, currentCount + 1);
+ })
+
+ const output: PlotData = [];
+ data.forEach((value, bucket) =>
+ output.push({x: Math.pow(2, bucket), y: value}));
+ return output;
+}
\ No newline at end of file
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 5788598..20e6bee 100644
--- a/blog/2024-11-15-playing-with-fire/src/Canvas.tsx
+++ b/blog/2024-11-15-playing-with-fire/src/Canvas.tsx
@@ -1,23 +1,80 @@
import React, {useCallback, useEffect, useState, createContext} from "react";
import {useColorMode} from "@docusaurus/theme-common";
+import BrowserOnly from "@docusaurus/BrowserOnly";
-export interface PainterProps {
- readonly width: number;
- readonly height: number;
- readonly setPainter: (painter: Iterator) => void;
+function invertImage(sourceImage: ImageData): ImageData {
+ const image = new ImageData(sourceImage.width, sourceImage.height);
+ image.data.forEach((value, index) =>
+ image.data[index] = index % 4 === 3 ? value : 0xff - value)
+
+ return image;
+}
+
+type InvertibleCanvasProps = {
+ width: number,
+ height: number,
+ hidden?: boolean,
+ // NOTE: Images are provided as a single-element array
+ //so we can allow re-painting with the same (modified) ImageData reference.
+ image?: [ImageData],
}
/**
- * Context provider for child elements to submit image iterator functions
- * (painters) for rendering
+ * Draw images to a canvas, automatically inverting colors as needed.
+ *
+ * @param width Canvas width
+ * @param height Canvas height
+ * @param hidden Hide the canvas element
+ * @param image Image data to draw on the canvas
*/
+const InvertibleCanvas: React.FC = ({width, height, hidden, image}) => {
+ const [canvasCtx, setCanvasCtx] = useState(null);
+ const canvasRef = useCallback(node => {
+ if (node !== null) {
+ setCanvasCtx(node.getContext("2d"));
+ }
+ }, []);
+
+ const [paintImage, setPaintImage] = useState<[ImageData]>(null);
+ useEffect(() => {
+ if (canvasCtx && paintImage) {
+ canvasCtx.putImageData(paintImage[0], 0, 0);
+ }
+ }, [canvasCtx, paintImage]);
+
+ const {colorMode} = useColorMode();
+ useEffect(() => {
+ if (image) {
+ setPaintImage(colorMode === 'light' ? image : [invertImage(image[0])]);
+ }
+ }, [image, colorMode]);
+
+ return (
+
+ )
+}
+
+type PainterProps = {
+ width: number;
+ height: number;
+ setPainter: (painter: Iterator) => void;
+}
export const PainterContext = createContext(null);
interface CanvasProps {
width?: number;
height?: number;
hidden?: boolean;
- children?: React.ReactNode;
+ children?: React.ReactElement;
}
/**
@@ -27,131 +84,61 @@ interface CanvasProps {
* a couple problems at once:
* - Incrementally drawing an image to the canvas
* - Interrupting drawing with new parameters
- * - Dark mode
*
* Running a full render is labor-intensive, so we model it
* as an iterator that yields an image of the current system.
* Internally, that iterator is re-queued on each new image;
- * so long as that image is returned quickly, we keep
- * the main loop running even with CPU-heavy code.
+ * so long as retrieving each image happens quickly,
+ * we keep the main loop running even with CPU-heavy code.
+ * As a side benefit, this also animates the chaos game nicely.
+ * TODO(bspeice): This also causes React to complain about maximum update depth
+ * Would this be better off spawning a `useEffect` animator
+ * that has access to a `setState` queue?
*
* To interrupt drawing, children set the active iterator
* through the context provider. This component doesn't care
* about which iterator is in progress, it exists only
* to fetch the next image and paint it to our canvas.
*
- * Finally, we make a distinction between "render" and "paint" buffers.
- * The render image is provided by the iterator, and then:
- * - If light mode is active, draw it to the canvas as-is
- * - If dark mode is active, copy the "render" buffer to the "paint" buffer,
- * invert colors, and then draw the image
- *
* TODO(bspeice): Can we make this "re-queueing iterator" pattern generic?
* It would be nice to have iterators returning arbitrary objects,
- * but we rely on contexts to manage the iterator, and there's
- * no good way to make those generic.
- *
- * @param width Canvas draw width
- * @param height Canvas draw height
- * @param hidden Hide the canvas
- * @param children Child elements
+ * but we rely on contexts to manage the iterator, and I can't find
+ * a good way to make those generic.
*/
export default function Canvas({width, height, hidden, children}: CanvasProps) {
- const [canvasCtx, setCanvasCtx] = useState(null);
- const canvasRef = useCallback(node => {
- if (node !== null) {
- setCanvasCtx(node.getContext("2d"));
- }
- }, []);
-
- // Holder objects are used to force re-painting even if the iterator
- // returns a modified image with the same reference
- type ImageHolder = { image?: ImageData };
-
- const [paintImage, setPaintImage] = useState({ image: null });
+ const [image, setImage] = useState<[ImageData]>(null);
+ const [painterHolder, setPainterHolder] = useState<[Iterator]>(null);
useEffect(() => {
- if (paintImage.image && canvasCtx) {
- canvasCtx.putImageData(paintImage.image, 0, 0);
- }
- }, [paintImage, canvasCtx]);
-
- const {colorMode} = useColorMode();
- const [renderImage, setRenderImage] = useState({ image: null });
- useEffect(() => {
- const image = renderImage.image;
- if (!image) {
+ if (!painterHolder) {
return;
}
- // If light mode is active, paint the image as-is
- if (colorMode === 'light') {
- setPaintImage({ image });
- return;
- }
-
- // If dark mode is active, copy the image into a new buffer
- // and invert colors prior to painting.
- // Copy alpha values as-is.
- const paintImage = new ImageData(image.width, image.height);
- image.data.forEach((value, index) => {
- const isAlpha = index % 4 === 3;
- paintImage.data[index] = isAlpha ? value : 255 - value;
- })
- setPaintImage({ image: paintImage });
- }, [colorMode, renderImage]);
-
- // Image iterators (painters) are also in a holder; this allows
- // re-submitting the existing iterator to draw the next frame,
- // and also allows child components to over-write the iterator
- // if a new set of parameters becomes available
- // TODO(bspeice): Potential race condition?
- // Not sure if it's possible for painters submitted by children
- // to be over-ridden as a result re-submitting the
- // existing iterator
- type PainterHolder = { painter?: Iterator };
- const [animHolder, setAnimHolder] = useState({ painter: null });
- useEffect(() => {
- const painter = animHolder.painter;
- if (!painter) {
- return;
- }
-
- if (!canvasCtx) {
- setAnimHolder({ painter });
- return;
- }
-
- const image = painter.next().value;
- if (image) {
- setRenderImage({ image });
- setAnimHolder({ painter });
+ const painter = painterHolder[0];
+ const nextImage = painter.next().value;
+ if (nextImage) {
+ setImage([nextImage]);
+ setPainterHolder([painter]);
} else {
- setAnimHolder({ painter: null });
+ setPainterHolder(null);
}
- }, [animHolder, canvasCtx]);
+ }, [painterHolder]);
- // Finally, child elements submit painters through a context provider
const [painter, setPainter] = useState>(null);
- useEffect(() => setAnimHolder({ painter }), [painter]);
+ useEffect(() => {
+ if (painter) {
+ setPainterHolder([painter]);
+ }
+ }, [painter]);
width = width ?? 500;
height = height ?? 500;
return (
<>
-
+
- {children}
+ {() => children}
>
)