speice.io/posts/2023/06/flam3/1-gasket.ts

96 lines
2.6 KiB
TypeScript

import {
randomBiUnit,
randomInteger,
imageIndex,
Renderer,
histIndex,
RenderParams,
} from "./0-utility.js";
type Transform = (x: number, y: number) => [number, number];
export class RendererGasket extends Renderer {
private values = new Uint8Array(this.size * this.size);
private x = randomBiUnit();
private y = randomBiUnit();
/**
* Translate values in the flame coordinate system to pixel coordinates
*
* A trivial implementation would take the range [-1, 1], shift it to [0, 2],
* then scale by the image size:
* pixelX = Math.floor((x + 1) * this.size / 2)
* pixelY = Math.floor((y + 1) * this.size / 2)
*
* However, because the gasket solution set only has values in the range [0, 1],
* that would lead to wasting 3/4 of the pixels. We'll instead plot just the range
* we care about:
* pixelX = Math.floor(x * this.size)
* pixelY = Math.floor(x * this.size)
*
* @param x point in the range [-1, 1]
* @param y point in the range [-1, 1]
*/
plot(x: number, y: number): void {
var pixelX = Math.floor(x * this.size);
var pixelY = Math.floor(y * this.size);
if (
pixelX < 0 ||
pixelX >= this.size ||
pixelY < 0 ||
pixelY >= this.size
) {
return;
}
const index = histIndex(pixelX, pixelY, this.size);
this.values[index] = 1;
}
run(quality: number): void {
const transforms: Transform[] = [
(x, y) => [x / 2, y / 2],
(x, y) => [(x + 1) / 2, y / 2],
(x, y) => [x / 2, (y + 1) / 2],
];
// NOTE: `x` and `y` are set as fields on this class
// (rather than here in the main chaos game method) because
// we render in chunks (to avoid bogging down the browser)
const iterations = quality * this.size * this.size;
for (var i = 0; i < iterations; i++) {
const transformIndex = randomInteger(0, transforms.length);
[this.x, this.y] = transforms[transformIndex](this.x, this.y);
if (i >= 20) {
this.plot(this.x, this.y);
}
}
}
render(image: ImageData): void {
for (var pixelX = 0; pixelX < image.width; pixelX++) {
for (var pixelY = 0; pixelY < image.height; pixelY++) {
const hIndex = histIndex(pixelX, pixelY, this.size);
if (!this.values[hIndex]) {
continue;
}
// Set the pixel black
const iIndex = imageIndex(pixelX, pixelY, this.size);
image.data[iIndex + 0] = 0;
image.data[iIndex + 1] = 0;
image.data[iIndex + 2] = 0;
image.data[iIndex + 3] = 0xff;
}
}
}
}
export const paramsGasket: RenderParams = {
quality: 1,
renderer: (size) => new RendererGasket(size),
};