diff --git a/pages/index.tsx b/pages/index.tsx index 54997d0..4e1e5d3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -4,7 +4,7 @@ export const Page = () => ( <>

Is this thing on?

- Code + Code

); diff --git a/posts/2023/06/flam3/0-canvas.tsx b/posts/2023/06/flam3/0-canvas.tsx new file mode 100644 index 0000000..53373e5 --- /dev/null +++ b/posts/2023/06/flam3/0-canvas.tsx @@ -0,0 +1,31 @@ +import React, { useEffect, useRef } from "react"; +import { renderFn } from "./0-utility"; + +export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => { + const canvasRef = useRef(null); + let image: ImageData | null = null; + useEffect(() => { + if (canvasRef.current) { + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + + if (!ctx) { + return; + } + + if ( + !image || + image.width !== canvas.width || + image.height !== canvas.height + ) { + image = ctx.createImageData(canvas.width, canvas.height); + } + + f(image); + + ctx.putImageData(image, 0, 0); + } + }); + + return ; +}; diff --git a/posts/2023/06/flam3/0-palette.ts b/posts/2023/06/flam3/0-palette.ts new file mode 100644 index 0000000..051f0f3 --- /dev/null +++ b/posts/2023/06/flam3/0-palette.ts @@ -0,0 +1,33 @@ +// https://stackoverflow.com/a/34356351 +function hexToBytes(hex: string) { + var bytes = []; + for (var i = 0; i < hex.length; i += 2) { + bytes.push(parseInt(hex.substring(i, i + 2), 16)); + } + + return bytes; +} + +export const paletteHex = + "7E3037762C45722B496E2A4E6A29506728536527546326565C265C5724595322574D2155482153462050451F4E441E4D431E4C3F1E473F1E453F1E433F1E3F3F1E3B3E1E393E1E37421D36431C38451C3A471B3B491B3C4A1A3C4B1A3D4D1A3E4F19405318435517445817465A16475D15495E154960154A65134E6812506B12526E1153711055720F55740F55770E577A0E59810C58840B58880A588B09588F08589107569307559A05539D0451A1034FA5024BA90147AA0046AC0045B00242B4043DBB0634BE082EC20A29C30B27C50C26C90F1DCC1116D32110D6280EDA300CDC380ADF4109E04508E24A08E45106E75704EA6402EC6B01EE7300EE7600EF7A00F07E00F18300F29000F29300F39600F39900F39C00F3A000F3A100F3A201F2A502F1A805F0A906EFAA08EEA909EEA80AEDA60CEBA50FE5A313E1A113DD9F13DB9E13D99D14D49C15D09815CC9518C79318BE8B1ABB891BB9871DB4811FB07D1FAB7621A671239C6227975C289256299053298E502A89482C853F2D803A2E7E3037762C45742B47722B496E2A4E6A29516728536326565C265C5724595322575022564E2255482153452050451F4E431E4C3F1E473E1D463D1D453F1E43411E413F1E3B3E1E37421D36421D38431D3B451C3A471B3A491B3C4B1A3D4D1A3E4F19405318435418445518455817465A16475D154960154A65134E66124F6812506B12526E1153711055740F55770E577A0E597E0D57810C58840B58880A588B09588F08589307559A05539C04529E0452A1034FA5024BA90147AC0045B00242B4043DB7053ABB0634BE0831C20A29C50C26C90F1DCC1116D01711D32110D72A0EDA300CDD390ADF4109E24A08E45106E75704E95F03EA6402EC6C01EE7300EF7A00F07E00F18300F28900F29000F39300F39600F39C00F3A000F3A100F3A201F2A502F2A503F1A805F0A807EFAA08EEA80AEDA60CEBA50FE9A411E5A313E1A113DD9F13D99D14D49C15D09815CC9518C79318C38F1ABE8B1AB9871DB4811FB07D1FAB7621A67123A16A249C6227975E289256298E502A89482C853F2D803A2E"; +export const paletteBytes = hexToBytes(paletteHex); +console.log(paletteBytes); + +// Re-scale colors to 0-1 (see flam3_get_palette) +export const paletteNumber = paletteBytes.map((b) => b / 0xff); +console.log(paletteNumber); + +export function colorIndex(c: number): number { + return Math.floor(c * (paletteNumber.length / 3)); +} + +export function colorFromIndex(c: number): [number, number, number] { + // A smarter coloring implementation would interpolate between points in the palette, + // but we'll use a step function here to keep things simple + const colorIndex = Math.floor(c * (paletteNumber.length / 3)) * 3; + return [ + paletteNumber[colorIndex + 0], + paletteNumber[colorIndex + 1], + paletteNumber[colorIndex + 2], + ]; +} diff --git a/posts/2023/06/flam3/0-utility.tsx b/posts/2023/06/flam3/0-utility.ts similarity index 59% rename from posts/2023/06/flam3/0-utility.tsx rename to posts/2023/06/flam3/0-utility.ts index 548278f..addea32 100644 --- a/posts/2023/06/flam3/0-utility.tsx +++ b/posts/2023/06/flam3/0-utility.ts @@ -1,36 +1,5 @@ -import React, { useEffect, useRef } from "react"; - export type renderFn = (image: ImageData) => void; -export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => { - const canvasRef = useRef(null); - let image: ImageData | null = null; - useEffect(() => { - if (canvasRef.current) { - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - - if (!ctx) { - return; - } - - if ( - !image || - image.width !== canvas.width || - image.height !== canvas.height - ) { - image = ctx.createImageData(canvas.width, canvas.height); - } - - f(image); - - ctx.putImageData(image, 0, 0); - } - }); - - return ; -}; - export function randomBiUnit() { // Math.random() produces a number in the range [0, 1), // scale to (-1, 1) diff --git a/posts/2023/06/flam3/2a-variations.ts b/posts/2023/06/flam3/2a-variations.ts index a54c747..c7e374d 100644 --- a/posts/2023/06/flam3/2a-variations.ts +++ b/posts/2023/06/flam3/2a-variations.ts @@ -14,6 +14,15 @@ export type Coefs = { f: number; }; +export const identityCoefs = { + a: 1, + b: 0, + c: 0, + d: 0, + e: 1, + f: 0, +}; + function r(x: number, y: number) { return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); } @@ -59,7 +68,7 @@ export class Transform { public readonly variations: [number, Variation][] ) {} - apply(x: number, y: number) { + apply(x: number, y: number): [number, number] { const xformX = this.coefs.a * x + this.coefs.b * y + this.coefs.c; const xformY = this.coefs.d * x + this.coefs.e * y + this.coefs.f; diff --git a/posts/2023/06/flam3/2b-post.ts b/posts/2023/06/flam3/2b-post.ts index 58a9c5d..132b795 100644 --- a/posts/2023/06/flam3/2b-post.ts +++ b/posts/2023/06/flam3/2b-post.ts @@ -14,6 +14,7 @@ import { transform2, transform3Weight, transform3, + identityCoefs, } from "./2a-variations"; export class TransformPost extends Transform { @@ -44,12 +45,15 @@ export function variationPost(coefs: Coefs, variation: Variation): Variation { }; } +export const transform1Post = new TransformPost( + transform1.coefs, + transform1.variations, + identityCoefs +); + export const transform2Post = new TransformPost( transform2.coefs, - [ - [1, linear], - [1, popcorn], - ], + transform2.variations, { a: 1, b: 0, @@ -60,6 +64,12 @@ export const transform2Post = new TransformPost( } ); +export const transform3Post = new TransformPost( + transform3.coefs, + transform3.variations, + identityCoefs +); + export function renderPost(image: ImageData) { const flame = new Flame([ [transform1Weight, transform1], diff --git a/posts/2023/06/flam3/2c-final.ts b/posts/2023/06/flam3/2c-final.ts index 10e47a0..8d12b41 100644 --- a/posts/2023/06/flam3/2c-final.ts +++ b/posts/2023/06/flam3/2c-final.ts @@ -3,13 +3,17 @@ import { Transform, julia, transform1Weight, - transform1, transform2Weight, transform3Weight, - transform3, render, + identityCoefs, } from "./2a-variations"; -import { transform2Post } from "./2b-post"; +import { + TransformPost, + transform1Post, + transform2Post, + transform3Post, +} from "./2b-post"; export class FlameFinal extends Flame { didLog: boolean = false; @@ -21,23 +25,12 @@ export class FlameFinal extends Flame { super(transforms); } - override step(): void { - super.step(); - [this.x, this.y] = this.final.apply(this.x, this.y); - } - override current(): [number, number] { - if (!this.didLog) { - this.didLog = true; - console.trace(`Getting final xform to plot`); - } - // NOTE: The final transform does not modify the iterator point - // return this.final.apply(this.x, this.y); - return [this.x, this.y]; + return this.final.apply(this.x, this.y); } } -export const transformFinal = new Transform( +export const transformFinal = new TransformPost( { a: 2, b: 0, @@ -46,14 +39,15 @@ export const transformFinal = new Transform( e: 2, f: 0, }, - [[1, julia]] + [[1, julia]], + identityCoefs ); export const flameFinal = new FlameFinal( [ - [transform1Weight, transform1], + [transform1Weight, transform1Post], [transform2Weight, transform2Post], - [transform3Weight, transform3], + [transform3Weight, transform3Post], ], transformFinal ); diff --git a/posts/2023/06/flam3/3c-logarithmic.ts b/posts/2023/06/flam3/3c-logarithmic.ts index 7c6939d..217290c 100644 --- a/posts/2023/06/flam3/3c-logarithmic.ts +++ b/posts/2023/06/flam3/3c-logarithmic.ts @@ -32,5 +32,5 @@ export class AccumulateLogarithmic extends Accumulator { export function renderLogarithmic(image: ImageData) { const accumulator = new AccumulateLogarithmic(image.width, image.height); - render(flameFinal, 10, accumulator, image); + render(flameFinal, 20, accumulator, image); } diff --git a/posts/2023/06/flam3/4b-color.ts b/posts/2023/06/flam3/4b-color.ts new file mode 100644 index 0000000..551359f --- /dev/null +++ b/posts/2023/06/flam3/4b-color.ts @@ -0,0 +1,175 @@ +import { colorFromIndex, colorIndex, paletteNumber } from "./0-palette"; +import { + histIndex, + imageIndex, + randomBiUnit, + weightedChoice, +} from "./0-utility"; +import { + Coefs, + Variation, + camera, + transform1Weight, + transform2Weight, + transform3Weight, +} from "./2a-variations"; +import { + TransformPost, + transform1Post, + transform2Post, + transform3Post, +} from "./2b-post"; +import { FlameFinal, transformFinal } from "./2c-final"; + +export class AccumulatorColor { + red: number[] = []; + green: number[] = []; + blue: number[] = []; + alpha: number[] = []; + + constructor(public readonly width: number, public readonly height: number) { + for (var i = 0; i < width * height; i++) { + this.red.push(0); + this.green.push(0); + this.blue.push(0); + this.alpha.push(0); + } + } + + accumulate(x: number, y: number, c: number) { + const [pixelX, pixelY] = camera(x, y, this.width); + + if ( + pixelX < 0 || + pixelX >= this.width || + pixelY < 0 || + pixelY >= this.height + ) { + return; + } + + const hIndex = histIndex(pixelX, pixelY, this.width); + const [r, g, b] = colorFromIndex(c); + + this.red[hIndex] += r; + this.green[hIndex] += g; + this.blue[hIndex] += b; + this.alpha[hIndex] += 1; + } + + render(image: ImageData) { + for (var x = 0; x < image.width; x++) { + for (var y = 0; y < image.height; y++) { + const hIndex = histIndex(x, y, image.width); + + const aNorm = this.alpha[hIndex] ? this.alpha[hIndex] : 1; + const aScale = Math.log10(aNorm) / (aNorm * 1.5); + + const iIdx = imageIndex(x, y, this.width); + image.data[iIdx + 0] = this.red[hIndex] * aScale * 0xff; + image.data[iIdx + 1] = this.green[hIndex] * aScale * 0xff; + image.data[iIdx + 2] = this.blue[hIndex] * aScale * 0xff; + image.data[iIdx + 3] = this.alpha[hIndex] * aScale * 0xff; + } + } + } +} + +export class TransformColor extends TransformPost { + constructor( + coefs: Coefs, + variations: [number, Variation][], + post: Coefs, + public readonly color: number + ) { + super(coefs, variations, post); + } +} + +export class FlameColor extends FlameFinal { + protected color: number = Math.random(); + + constructor(transforms: [number, TransformColor][], final: TransformColor) { + super(transforms, final); + } + + step() { + const [_index, transform] = weightedChoice(this.transforms); + [this.x, this.y] = transform.apply(this.x, this.y); + const transformColor = (transform as TransformColor).color; + this.color = (this.color + transformColor) / 2; + } + + currentWithColor(): [number, number, number] { + const [finalX, finalY] = this.final.apply(this.x, this.y); + // TODO(bspeice): Why does everyone ignore final coloring? + // In `flam3`, the `color_speed` is set to 0 for the final xform, + // so it doesn't actually get used. + return [finalX, finalY, this.color]; + } +} + +function render( + flame: FlameColor, + quality: number, + accumulator: AccumulatorColor, + image: ImageData +) { + const iterations = quality * image.width * image.height; + + for (var i = 0; i < iterations; i++) { + flame.step(); + + if (i > 20) { + const [flameX, flameY, color] = flame.currentWithColor(); + accumulator.accumulate(flameX, flameY, color); + } + } + + accumulator.render(image); +} + +export const transform1ColorValue = 0; +export const transform1Color = new TransformColor( + transform1Post.coefs, + transform1Post.variations, + transform1Post.post, + transform1ColorValue +); +export const transform2ColorValue = 0.844; +export const transform2Color = new TransformColor( + transform2Post.coefs, + transform2Post.variations, + transform2Post.post, + transform2ColorValue +); + +export const transform3ColorValue = 0.349; +export const transform3Color = new TransformColor( + transform3Post.coefs, + transform3Post.variations, + transform3Post.post, + transform3ColorValue +); + +export const transformFinalColorValue = 0; +export const transformFinalColor = new TransformColor( + transformFinal.coefs, + transformFinal.variations, + transformFinal.post, + transformFinalColorValue +); + +export const flameColor = new FlameColor( + [ + [transform1Weight, transform1Color], + [transform2Weight, transform2Color], + [transform3Weight, transform3Color], + ], + transformFinalColor +); + +export function renderColor(image: ImageData) { + const accumulator = new AccumulatorColor(image.width, image.height); + render(flameColor, 40, accumulator, image); +} diff --git a/posts/2023/06/flam3/index.tsx b/posts/2023/06/flam3/index.tsx index 3166430..0d114f0 100644 --- a/posts/2023/06/flam3/index.tsx +++ b/posts/2023/06/flam3/index.tsx @@ -1,6 +1,6 @@ import Blog from "../../../LayoutBlog"; -import { Canvas } from "./0-utility"; +import { Canvas } from "./0-canvas"; import { gasket } from "./1-gasket"; import { renderBaseline } from "./2a-variations"; import { renderPost } from "./2b-post"; @@ -13,6 +13,7 @@ import { renderTransform2, renderTransform3, } from "./4a-solo"; +import { renderColor } from "./4b-color"; export default function () { const Layout = Blog({ @@ -31,12 +32,9 @@ export default function () {
- +
- {/* - - */} ); }