Implement color

This commit is contained in:
Bradlee Speice 2023-07-15 22:48:06 +00:00
parent ea946b2ae9
commit 90ac3559ae
10 changed files with 281 additions and 62 deletions

View File

@ -4,7 +4,7 @@ export const Page = () => (
<> <>
<p>Is this thing on?</p> <p>Is this thing on?</p>
<p> <p>
<a href="/2019/02/the-whole-world">Code</a> <a href="/2023/06/flam3">Code</a>
</p> </p>
</> </>
); );

View File

@ -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<HTMLCanvasElement | null>(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 <canvas ref={canvasRef} width={400} height={400} />;
};

View File

@ -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],
];
}

View File

@ -1,36 +1,5 @@
import React, { useEffect, useRef } from "react";
export type renderFn = (image: ImageData) => void; export type renderFn = (image: ImageData) => void;
export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => {
const canvasRef = useRef<HTMLCanvasElement | null>(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 <canvas ref={canvasRef} width={400} height={400} />;
};
export function randomBiUnit() { export function randomBiUnit() {
// Math.random() produces a number in the range [0, 1), // Math.random() produces a number in the range [0, 1),
// scale to (-1, 1) // scale to (-1, 1)

View File

@ -14,6 +14,15 @@ export type Coefs = {
f: number; f: number;
}; };
export const identityCoefs = {
a: 1,
b: 0,
c: 0,
d: 0,
e: 1,
f: 0,
};
function r(x: number, y: number) { function r(x: number, y: number) {
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
} }
@ -59,7 +68,7 @@ export class Transform {
public readonly variations: [number, Variation][] 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 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; const xformY = this.coefs.d * x + this.coefs.e * y + this.coefs.f;

View File

@ -14,6 +14,7 @@ import {
transform2, transform2,
transform3Weight, transform3Weight,
transform3, transform3,
identityCoefs,
} from "./2a-variations"; } from "./2a-variations";
export class TransformPost extends Transform { 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( export const transform2Post = new TransformPost(
transform2.coefs, transform2.coefs,
[ transform2.variations,
[1, linear],
[1, popcorn],
],
{ {
a: 1, a: 1,
b: 0, 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) { export function renderPost(image: ImageData) {
const flame = new Flame([ const flame = new Flame([
[transform1Weight, transform1], [transform1Weight, transform1],

View File

@ -3,13 +3,17 @@ import {
Transform, Transform,
julia, julia,
transform1Weight, transform1Weight,
transform1,
transform2Weight, transform2Weight,
transform3Weight, transform3Weight,
transform3,
render, render,
identityCoefs,
} from "./2a-variations"; } from "./2a-variations";
import { transform2Post } from "./2b-post"; import {
TransformPost,
transform1Post,
transform2Post,
transform3Post,
} from "./2b-post";
export class FlameFinal extends Flame { export class FlameFinal extends Flame {
didLog: boolean = false; didLog: boolean = false;
@ -21,23 +25,12 @@ export class FlameFinal extends Flame {
super(transforms); super(transforms);
} }
override step(): void {
super.step();
[this.x, this.y] = this.final.apply(this.x, this.y);
}
override current(): [number, number] { override current(): [number, number] {
if (!this.didLog) { return this.final.apply(this.x, this.y);
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];
} }
} }
export const transformFinal = new Transform( export const transformFinal = new TransformPost(
{ {
a: 2, a: 2,
b: 0, b: 0,
@ -46,14 +39,15 @@ export const transformFinal = new Transform(
e: 2, e: 2,
f: 0, f: 0,
}, },
[[1, julia]] [[1, julia]],
identityCoefs
); );
export const flameFinal = new FlameFinal( export const flameFinal = new FlameFinal(
[ [
[transform1Weight, transform1], [transform1Weight, transform1Post],
[transform2Weight, transform2Post], [transform2Weight, transform2Post],
[transform3Weight, transform3], [transform3Weight, transform3Post],
], ],
transformFinal transformFinal
); );

View File

@ -32,5 +32,5 @@ export class AccumulateLogarithmic extends Accumulator {
export function renderLogarithmic(image: ImageData) { export function renderLogarithmic(image: ImageData) {
const accumulator = new AccumulateLogarithmic(image.width, image.height); const accumulator = new AccumulateLogarithmic(image.width, image.height);
render(flameFinal, 10, accumulator, image); render(flameFinal, 20, accumulator, image);
} }

View File

@ -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);
}

View File

@ -1,6 +1,6 @@
import Blog from "../../../LayoutBlog"; import Blog from "../../../LayoutBlog";
import { Canvas } from "./0-utility"; import { Canvas } from "./0-canvas";
import { gasket } from "./1-gasket"; import { gasket } from "./1-gasket";
import { renderBaseline } from "./2a-variations"; import { renderBaseline } from "./2a-variations";
import { renderPost } from "./2b-post"; import { renderPost } from "./2b-post";
@ -13,6 +13,7 @@ import {
renderTransform2, renderTransform2,
renderTransform3, renderTransform3,
} from "./4a-solo"; } from "./4a-solo";
import { renderColor } from "./4b-color";
export default function () { export default function () {
const Layout = Blog({ const Layout = Blog({
@ -31,12 +32,9 @@ export default function () {
<Canvas f={renderFinal} /> <Canvas f={renderFinal} />
</div> </div>
<div> <div>
<Canvas f={renderLinear} />
<Canvas f={renderLogarithmic} /> <Canvas f={renderLogarithmic} />
<Canvas f={renderColor} />
</div> </div>
{/* <Canvas f={renderTransform1} />
<Canvas f={renderTransform2} />
<Canvas f={renderTransform3} /> */}
</Layout> </Layout>
); );
} }