mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Refactor, fix julia bug, implement binary/linear/logarithmic membership
This commit is contained in:
parent
e8ec0e0521
commit
671b97b7a0
@ -31,6 +31,40 @@ export const Canvas: React.FC<{ f: renderFn }> = ({ f }) => {
|
|||||||
return <canvas ref={canvasRef} width={400} height={400} />;
|
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)
|
||||||
|
return Math.random() * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
export function randomInteger(min: number, max: number) {
|
export function randomInteger(min: number, max: number) {
|
||||||
return Math.floor(Math.random() * (max - min)) + min;
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function weightedChoice<T>(choices: [number, T][]) {
|
||||||
|
const weightSum = choices.reduce(
|
||||||
|
(current, [weight, _t]) => current + weight,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
var choice = Math.random() * weightSum;
|
||||||
|
|
||||||
|
for (var i = 0; i < choices.length; i++) {
|
||||||
|
const [weight, t] = choices[i];
|
||||||
|
if (choice < weight) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
choice -= weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "unreachable";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imageIndex(x: number, y: number, width: number) {
|
||||||
|
// Taken from: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
|
||||||
|
return y * (width * 4) + x * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function histIndex(x: number, y: number, width: number) {
|
||||||
|
return y * width + x;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { randomInteger, renderFn } from "./0-utility";
|
import { randomBiUnit, randomInteger, renderFn, imageIndex } from "./0-utility";
|
||||||
|
|
||||||
const ITER = 100_000;
|
|
||||||
|
|
||||||
function plot(x: number, y: number, image: ImageData) {
|
function plot(x: number, y: number, image: ImageData) {
|
||||||
// A trivial `plot` implementation would take the range [-1, 1],
|
// A trivial `plot` implementation would take the range [-1, 1],
|
||||||
@ -19,10 +17,8 @@ function plot(x: number, y: number, image: ImageData) {
|
|||||||
var pixelX = Math.floor((-x + 1) * image.width);
|
var pixelX = Math.floor((-x + 1) * image.width);
|
||||||
var pixelY = Math.floor((-y + 1) * image.height);
|
var pixelY = Math.floor((-y + 1) * image.height);
|
||||||
|
|
||||||
// Now translate the (x, y) pixel coordinates to a buffer index
|
// Set the pixel black:
|
||||||
// and paint it black:
|
const index = imageIndex(pixelX, pixelY, image.width);
|
||||||
const index = pixelY * (image.width * 4) + pixelX * 4;
|
|
||||||
|
|
||||||
image.data[index + 0] = 0;
|
image.data[index + 0] = 0;
|
||||||
image.data[index + 1] = 0;
|
image.data[index + 1] = 0;
|
||||||
image.data[index + 2] = 0;
|
image.data[index + 2] = 0;
|
||||||
@ -44,13 +40,13 @@ export const gasket: renderFn = (image) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let x = Math.random() * 2 - 1;
|
let x = randomBiUnit();
|
||||||
let y = Math.random() * 2 - 1;
|
let y = randomBiUnit();
|
||||||
|
|
||||||
// Heuristic for iteration count
|
// Plot with quality 1
|
||||||
const iter = image.height * image.width;
|
const iterations = image.height * image.width;
|
||||||
|
|
||||||
for (var i = 0; i < iter; i++) {
|
for (var i = 0; i < iterations; i++) {
|
||||||
const Fi = randomInteger(0, F.length);
|
const Fi = randomInteger(0, F.length);
|
||||||
[x, y] = F[Fi](x, y);
|
[x, y] = F[Fi](x, y);
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { randomBiUnit, weightedChoice } from "./0-utility";
|
||||||
|
|
||||||
export type Variation = (
|
export type Variation = (
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
@ -17,7 +19,7 @@ function r(x: number, y: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function theta(x: number, y: number) {
|
function theta(x: number, y: number) {
|
||||||
return Math.atan2(y, x);
|
return Math.atan2(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function omega(): number {
|
function omega(): number {
|
||||||
@ -28,10 +30,8 @@ export const linear: Variation = (x, y) => [x, y];
|
|||||||
|
|
||||||
export const julia: Variation = (x, y) => {
|
export const julia: Variation = (x, y) => {
|
||||||
const sqrtR = Math.sqrt(r(x, y));
|
const sqrtR = Math.sqrt(r(x, y));
|
||||||
return [
|
const thetaVal = theta(x, y) / 2 + omega();
|
||||||
sqrtR * Math.cos(theta(x, y) / 2 + omega()),
|
return [sqrtR * Math.cos(thetaVal), sqrtR * Math.sin(thetaVal)];
|
||||||
sqrtR * Math.sin(theta(x, y) / 2 + omega()),
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const popcorn: Variation = (x, y, transformCoefs) => {
|
export const popcorn: Variation = (x, y, transformCoefs) => {
|
||||||
@ -74,28 +74,9 @@ export class Transform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function weightedChoice<T>(choices: [number, T][]) {
|
|
||||||
const weightSum = choices.reduce(
|
|
||||||
(current, [weight, _t]) => current + weight,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
var choice = Math.random() * weightSum;
|
|
||||||
|
|
||||||
for (var i = 0; i < choices.length; i++) {
|
|
||||||
const [weight, t] = choices[i];
|
|
||||||
if (choice < weight) {
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
choice -= weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw "unreachable";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Flame {
|
export class Flame {
|
||||||
x: number = Math.random() * 2 - 1;
|
protected x: number = randomBiUnit();
|
||||||
y: number = Math.random() * 2 - 1;
|
protected y: number = randomBiUnit();
|
||||||
|
|
||||||
constructor(public readonly transforms: [number, Transform][]) {}
|
constructor(public readonly transforms: [number, Transform][]) {}
|
||||||
|
|
||||||
@ -103,17 +84,35 @@ export class Flame {
|
|||||||
const transform = weightedChoice(this.transforms);
|
const transform = weightedChoice(this.transforms);
|
||||||
[this.x, this.y] = transform.apply(this.x, this.y);
|
[this.x, this.y] = transform.apply(this.x, this.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
current() {
|
||||||
|
return [this.x, this.y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function camera(x: number, y: number, size: number): [number, number] {
|
||||||
|
// Assuming both:
|
||||||
|
// - The origin is the intended center of the output image
|
||||||
|
// - The output image is square
|
||||||
|
// ...then map points in the range (-scale, scale) to pixel coordinates.
|
||||||
|
//
|
||||||
|
// The way `flam3` actually calculates the "camera" for taking a point
|
||||||
|
// and determining which pixel to update is fairly involved. The example
|
||||||
|
// fractal was designed in Apophysis (which shows points in the range
|
||||||
|
// [-2, 2] by default) so we use that assumption to simplify the math here.
|
||||||
|
return [Math.floor(((x + 2) * size) / 4), Math.floor(((y + 2) * size) / 4)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function plot(x: number, y: number, image: ImageData) {
|
export function plot(x: number, y: number, image: ImageData) {
|
||||||
const pixelX = Math.floor(((x + 2) * image.width) / 4);
|
// "Zoom out" the camera by a factor of 2 to match the default Apophysis scaling
|
||||||
const pixelY = Math.floor(((y + 2) * image.height) / 4);
|
// (plot all points in the range [-2, 2])
|
||||||
|
const [pixelX, pixelY] = camera(x, y, image.width);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pixelX < 0 ||
|
pixelX < 0 ||
|
||||||
pixelX > image.width ||
|
pixelX >= image.width ||
|
||||||
pixelY < 0 ||
|
pixelY < 0 ||
|
||||||
pixelY > image.height
|
pixelY >= image.height
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -132,7 +131,8 @@ export function render(flame: Flame, quality: number, image: ImageData) {
|
|||||||
for (var i = 0; i < iterations; i++) {
|
for (var i = 0; i < iterations; i++) {
|
||||||
flame.step();
|
flame.step();
|
||||||
if (i > 20) {
|
if (i > 20) {
|
||||||
plot(flame.x, flame.y, image);
|
const [flameX, flameY] = flame.current();
|
||||||
|
plot(flameX, flameY, image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,7 +167,6 @@ export const transform2 = new Transform(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const transform3Weight = 0.42233;
|
export const transform3Weight = 0.42233;
|
||||||
export const transform3Pdj = [1.09358, 2.13048, 2.54127, 2.37267] as const;
|
|
||||||
export const transform3 = new Transform(
|
export const transform3 = new Transform(
|
||||||
{
|
{
|
||||||
a: 1.51523,
|
a: 1.51523,
|
||||||
@ -177,7 +176,7 @@ export const transform3 = new Transform(
|
|||||||
e: -1.455964,
|
e: -1.455964,
|
||||||
f: -0.362059,
|
f: -0.362059,
|
||||||
},
|
},
|
||||||
[[1, pdj(...transform3Pdj)]]
|
[[1, pdj(1.09358, 2.13048, 2.54127, 2.37267)]]
|
||||||
);
|
);
|
||||||
|
|
||||||
export function renderBaseline(image: ImageData) {
|
export function renderBaseline(image: ImageData) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Coefs,
|
|
||||||
Flame,
|
Flame,
|
||||||
Transform,
|
Transform,
|
||||||
julia,
|
julia,
|
||||||
@ -13,6 +12,8 @@ import {
|
|||||||
import { transform2Post } from "./2b-post";
|
import { transform2Post } from "./2b-post";
|
||||||
|
|
||||||
export class FlameFinal extends Flame {
|
export class FlameFinal extends Flame {
|
||||||
|
didLog: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
transforms: [number, Transform][],
|
transforms: [number, Transform][],
|
||||||
public readonly final: Transform
|
public readonly final: Transform
|
||||||
@ -20,10 +21,20 @@ export class FlameFinal extends Flame {
|
|||||||
super(transforms);
|
super(transforms);
|
||||||
}
|
}
|
||||||
|
|
||||||
step() {
|
override step(): void {
|
||||||
super.step();
|
super.step();
|
||||||
[this.x, this.y] = this.final.apply(this.x, this.y);
|
[this.x, this.y] = this.final.apply(this.x, this.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override current() {
|
||||||
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const transformFinal = new Transform(
|
export const transformFinal = new Transform(
|
||||||
@ -38,15 +49,15 @@ export const transformFinal = new Transform(
|
|||||||
[[1, julia]]
|
[[1, julia]]
|
||||||
);
|
);
|
||||||
|
|
||||||
export function renderFinal(image: ImageData) {
|
export const flameFinal = new FlameFinal(
|
||||||
const flame = new FlameFinal(
|
[
|
||||||
[
|
[transform1Weight, transform1],
|
||||||
[transform1Weight, transform1],
|
[transform2Weight, transform2Post],
|
||||||
[transform2Weight, transform2Post],
|
[transform3Weight, transform3],
|
||||||
[transform3Weight, transform3],
|
],
|
||||||
],
|
transformFinal
|
||||||
transformFinal
|
);
|
||||||
);
|
|
||||||
|
|
||||||
render(flame, 1, image);
|
export function renderFinal(image: ImageData) {
|
||||||
|
render(flameFinal, 1, image);
|
||||||
}
|
}
|
||||||
|
78
posts/2023/06/flam3/3a-binary.ts
Normal file
78
posts/2023/06/flam3/3a-binary.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { histIndex, imageIndex } from "./0-utility";
|
||||||
|
import { camera } from "./2a-variations";
|
||||||
|
import { flameFinal, FlameFinal } from "./2c-final";
|
||||||
|
|
||||||
|
export abstract class Accumulator {
|
||||||
|
histogram: number[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly width: number,
|
||||||
|
protected readonly height: number
|
||||||
|
) {
|
||||||
|
for (var i = 0; i < width * height; i++) {
|
||||||
|
this.histogram.push(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulate(x: number, y: number) {
|
||||||
|
const [pixelX, pixelY] = camera(x, y, this.width);
|
||||||
|
|
||||||
|
if (
|
||||||
|
pixelX < 0 ||
|
||||||
|
pixelX >= this.width ||
|
||||||
|
pixelY < 0 ||
|
||||||
|
pixelY >= this.height
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = histIndex(pixelX, pixelY, this.width);
|
||||||
|
this.histogram[index] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract render(image: ImageData): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccumulateBinary extends Accumulator {
|
||||||
|
render(image: ImageData) {
|
||||||
|
for (var x = 0; x < image.width; x++) {
|
||||||
|
for (var y = 0; y < image.height; y++) {
|
||||||
|
const index = histIndex(x, y, image.width);
|
||||||
|
|
||||||
|
// Color black if this pixel is part of the solution set, white otherwise
|
||||||
|
const value = this.histogram[index] > 0 ? 0 : 0xff;
|
||||||
|
|
||||||
|
const iIdx = imageIndex(x, y, image.width);
|
||||||
|
image.data[iIdx + 0] = value;
|
||||||
|
image.data[iIdx + 1] = value;
|
||||||
|
image.data[iIdx + 2] = value;
|
||||||
|
image.data[iIdx + 3] = 0xff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render(
|
||||||
|
flame: FlameFinal,
|
||||||
|
quality: number,
|
||||||
|
accumulator: Accumulator,
|
||||||
|
image: ImageData
|
||||||
|
) {
|
||||||
|
const iterations = quality * image.width * image.height;
|
||||||
|
|
||||||
|
for (var i = 0; i < iterations; i++) {
|
||||||
|
flame.step();
|
||||||
|
|
||||||
|
if (i > 20) {
|
||||||
|
const [flameX, flameY] = flame.current();
|
||||||
|
accumulator.accumulate(flameX, flameY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulator.render(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderBinary(image: ImageData) {
|
||||||
|
const accumulator = new AccumulateBinary(image.width, image.height);
|
||||||
|
render(flameFinal, 10, accumulator, image);
|
||||||
|
}
|
30
posts/2023/06/flam3/3b-linear.ts
Normal file
30
posts/2023/06/flam3/3b-linear.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { histIndex, imageIndex } from "./0-utility";
|
||||||
|
import { flameFinal } from "./2c-final";
|
||||||
|
import { Accumulator, render } from "./3a-binary";
|
||||||
|
|
||||||
|
export class AccumulateLinear extends Accumulator {
|
||||||
|
render(image: ImageData): void {
|
||||||
|
const maxValue = Math.max(...this.histogram);
|
||||||
|
|
||||||
|
for (var x = 0; x < image.width; x++) {
|
||||||
|
for (var y = 0; y < image.height; y++) {
|
||||||
|
const index = histIndex(x, y, image.width);
|
||||||
|
|
||||||
|
// Color full black if this pixel is maxValue, white if not part
|
||||||
|
// of the solution set
|
||||||
|
const value = (1 - this.histogram[index] / maxValue) * 0xff;
|
||||||
|
|
||||||
|
const iIdx = imageIndex(x, y, image.width);
|
||||||
|
image.data[iIdx + 0] = value;
|
||||||
|
image.data[iIdx + 1] = value;
|
||||||
|
image.data[iIdx + 2] = value;
|
||||||
|
image.data[iIdx + 3] = 0xff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderLinear(image: ImageData) {
|
||||||
|
const accumulator = new AccumulateLinear(image.width, image.height);
|
||||||
|
render(flameFinal, 10, accumulator, image);
|
||||||
|
}
|
36
posts/2023/06/flam3/3c-logarithmic.ts
Normal file
36
posts/2023/06/flam3/3c-logarithmic.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { histIndex, imageIndex } from "./0-utility";
|
||||||
|
import { flameFinal } from "./2c-final";
|
||||||
|
import { Accumulator, render } from "./3a-binary";
|
||||||
|
|
||||||
|
export class AccumulateLogarithmic extends Accumulator {
|
||||||
|
render(image: ImageData): void {
|
||||||
|
// Re-scale vibrancy to be log scale...
|
||||||
|
for (var i = 0; i < this.histogram.length; i++) {
|
||||||
|
this.histogram[i] = Math.log(this.histogram[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...but otherwise render the same way as linear
|
||||||
|
const maxValue = Math.max(...this.histogram);
|
||||||
|
|
||||||
|
for (var x = 0; x < image.width; x++) {
|
||||||
|
for (var y = 0; y < image.height; y++) {
|
||||||
|
const index = histIndex(x, y, image.width);
|
||||||
|
|
||||||
|
// Color full black if this pixel is maxValue, white if not part
|
||||||
|
// of the solution set
|
||||||
|
const value = (1 - this.histogram[index] / maxValue) * 0xff;
|
||||||
|
|
||||||
|
const iIdx = imageIndex(x, y, image.width);
|
||||||
|
image.data[iIdx + 0] = value;
|
||||||
|
image.data[iIdx + 1] = value;
|
||||||
|
image.data[iIdx + 2] = value;
|
||||||
|
image.data[iIdx + 3] = 0xff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderLogarithmic(image: ImageData) {
|
||||||
|
const accumulator = new AccumulateLogarithmic(image.width, image.height);
|
||||||
|
render(flameFinal, 10, accumulator, image);
|
||||||
|
}
|
@ -5,6 +5,9 @@ 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";
|
||||||
import { renderFinal } from "./2c-final";
|
import { renderFinal } from "./2c-final";
|
||||||
|
import { renderBinary } from "./3a-binary";
|
||||||
|
import { renderLinear } from "./3b-linear";
|
||||||
|
import { renderLogarithmic } from "./3c-logarithmic";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const Layout = Blog({
|
const Layout = Blog({
|
||||||
@ -14,14 +17,17 @@ export default function () {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div>
|
{/* <div>
|
||||||
<Canvas f={gasket} />
|
<Canvas f={gasket} />
|
||||||
<Canvas f={renderBaseline} />
|
<Canvas f={renderBaseline} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Canvas f={renderPost} />
|
<Canvas f={renderPost} />
|
||||||
<Canvas f={renderFinal} />
|
<Canvas f={renderFinal} />
|
||||||
</div>
|
</div> */}
|
||||||
|
<Canvas f={renderBinary} />
|
||||||
|
<Canvas f={renderLinear} />
|
||||||
|
<Canvas f={renderLogarithmic} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user