Checkpoint for histogram

It takes a lot of render time to get a usable result, and it's not that interesting. Committing so I can save the work if I want to revisit it, but abandoning the idea for now.
This commit is contained in:
Bradlee Speice 2024-12-01 18:17:36 -05:00
parent 06069fdcea
commit 79b66337e8
9 changed files with 192 additions and 153 deletions

View File

@ -198,8 +198,7 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock> <CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
import BrowserOnly from "@docusaurus/BrowserOnly";
import GasketWeighted from "./GasketWeighted"; import GasketWeighted from "./GasketWeighted";
import Canvas from "../src/Canvas"; import Canvas from "../src/Canvas";
<Canvas><BrowserOnly>{() => <GasketWeighted/>}</BrowserOnly></Canvas> <Canvas><GasketWeighted/></Canvas>

View File

@ -24,6 +24,12 @@ import {VariationEditor, VariationProps} from "./VariationEditor";
import {CoefEditor} from "./CoefEditor"; import {CoefEditor} from "./CoefEditor";
import {Transform} from "../src/transform"; 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() { export default function FlameFinal() {
const {width, height, setPainter} = useContext(PainterContext); const {width, height, setPainter} = useContext(PainterContext);
@ -43,12 +49,6 @@ export default function FlameFinal() {
const resetXformFinalCoefsPost = () => setXformFinalCoefsPost(xformFinalCoefsPostDefault); const resetXformFinalCoefsPost = () => setXformFinalCoefsPost(xformFinalCoefsPostDefault);
useEffect(() => { 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 finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
const finalTransform = buildTransform(xformFinalCoefs, finalBlend); const finalTransform = buildTransform(xformFinalCoefs, finalBlend);
const finalPost = transformPost(finalTransform, xformFinalCoefsPost); const finalPost = transformPost(finalTransform, xformFinalCoefsPost);

View File

@ -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 try changing them around to see which parts of the image are controlled by
each transform. each transform.
import BrowserOnly from "@docusaurus/BrowserOnly";
import Canvas from "../src/Canvas"; import Canvas from "../src/Canvas";
import FlameBlend from "./FlameBlend"; import FlameBlend from "./FlameBlend";
<Canvas><BrowserOnly>{() => <FlameBlend/>}</BrowserOnly></Canvas> <Canvas><FlameBlend/></Canvas>
## Post transforms ## Post transforms
@ -161,10 +160,10 @@ $$
import FlamePost from "./FlamePost"; import FlamePost from "./FlamePost";
<Canvas><BrowserOnly>{() => <FlamePost/>}</BrowserOnly></Canvas> <Canvas><FlamePost/></Canvas>
## Final transform ## Final transform
import FlameFinal from "./FlameFinal"; import FlameFinal from "./FlameFinal";
<Canvas><BrowserOnly>{() => <FlameFinal/>}</BrowserOnly></Canvas> <Canvas><FlameFinal/></Canvas>

View File

@ -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() { function* plotChaosGame(width: number, height: number, setPdf: (data: PlotData) => void, setCdf: (data: PlotData) => void) {
return <VictoryArea data={} 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 (
<VictoryChart theme={VictoryTheme.clean}>
<VictoryLine
data={pdfData}
interpolation='natural'
/>
</VictoryChart>
)
} }

View File

@ -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<number, number>();
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;
}
}

View File

@ -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;
}

View File

@ -23,6 +23,13 @@ We'll render the reference image again, but this time, counting the times
we tried to turn on a pixel. we tried to turn on a pixel.
import CodeBlock from "@theme/CodeBlock"; import CodeBlock from "@theme/CodeBlock";
import plotHistogramSource from "!!raw-loader!./PlotHistogram"; import plotHistogramSource from "!!raw-loader!./plotHistogram";
<CodeBlock language="typescript">{plotHistogramSource}</CodeBlock> <CodeBlock language="typescript">{plotHistogramSource}</CodeBlock>
import Canvas from "../src/Canvas";
import FlameHistogram from "./FlameHistogram";
<Canvas width={400} height={400} hidden={true}>
<FlameHistogram/>
</Canvas>

View File

@ -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<number, number>();
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;
}

View File

@ -1,23 +1,80 @@
import React, {useCallback, useEffect, useState, createContext} from "react"; import React, {useCallback, useEffect, useState, createContext} from "react";
import {useColorMode} from "@docusaurus/theme-common"; import {useColorMode} from "@docusaurus/theme-common";
import BrowserOnly from "@docusaurus/BrowserOnly";
export interface PainterProps { function invertImage(sourceImage: ImageData): ImageData {
readonly width: number; const image = new ImageData(sourceImage.width, sourceImage.height);
readonly height: number; image.data.forEach((value, index) =>
readonly setPainter: (painter: Iterator<ImageData>) => void; 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 * Draw images to a canvas, automatically inverting colors as needed.
* (painters) for rendering *
* @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<InvertibleCanvasProps> = ({width, height, hidden, image}) => {
const [canvasCtx, setCanvasCtx] = useState<CanvasRenderingContext2D>(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 (
<canvas
ref={canvasRef}
width={width}
height={height}
hidden={hidden ?? false}
style={{
aspectRatio: width / height,
width: '75%'
}}
/>
)
}
type PainterProps = {
width: number;
height: number;
setPainter: (painter: Iterator<ImageData>) => void;
}
export const PainterContext = createContext<PainterProps>(null); export const PainterContext = createContext<PainterProps>(null);
interface CanvasProps { interface CanvasProps {
width?: number; width?: number;
height?: number; height?: number;
hidden?: boolean; hidden?: boolean;
children?: React.ReactNode; children?: React.ReactElement;
} }
/** /**
@ -27,131 +84,61 @@ interface CanvasProps {
* a couple problems at once: * a couple problems at once:
* - Incrementally drawing an image to the canvas * - Incrementally drawing an image to the canvas
* - Interrupting drawing with new parameters * - Interrupting drawing with new parameters
* - Dark mode
* *
* Running a full render is labor-intensive, so we model it * Running a full render is labor-intensive, so we model it
* as an iterator that yields an image of the current system. * as an iterator that yields an image of the current system.
* Internally, that iterator is re-queued on each new image; * Internally, that iterator is re-queued on each new image;
* so long as that image is returned quickly, we keep * so long as retrieving each image happens quickly,
* the main loop running even with CPU-heavy code. * 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 * To interrupt drawing, children set the active iterator
* through the context provider. This component doesn't care * through the context provider. This component doesn't care
* about which iterator is in progress, it exists only * about which iterator is in progress, it exists only
* to fetch the next image and paint it to our canvas. * 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? * TODO(bspeice): Can we make this "re-queueing iterator" pattern generic?
* It would be nice to have iterators returning arbitrary objects, * It would be nice to have iterators returning arbitrary objects,
* but we rely on contexts to manage the iterator, and there's * but we rely on contexts to manage the iterator, and I can't find
* no good way to make those generic. * a 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
*/ */
export default function Canvas({width, height, hidden, children}: CanvasProps) { export default function Canvas({width, height, hidden, children}: CanvasProps) {
const [canvasCtx, setCanvasCtx] = useState<CanvasRenderingContext2D>(null); const [image, setImage] = useState<[ImageData]>(null);
const canvasRef = useCallback(node => { const [painterHolder, setPainterHolder] = useState<[Iterator<ImageData>]>(null);
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<ImageHolder>({ image: null });
useEffect(() => { useEffect(() => {
if (paintImage.image && canvasCtx) { if (!painterHolder) {
canvasCtx.putImageData(paintImage.image, 0, 0);
}
}, [paintImage, canvasCtx]);
const {colorMode} = useColorMode();
const [renderImage, setRenderImage] = useState<ImageHolder>({ image: null });
useEffect(() => {
const image = renderImage.image;
if (!image) {
return; return;
} }
// If light mode is active, paint the image as-is const painter = painterHolder[0];
if (colorMode === 'light') { const nextImage = painter.next().value;
setPaintImage({ image }); if (nextImage) {
return; setImage([nextImage]);
} setPainterHolder([painter]);
// 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<ImageData> };
const [animHolder, setAnimHolder] = useState<PainterHolder>({ 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 });
} else { } else {
setAnimHolder({ painter: null }); setPainterHolder(null);
} }
}, [animHolder, canvasCtx]); }, [painterHolder]);
// Finally, child elements submit painters through a context provider
const [painter, setPainter] = useState<Iterator<ImageData>>(null); const [painter, setPainter] = useState<Iterator<ImageData>>(null);
useEffect(() => setAnimHolder({ painter }), [painter]); useEffect(() => {
if (painter) {
setPainterHolder([painter]);
}
}, [painter]);
width = width ?? 500; width = width ?? 500;
height = height ?? 500; height = height ?? 500;
return ( return (
<> <>
<center> <center>
<canvas <InvertibleCanvas width={width} height={height} hidden={hidden} image={image}/>
ref={canvasRef}
width={width}
height={height}
hidden={hidden ?? false}
style={{
aspectRatio: width / height,
width: '80%'
}}
/>
</center> </center>
<PainterContext.Provider value={{width, height, setPainter}}> <PainterContext.Provider value={{width, height, setPainter}}>
{children} <BrowserOnly>{() => children}</BrowserOnly>
</PainterContext.Provider> </PainterContext.Provider>
</> </>
) )