mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Attempt to generalize
This commit is contained in:
parent
aba3c9f988
commit
e2a0ee1d72
@ -9,6 +9,7 @@ const functions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function chaosGame(image) {
|
function chaosGame(image) {
|
||||||
|
var plotter = new Plotter(image.width, image.height);
|
||||||
var [x, y] = [randomBiUnit(), randomBiUnit()];
|
var [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||||
|
|
||||||
for (var count = 0; count < iterations; count++) {
|
for (var count = 0; count < iterations; count++) {
|
||||||
@ -16,9 +17,11 @@ function chaosGame(image) {
|
|||||||
[x, y] = functions[i](x, y);
|
[x, y] = functions[i](x, y);
|
||||||
|
|
||||||
if (count > 20) {
|
if (count > 20) {
|
||||||
plot(x, y, image);
|
plotter.plot(x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plotter.paint(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<Canvas renderFn={chaosGame}/>)
|
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:
|
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$:
|
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)
|
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
|
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) {
|
export function imageIndex(x: number, y: number, width: number, stride: number): number {
|
||||||
|
return y * (width * stride) + x * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public plot(x: number, y: number) {
|
||||||
// Translate (x,y) coordinates to pixel coordinates.
|
// Translate (x,y) coordinates to pixel coordinates.
|
||||||
// The display range we care about is x=[0, 1], y=[0, 1],
|
// The display range we care about is x=[0, 1], y=[0, 1],
|
||||||
// so our pixelX and pixelY coordinates are easy to calculate:
|
// so our pixelX and pixelY coordinates are easy to calculate:
|
||||||
const pixelX = Math.floor(x * image.width);
|
const pixelX = Math.floor(x * this.width);
|
||||||
const pixelY = Math.floor(y * image.height);
|
const pixelY = Math.floor(y * this.height);
|
||||||
|
|
||||||
// If we have an (x,y) coordinate outside the display range,
|
// Translate the pixel coordinates into an index
|
||||||
// skip it
|
const pixelIndex = imageIndex(pixelX, pixelY, this.width, 1);
|
||||||
if (
|
|
||||||
pixelX < 0 ||
|
// If the index is valid, enable that pixel
|
||||||
pixelX > image.width ||
|
if (0 <= pixelIndex && pixelIndex < this.pixels.length) {
|
||||||
pixelY < 0 ||
|
this.pixels[pixelIndex] = true;
|
||||||
pixelY > image.height
|
}
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageData is an array that contains four bytes per pixel
|
public paint(image: ImageData) {
|
||||||
// (one for each of the red, green, blue, and alpha values).
|
// "Paint" all our pixels by setting their value to black
|
||||||
// The (pixelX, pixelY) coordinates are used to find where
|
for (var pixelIndex = 0; pixelIndex < this.pixels.length; pixelIndex++) {
|
||||||
// in the image we need to write.
|
if (this.pixels[pixelIndex]) {
|
||||||
const index = pixelY * (image.width * 4) + pixelX * 4;
|
const imageIndex = pixelIndex * 4;
|
||||||
|
image.data[imageIndex] = 0; // red
|
||||||
// Set the pixel to black by writing a 0 to the first three
|
image.data[imageIndex + 1] = 0; // green
|
||||||
// bytes (red, green, blue), and 256 to the last byte (alpha),
|
image.data[imageIndex + 2] = 0; // blue
|
||||||
// starting at our index:
|
image.data[imageIndex + 3] = 0xff; // alpha
|
||||||
image.data[index] = 0;
|
}
|
||||||
image.data[index + 1] = 0;
|
}
|
||||||
image.data[index + 2] = 0;
|
}
|
||||||
image.data[index + 3] = 0xff;
|
|
||||||
}
|
}
|
@ -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 React from 'react';
|
||||||
|
|
||||||
import randomBiUnit from './biunit';
|
import { randomBiUnit } from '../src/randomBiunit';
|
||||||
import plot from './plot';
|
import { Plotter } from './plot';
|
||||||
import randomInteger from './randint';
|
import { randomInteger } from '../src/randomInteger';
|
||||||
import Canvas from './Canvas';
|
import Canvas from './Canvas';
|
||||||
|
|
||||||
const Scope = {
|
const Scope = {
|
||||||
React,
|
React,
|
||||||
plot,
|
Plotter,
|
||||||
randomBiUnit,
|
randomBiUnit,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
Canvas
|
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 {
|
export interface Transform {
|
||||||
coefs: Coefs,
|
coefs: Coefs,
|
||||||
variations: [number, Variation][],
|
variations: [number, Variation][],
|
||||||
coefsPost: Coefs,
|
enabled: boolean,
|
||||||
color: number
|
coefsPost?: Coefs,
|
||||||
|
color?: number
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user