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 () {
-
+
- {/*
-
- */}
);
}