mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 08:38:09 -05:00
Implement color
This commit is contained in:
parent
ea946b2ae9
commit
90ac3559ae
@ -4,7 +4,7 @@ export const Page = () => (
|
||||
<>
|
||||
<p>Is this thing on?</p>
|
||||
<p>
|
||||
<a href="/2019/02/the-whole-world">Code</a>
|
||||
<a href="/2023/06/flam3">Code</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
31
posts/2023/06/flam3/0-canvas.tsx
Normal file
31
posts/2023/06/flam3/0-canvas.tsx
Normal 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} />;
|
||||
};
|
33
posts/2023/06/flam3/0-palette.ts
Normal file
33
posts/2023/06/flam3/0-palette.ts
Normal 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],
|
||||
];
|
||||
}
|
@ -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<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() {
|
||||
// Math.random() produces a number in the range [0, 1),
|
||||
// scale to (-1, 1)
|
@ -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;
|
||||
|
||||
|
@ -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],
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
|
175
posts/2023/06/flam3/4b-color.ts
Normal file
175
posts/2023/06/flam3/4b-color.ts
Normal 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);
|
||||
}
|
@ -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 () {
|
||||
<Canvas f={renderFinal} />
|
||||
</div>
|
||||
<div>
|
||||
<Canvas f={renderLinear} />
|
||||
<Canvas f={renderLogarithmic} />
|
||||
<Canvas f={renderColor} />
|
||||
</div>
|
||||
{/* <Canvas f={renderTransform1} />
|
||||
<Canvas f={renderTransform2} />
|
||||
<Canvas f={renderTransform3} /> */}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user