mirror of
				https://github.com/bspeice/speice.io
				synced 2025-11-03 18:10:32 -05:00 
			
		
		
		
	Attempt to generalize
This commit is contained in:
		@ -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}/>)
 | 
			
		||||
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
export default function randomBiUnit() {
 | 
			
		||||
    return Math.random() * 2 - 1;
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
export default function randomInteger(min: number, max: number) {
 | 
			
		||||
    return Math.floor(Math.random() * (max - min)) + min;
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										99
									
								
								blog/2024-11-15-playing-with-fire/src/FlameCanvas.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								blog/2024-11-15-playing-with-fire/src/FlameCanvas.tsx
									
									
									
									
									
										Normal 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>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								blog/2024-11-15-playing-with-fire/src/randomBiunit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								blog/2024-11-15-playing-with-fire/src/randomBiunit.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
export function randomBiUnit() {
 | 
			
		||||
    return Math.random() * 2 - 1;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								blog/2024-11-15-playing-with-fire/src/randomChoice.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								blog/2024-11-15-playing-with-fire/src/randomChoice.ts
									
									
									
									
									
										Normal 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";
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								blog/2024-11-15-playing-with-fire/src/randomInteger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								blog/2024-11-15-playing-with-fire/src/randomInteger.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
export function randomInteger(min: number, max: number) {
 | 
			
		||||
    return Math.floor(Math.random() * (max - min)) + min;
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user