Update documentation

This commit is contained in:
Bradlee Speice 2024-12-01 15:16:30 -05:00
parent b7eed2297a
commit 06069fdcea
6 changed files with 95 additions and 24 deletions

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import {VictoryArea} from "victory";
function F() {
return <VictoryArea data={}
}

View File

@ -0,0 +1,35 @@
// 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,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?
<!-- truncate -->
## 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";
<CodeBlock language="typescript">{plotHistogramSource}</CodeBlock>

View File

@ -16,6 +16,7 @@ export const PainterContext = createContext<PainterProps>(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<CanvasRenderingContext2D>(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%'