mirror of
https://github.com/bspeice/speice.io
synced 2025-07-12 03:04:54 -04:00
Implement offline rendering, default to pre-render
This commit is contained in:
223
posts/2023/06/flam3/2a-baseline.ts
Normal file
223
posts/2023/06/flam3/2a-baseline.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import {
|
||||
RenderParams,
|
||||
Renderer,
|
||||
histIndex,
|
||||
imageIndex,
|
||||
randomBiUnit,
|
||||
weightedChoice,
|
||||
} from "./0-utility.js";
|
||||
|
||||
export type Variation = (
|
||||
x: number,
|
||||
y: number,
|
||||
transformCoefs: Coefs
|
||||
) => [number, number];
|
||||
|
||||
export type Coefs = {
|
||||
a: number;
|
||||
b: number;
|
||||
c: number;
|
||||
d: number;
|
||||
e: number;
|
||||
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));
|
||||
}
|
||||
|
||||
function theta(x: number, y: number) {
|
||||
return Math.atan2(x, y);
|
||||
}
|
||||
|
||||
function omega(): number {
|
||||
return Math.random() > 0.5 ? Math.PI : 0;
|
||||
}
|
||||
|
||||
export const linear: Variation = (x, y) => [x, y];
|
||||
|
||||
export const julia: Variation = (x, y) => {
|
||||
const sqrtR = Math.sqrt(r(x, y));
|
||||
const thetaVal = theta(x, y) / 2 + omega();
|
||||
return [sqrtR * Math.cos(thetaVal), sqrtR * Math.sin(thetaVal)];
|
||||
};
|
||||
|
||||
export const popcorn: Variation = (x, y, transformCoefs) => {
|
||||
return [
|
||||
x + transformCoefs.c * Math.sin(Math.tan(3 * y)),
|
||||
y + transformCoefs.f * Math.sin(Math.tan(3 * x)),
|
||||
];
|
||||
};
|
||||
|
||||
export const pdj: (
|
||||
pdjA: number,
|
||||
pdjB: number,
|
||||
pdjC: number,
|
||||
pdjD: number
|
||||
) => Variation = (pdjA, pdjB, pdjC, pdjD) => {
|
||||
return (x, y) => [
|
||||
Math.sin(pdjA * y) - Math.cos(pdjB * x),
|
||||
Math.sin(pdjC * x) - Math.cos(pdjD * y),
|
||||
];
|
||||
};
|
||||
|
||||
export class Transform {
|
||||
constructor(
|
||||
public readonly coefs: Coefs,
|
||||
public readonly variations: [number, Variation][]
|
||||
) {}
|
||||
|
||||
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;
|
||||
|
||||
var [curX, curY] = [0, 0];
|
||||
this.variations.forEach(([blend, variation]) => {
|
||||
const [varX, varY] = variation(xformX, xformY, this.coefs);
|
||||
curX += blend * varX;
|
||||
curY += blend * varY;
|
||||
});
|
||||
|
||||
return [curX, curY];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate values in the flame coordinate system to pixel coordinates
|
||||
*
|
||||
* The way `flam3` actually calculates the "camera" for mapping a point
|
||||
* to its pixel coordinate is fairly involved - it also needs to calculate
|
||||
* zoom and rotation. We'll make some simplifying assumptions:
|
||||
* - The final image is square
|
||||
* - We want to plot the range [-2, 2]
|
||||
*
|
||||
* The reference parameters were designed in Apophysis, which uses the
|
||||
* range [-2, 2] by default (the `scale` parameter in XML defines the
|
||||
* "pixels per unit", and with the default zoom, is chosen to give a
|
||||
* range of [-2, 2]).
|
||||
*
|
||||
* @param x point in the range [-2, 2]
|
||||
* @param y point in the range [-2, 2]
|
||||
* @param size image size
|
||||
* @returns pair of pixel coordinates
|
||||
*/
|
||||
export function camera(x: number, y: number, size: number): [number, number] {
|
||||
return [Math.floor(((x + 2) * size) / 4), Math.floor(((y + 2) * size) / 4)];
|
||||
}
|
||||
|
||||
export class RendererFlame extends Renderer {
|
||||
private values = new Uint8Array(this.size * this.size);
|
||||
protected x = randomBiUnit();
|
||||
protected y = randomBiUnit();
|
||||
|
||||
constructor(size: number, public readonly transforms: [number, Transform][]) {
|
||||
super(size);
|
||||
}
|
||||
|
||||
plot(x: number, y: number) {
|
||||
const [pixelX, pixelY] = camera(x, y, this.size);
|
||||
|
||||
if (
|
||||
pixelX < 0 ||
|
||||
pixelX >= this.size ||
|
||||
pixelY < 0 ||
|
||||
pixelY >= this.size
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hIndex = histIndex(pixelX, pixelY, this.size);
|
||||
this.values[hIndex] = 1;
|
||||
}
|
||||
|
||||
run(quality: number): void {
|
||||
const iterations = quality * this.size * this.size;
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
const [_, transform] = weightedChoice(this.transforms);
|
||||
[this.x, this.y] = transform.apply(this.x, this.y);
|
||||
|
||||
if (i > 20) {
|
||||
this.plot(this.x, this.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(image: ImageData): void {
|
||||
for (var x = 0; x < this.size; x++) {
|
||||
for (var y = 0; y < this.size; y++) {
|
||||
const hIndex = histIndex(x, y, this.size);
|
||||
if (!this.values[hIndex]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const iIndex = imageIndex(x, y, this.size);
|
||||
image.data[iIndex + 0] = 0;
|
||||
image.data[iIndex + 1] = 0;
|
||||
image.data[iIndex + 2] = 0;
|
||||
image.data[iIndex + 3] = 0xff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const transform1Weight = 0.56453495;
|
||||
export const transform1 = new Transform(
|
||||
{
|
||||
a: -1.381068,
|
||||
b: -1.381068,
|
||||
c: 0,
|
||||
d: 1.381068,
|
||||
e: -1.381068,
|
||||
f: 0,
|
||||
},
|
||||
[[1, julia]]
|
||||
);
|
||||
|
||||
export const transform2Weight = 0.013135;
|
||||
export const transform2 = new Transform(
|
||||
{
|
||||
a: 0.031393,
|
||||
b: 0.031367,
|
||||
c: 0,
|
||||
d: -0.031367,
|
||||
e: 0.031393,
|
||||
f: 0,
|
||||
},
|
||||
[
|
||||
[1, linear],
|
||||
[1, popcorn],
|
||||
]
|
||||
);
|
||||
|
||||
export const transform3Weight = 0.42233;
|
||||
export const transform3 = new Transform(
|
||||
{
|
||||
a: 1.51523,
|
||||
b: -3.048677,
|
||||
c: 0.724135,
|
||||
d: 0.740356,
|
||||
e: -1.455964,
|
||||
f: -0.362059,
|
||||
},
|
||||
[[1, pdj(1.09358, 2.13048, 2.54127, 2.37267)]]
|
||||
);
|
||||
|
||||
export const transformAll: [number, Transform][] = [
|
||||
[transform1Weight, transform1],
|
||||
[transform2Weight, transform2],
|
||||
[transform3Weight, transform3],
|
||||
];
|
||||
|
||||
export const paramsBaseline: RenderParams = {
|
||||
quality: 1,
|
||||
renderer: (size) => new RendererFlame(size, transformAll),
|
||||
};
|
Reference in New Issue
Block a user