From 7f44243cd0a23b933325d49b5c2eb8dae753fb32 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Tue, 18 Jul 2023 02:21:01 +0000 Subject: [PATCH] Convert baseline renderer to animation --- posts/2023/06/flam3/0-canvas.tsx | 25 ++++++++-- posts/2023/06/flam3/1-gasket.ts | 1 + posts/2023/06/flam3/2a-variations.ts | 75 ++++++++++++++++++++++++++-- posts/2023/06/flam3/index.tsx | 17 +++++-- 4 files changed, 104 insertions(+), 14 deletions(-) diff --git a/posts/2023/06/flam3/0-canvas.tsx b/posts/2023/06/flam3/0-canvas.tsx index ab29cd0..cf182fe 100644 --- a/posts/2023/06/flam3/0-canvas.tsx +++ b/posts/2023/06/flam3/0-canvas.tsx @@ -35,14 +35,16 @@ export type CanvasParams = { size: number; qualityMax: number; qualityStep: number; - renderer: Renderer; + renderer: (size: number) => Renderer; }; export const CanvasRenderer: React.FC<{ params: CanvasParams }> = ({ params, }) => { const canvasRef = useRef(null); + var qualityCurrent: number = 0; + var rendererCurrent: Renderer = params.renderer(params.size); const animate = () => { const ctx = canvasRef.current?.getContext("2d"); @@ -51,21 +53,34 @@ export const CanvasRenderer: React.FC<{ params: CanvasParams }> = ({ } const image = ctx.createImageData(params.size, params.size); - params.renderer.run(params.qualityStep); - params.renderer.render(image); + rendererCurrent.run(params.qualityStep); + rendererCurrent.render(image); ctx.putImageData(image, 0, 0); - qualityCurrent += params.qualityStep; if (qualityCurrent < params.qualityMax) { + qualityCurrent += params.qualityStep; requestAnimationFrame(animate); } }; + const reset = () => { + qualityCurrent = 0; + rendererCurrent = params.renderer(params.size); + requestAnimationFrame(animate); + }; + useEffect(() => { if (canvasRef.current) { requestAnimationFrame(animate); } }, []); - return ; + return ( + + ); }; diff --git a/posts/2023/06/flam3/1-gasket.ts b/posts/2023/06/flam3/1-gasket.ts index bf1c410..590866b 100644 --- a/posts/2023/06/flam3/1-gasket.ts +++ b/posts/2023/06/flam3/1-gasket.ts @@ -1,3 +1,4 @@ +import { CanvasParams } from "./0-canvas"; import { randomBiUnit, randomInteger, diff --git a/posts/2023/06/flam3/2a-variations.ts b/posts/2023/06/flam3/2a-variations.ts index c7e374d..ecc974b 100644 --- a/posts/2023/06/flam3/2a-variations.ts +++ b/posts/2023/06/flam3/2a-variations.ts @@ -1,10 +1,17 @@ -import { randomBiUnit, weightedChoice } from "./0-utility"; +import { + Renderer, + histIndex, + imageIndex, + randomBiUnit, + weightedChoice, +} from "./0-utility"; export type Variation = ( x: number, y: number, transformCoefs: Coefs ) => [number, number]; + export type Coefs = { a: number; b: number; @@ -83,6 +90,66 @@ export class Transform { } } +export class RendererFlame extends Renderer { + private values = new Uint8Array(this.size * this.size); + + constructor(size: number, public readonly transforms: [number, Transform][]) { + super(size); + } + + plot(x: number, y: number) { + // By default, Apophysis uses a camera that covers the range [-2, 2] + // (specifically, the `scale` parameter becomes `pixels_per_unit` in flam3) + // Shift the coordinate system to [0, 4], then scale to pixel coordinates + const pixelX = Math.floor(((x + 2) * this.size) / 4); + const pixelY = Math.floor(((y + 2) * this.size) / 4); + + if ( + pixelX < 0 || + pixelX >= this.size || + pixelY < 0 || + pixelY >= this.size + ) { + return; + } + + const hIndex = histIndex(pixelX, pixelY, this.size); + this.values[hIndex] = 1; + } + + run(quality: number): void { + var x = randomBiUnit(); + var y = randomBiUnit(); + + const iterations = quality * this.size * this.size; + for (var i = 0; i < iterations; i++) { + const [_, transform] = weightedChoice(this.transforms); + [x, y] = transform.apply(x, y); + + if (i > 20) { + this.plot(x, y); + } + } + } + + render(image: ImageData): void { + for (var x = 0; x < this.size; x++) { + for (var y = 0; y < this.size; y++) { + const hIndex = histIndex(x, y, this.size); + if (!this.values[hIndex]) { + continue; + } + + const iIndex = imageIndex(x, y, this.size); + image.data[iIndex + 0] = 0; + image.data[iIndex + 1] = 0; + image.data[iIndex + 2] = 0; + image.data[iIndex + 3] = 0xff; + } + } + } +} + export class Flame { protected x: number = randomBiUnit(); protected y: number = randomBiUnit(); @@ -186,12 +253,10 @@ export const transform3 = new Transform( [[1, pdj(1.09358, 2.13048, 2.54127, 2.37267)]] ); -export function renderBaseline(image: ImageData) { - const flame = new Flame([ +export function rendererBaseline(size: number) { + return new RendererFlame(size, [ [transform1Weight, transform1], [transform2Weight, transform2], [transform3Weight, transform3], ]); - - render(flame, 1, image); } diff --git a/posts/2023/06/flam3/index.tsx b/posts/2023/06/flam3/index.tsx index 7b80a0d..8624e59 100644 --- a/posts/2023/06/flam3/index.tsx +++ b/posts/2023/06/flam3/index.tsx @@ -2,7 +2,7 @@ import Blog from "../../../LayoutBlog"; import { Canvas, CanvasRenderer } from "./0-canvas"; import { RendererGasket } from "./1-gasket"; -import { renderBaseline } from "./2a-variations"; +import { rendererBaseline } from "./2a-variations"; import { renderPost } from "./2b-post"; import { renderFinal } from "./2c-final"; import { renderBinary } from "./3a-binary"; @@ -23,13 +23,22 @@ export default function () { }); return ( - */} + rendererBaseline(size), + qualityMax: 1, + qualityStep: 0.1, }} /> {/*