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 ( +