diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/Gasket.jsx b/blog/2024-11-15-playing-with-fire/1-introduction/Gasket.jsx index 5dcc0e5..36b32ea 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/Gasket.jsx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/Gasket.jsx @@ -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() diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/biunit.ts b/blog/2024-11-15-playing-with-fire/1-introduction/biunit.ts deleted file mode 100644 index 21bf035..0000000 --- a/blog/2024-11-15-playing-with-fire/1-introduction/biunit.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function randomBiUnit() { - return Math.random() * 2 - 1; -} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx index 9e6758c..74c34a3 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx @@ -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' -{biunitSource} +{randomBiunitSource} 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' -{randintSource} +{randomIntegerSource} 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 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 3bf9f03..523635c 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,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; + + 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 + } + } + } } \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/randint.ts b/blog/2024-11-15-playing-with-fire/1-introduction/randint.ts deleted file mode 100644 index 07d79a3..0000000 --- a/blog/2024-11-15-playing-with-fire/1-introduction/randint.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function randomInteger(min: number, max: number) { - return Math.floor(Math.random() * (max - min)) + min; -} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/scope.ts b/blog/2024-11-15-playing-with-fire/1-introduction/scope.ts index f020b05..ff3fcbf 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/scope.ts +++ b/blog/2024-11-15-playing-with-fire/1-introduction/scope.ts @@ -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 diff --git a/blog/2024-11-15-playing-with-fire/src/FlameCanvas.tsx b/blog/2024-11-15-playing-with-fire/src/FlameCanvas.tsx new file mode 100644 index 0000000..4b9563c --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/FlameCanvas.tsx @@ -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({}); + +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 = ({renderer, children}) => { + const ITER_STEP = 10000; + const canvasRef = useRef(null); + + const colorMode = useColorMode(); + const [quality, setQuality] = useState(); + const [flame, setFlame] = useState(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 ( + + + {children} + + ) +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/randomBiunit.ts b/blog/2024-11-15-playing-with-fire/src/randomBiunit.ts new file mode 100644 index 0000000..7e5f389 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/randomBiunit.ts @@ -0,0 +1,3 @@ +export function randomBiUnit() { + return Math.random() * 2 - 1; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/randomChoice.ts b/blog/2024-11-15-playing-with-fire/src/randomChoice.ts new file mode 100644 index 0000000..4920f87 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/randomChoice.ts @@ -0,0 +1,22 @@ +/** + * @param choices array of [weight, value] pairs + * @returns pair of [index, value] + */ +export function randomChoice(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"; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/randomInteger.ts b/blog/2024-11-15-playing-with-fire/src/randomInteger.ts new file mode 100644 index 0000000..c3a6719 --- /dev/null +++ b/blog/2024-11-15-playing-with-fire/src/randomInteger.ts @@ -0,0 +1,3 @@ +export function randomInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min)) + min; +} \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/src/transform.ts b/blog/2024-11-15-playing-with-fire/src/transform.ts index e719c57..3caf1fa 100644 --- a/blog/2024-11-15-playing-with-fire/src/transform.ts +++ b/blog/2024-11-15-playing-with-fire/src/transform.ts @@ -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 } \ No newline at end of file