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>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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
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 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)
|
@ -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;
|
||||||
|
|
||||||
|
@ -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],
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user