Attempt to generalize

This commit is contained in:
Bradlee Speice 2024-11-23 15:26:48 -05:00
parent aba3c9f988
commit e2a0ee1d72
11 changed files with 177 additions and 44 deletions

View File

@ -9,6 +9,7 @@ const functions = [
]
function chaosGame(image) {
var plotter = new Plotter(image.width, image.height);
var [x, y] = [randomBiUnit(), randomBiUnit()];
for (var count = 0; count < iterations; count++) {
@ -16,9 +17,11 @@ function chaosGame(image) {
[x, y] = functions[i](x, y);
if (count > 20) {
plot(x, y, image);
plotter.plot(x, y);
}
}
plotter.paint(image);
}
render(<Canvas renderFn={chaosGame}/>)

View File

@ -1,3 +0,0 @@
export default function randomBiUnit() {
return Math.random() * 2 - 1;
}

View File

@ -157,15 +157,15 @@ Let's turn this into code, one piece at a time.
First, the "bi-unit square" is the range $[-1, 1]$. We can pick a random point like this:
import biunitSource from '!!raw-loader!./biunit'
import randomBiunitSource from '!!raw-loader!../src/randomBiunit'
<CodeBlock language="typescript">{biunitSource}</CodeBlock>
<CodeBlock language="typescript">{randomBiunitSource}</CodeBlock>
Next, we need to choose a random integer from $0$ to $n - 1$:
import randintSource from '!!raw-loader!./randint'
import randomIntegerSource from '!!raw-loader!../src/randomInteger'
<CodeBlock language="typescript">{randintSource}</CodeBlock>
<CodeBlock language="typescript">{randomIntegerSource}</CodeBlock>
Finally, implementing the `plot` function. Web browsers have a [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)
we can use for 2D graphics. In our case, the plot function will take an $(x,y)$ coordinate and plot it by

View File

@ -1,32 +1,40 @@
export default function plot(x: number, y: number, image: ImageData) {
// Translate (x,y) coordinates to pixel coordinates.
// 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);
const pixelY = Math.floor(y * image.height);
export function imageIndex(x: number, y: number, width: number, stride: number): number {
return y * (width * stride) + x * 4;
}
// If we have an (x,y) coordinate outside the display range,
// skip it
if (
pixelX < 0 ||
pixelX > image.width ||
pixelY < 0 ||
pixelY > image.height
) {
return;
export class Plotter {
private readonly pixels: Array<boolean>;
constructor(private readonly width: number, private readonly height: number) {
this.pixels = new Array(width * height).fill(false);
}
// ImageData is an array that contains four bytes per pixel
// (one for each of the red, green, blue, and alpha values).
// The (pixelX, pixelY) coordinates are used to find where
// in the image we need to write.
const index = pixelY * (image.width * 4) + pixelX * 4;
public plot(x: number, y: number) {
// Translate (x,y) coordinates to pixel coordinates.
// 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 * this.width);
const pixelY = Math.floor(y * this.height);
// Set the pixel to black by writing a 0 to the first three
// bytes (red, green, blue), and 256 to the last byte (alpha),
// starting at our index:
image.data[index] = 0;
image.data[index + 1] = 0;
image.data[index + 2] = 0;
image.data[index + 3] = 0xff;
// Translate the pixel coordinates into an index
const pixelIndex = imageIndex(pixelX, pixelY, this.width, 1);
// If the index is valid, enable that pixel
if (0 <= pixelIndex && pixelIndex < this.pixels.length) {
this.pixels[pixelIndex] = true;
}
}
public paint(image: ImageData) {
// "Paint" all our pixels by setting their value to black
for (var pixelIndex = 0; pixelIndex < this.pixels.length; pixelIndex++) {
if (this.pixels[pixelIndex]) {
const imageIndex = pixelIndex * 4;
image.data[imageIndex] = 0; // red
image.data[imageIndex + 1] = 0; // green
image.data[imageIndex + 2] = 0; // blue
image.data[imageIndex + 3] = 0xff; // alpha
}
}
}
}

View File

@ -1,3 +0,0 @@
export default function randomInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}

View File

@ -1,13 +1,13 @@
import React from 'react';
import randomBiUnit from './biunit';
import plot from './plot';
import randomInteger from './randint';
import { randomBiUnit } from '../src/randomBiunit';
import { Plotter } from './plot';
import { randomInteger } from '../src/randomInteger';
import Canvas from './Canvas';
const Scope = {
React,
plot,
Plotter,
randomBiUnit,
randomInteger,
Canvas

View File

@ -0,0 +1,99 @@
import {createContext, useEffect, useRef, useState} from "react";
import {Transform} from "./transform";
import {randomBiUnit} from "./randomBiunit";
import {useColorMode} from "@docusaurus/theme-common";
import {randomChoice} from "./randomChoice";
export interface Flame {
transforms: [number, Transform][];
final: Transform;
palette: Uint8Array;
}
interface IFlameContext {
setQuality?: (quality: number) => void;
setFlame?: (flame: Flame) => void;
}
export const FlameContext = createContext<IFlameContext>({});
abstract class Renderer {
protected readonly _size: number;
protected _x: number = randomBiUnit();
protected _y: number = randomBiUnit();
protected _iterations: number = 0;
protected _flame?: Flame;
protected constructor(size: number) {
this._size = size;
}
abstract plot(x: number, y: number, c: number): void;
abstract run(iterations: number): void;
abstract paint(image: ImageData): void;
public get size(): number {
return this._size;
}
public get iterations(): number {
return this._iterations;
}
public set flame(value: Flame) {
[this._x, this._y] = [randomBiUnit(), randomBiUnit()];
this._iterations = 0;
}
}
export interface Props {
renderer: Renderer;
children?: React.ReactNode;
}
export const FlameCanvas: React.FC<Props> = ({renderer, children}) => {
const ITER_STEP = 10000;
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const colorMode = useColorMode();
const [quality, setQuality] = useState<number>();
const [flame, setFlame] = useState<Flame>(null);
var image: ImageData = null;
function animate() {
renderer.run(ITER_STEP);
paint();
if (renderer.iterations < quality * renderer.size * renderer.size) {
requestAnimationFrame(this);
}
}
function paint() {
}
useEffect(() => {
renderer.flame = flame;
requestAnimationFrame(animate);
}, [quality, flame]);
useEffect(() => { paint(); }, [colorMode]);
return (
<FlameContext.Provider value={{setFlame, setQuality}}>
<canvas
ref={canvasRef}
width={renderer.size}
height={renderer.size}
style={{
aspectRatio: '1 / 1',
width: '100%',
}}
/>
{children}
</FlameContext.Provider>
)
}

View File

@ -0,0 +1,3 @@
export function randomBiUnit() {
return Math.random() * 2 - 1;
}

View File

@ -0,0 +1,22 @@
/**
* @param choices array of [weight, value] pairs
* @returns pair of [index, value]
*/
export function randomChoice<T>(choices: [number, T][]): [number, T] {
const weightSum = choices.reduce(
(current, [weight, _]) => current + weight,
0
);
var choice = Math.random() * weightSum;
for (var i = 0; i < choices.length; i++) {
const [weight, t] = choices[i];
if (choice < weight) {
return [i, t];
}
choice -= weight;
}
throw "unreachable";
}

View File

@ -0,0 +1,3 @@
export function randomInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}

View File

@ -4,6 +4,7 @@ import { Variation } from './variations'
export interface Transform {
coefs: Coefs,
variations: [number, Variation][],
coefsPost: Coefs,
color: number
enabled: boolean,
coefsPost?: Coefs,
color?: number
}