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 e18a5b0..23f0b2f 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,5 +1,7 @@ export function plot(x: number, y: number, image: ImageData) { - // Translate (x,y) coordinates to pixel coordinates. + // Translate (x,y) coordinates to pixel coordinates; + // also known as a "camera" function. + // // The display range we care about is x=[0, 1], y=[0, 1], // so our pixelX and pixelY coordinates are easy to calculate: const pixelX = Math.floor(x * image.width); diff --git a/blog/2024-11-15-playing-with-fire/3-log-density.mdx b/blog/2024-11-15-playing-with-fire/3-log-density.mdx deleted file mode 100644 index fe7ddfe..0000000 --- a/blog/2024-11-15-playing-with-fire/3-log-density.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -slug: 2024/11/playing-with-fire-log-density -title: "Playing with fire: Log-density display" -date: 2024-11-15 14:00:00 -authors: [bspeice] -tags: [] ---- - -Testing \ 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 new file mode 100644 index 0000000..3e134d7 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/3-log-density/FlameHistogram.tsx @@ -0,0 +1,5 @@ +import {VictoryArea} from "victory"; + +function F() { + return (); + 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/index.mdx b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx new file mode 100644 index 0000000..37292ec --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx @@ -0,0 +1,28 @@ +--- +slug: 2024/11/playing-with-fire-log-density +title: "Playing with fire: Log-density display" +date: 2024-11-15 14:00:00 +authors: [bspeice] +tags: [] +--- + +So far, our `plot()` function has been fairly simple; map an input coordinate +to a specific pixel, and color in that pixel. +This works well for simple function systems (like Sierpinski's Gasket), +but more complex systems (like our reference parameters) produce grainy images. + +Every time we "turn on" pixels that have already been enabled, we're wasting work. +Can we do something more intelligent with that information? + + + +## Image histograms + +To start with, it's worth demonstrating how much work is actually "wasted." +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"; + +{plotHistogramSource} 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 16f694d..5788598 100644 --- a/blog/2024-11-15-playing-with-fire/src/Canvas.tsx +++ b/blog/2024-11-15-playing-with-fire/src/Canvas.tsx @@ -16,6 +16,7 @@ export const PainterContext = createContext(null); interface CanvasProps { width?: number; height?: number; + hidden?: boolean; children?: React.ReactNode; } @@ -23,31 +24,39 @@ interface CanvasProps { * Draw fractal flames to a canvas. * * This component is a bit involved because it attempts to solve - * a couple problems at the same time: + * a couple problems at once: * - Incrementally drawing an image to the canvas - * - Interrupting drawing with new parameters on demand + * - Interrupting drawing with new parameters * - Dark mode * - * Image iterators provide a means to draw incremental images; - * iterators can easily checkpoint state, and this component will - * request the next image on the next animation frame. As a result, - * the browser should be responsive even though we run CPU-heavy - * code on the main thread. + * 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. * - * Swapping a new iterator allows interrupting a render in progress, - * as the canvas completely repaints on each provided image. + * 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, check whether dark mode is active, and invert the most - * recent image prior to painting if so. + * 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 * - * PainterContext is used to allow child elements to swap in - * new iterators. + * 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 */ -export default function Canvas({width, height, children}: CanvasProps) { +export default function Canvas({width, height, hidden, children}: CanvasProps) { const [canvasCtx, setCanvasCtx] = useState(null); const canvasRef = useCallback(node => { if (node !== null) { @@ -134,6 +143,7 @@ export default function Canvas({width, height, children}: CanvasProps) { ref={canvasRef} width={width} height={height} + hidden={hidden ?? false} style={{ aspectRatio: width / height, width: '80%'