mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Implement everything in terms of an incremental render
This commit is contained in:
parent
7f44243cd0
commit
04d5099338
@ -1,34 +1,5 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Renderer, renderFn } from "./0-utility";
|
import { Renderer } 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} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CanvasParams = {
|
export type CanvasParams = {
|
||||||
defaultUrl: string;
|
defaultUrl: string;
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
// 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],
|
|
||||||
];
|
|
||||||
}
|
|
@ -11,6 +11,8 @@ type Transform = (x: number, y: number) => [number, number];
|
|||||||
|
|
||||||
export class RendererGasket extends Renderer {
|
export class RendererGasket extends Renderer {
|
||||||
private values = new Uint8Array(this.size * this.size);
|
private values = new Uint8Array(this.size * this.size);
|
||||||
|
private x = randomBiUnit();
|
||||||
|
private y = randomBiUnit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate values in the flame coordinate system to pixel coordinates
|
* Translate values in the flame coordinate system to pixel coordinates
|
||||||
@ -53,16 +55,17 @@ export class RendererGasket extends Renderer {
|
|||||||
(x, y) => [x / 2, (y + 1) / 2],
|
(x, y) => [x / 2, (y + 1) / 2],
|
||||||
];
|
];
|
||||||
|
|
||||||
let x = randomBiUnit();
|
// NOTE: `x` and `y` are set as fields on this class
|
||||||
let y = randomBiUnit();
|
// (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;
|
const iterations = quality * this.size * this.size;
|
||||||
for (var i = 0; i < iterations; i++) {
|
for (var i = 0; i < iterations; i++) {
|
||||||
const transformIndex = randomInteger(0, transforms.length);
|
const transformIndex = randomInteger(0, transforms.length);
|
||||||
[x, y] = transforms[transformIndex](x, y);
|
[this.x, this.y] = transforms[transformIndex](this.x, this.y);
|
||||||
|
|
||||||
if (i >= 20) {
|
if (i >= 20) {
|
||||||
this.plot(x, y);
|
this.plot(this.x, this.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,3 +88,7 @@ export class RendererGasket extends Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildGasket(size: number) {
|
||||||
|
return new RendererGasket(size);
|
||||||
|
}
|
||||||
|
@ -90,19 +90,40 @@ export class Transform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export class RendererFlame extends Renderer {
|
||||||
private values = new Uint8Array(this.size * this.size);
|
private values = new Uint8Array(this.size * this.size);
|
||||||
|
protected x = randomBiUnit();
|
||||||
|
protected y = randomBiUnit();
|
||||||
|
|
||||||
constructor(size: number, public readonly transforms: [number, Transform][]) {
|
constructor(size: number, public readonly transforms: [number, Transform][]) {
|
||||||
super(size);
|
super(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
plot(x: number, y: number) {
|
plot(x: number, y: number) {
|
||||||
// By default, Apophysis uses a camera that covers the range [-2, 2]
|
const [pixelX, pixelY] = camera(x, y, this.size);
|
||||||
// (specifically, the `scale` parameter becomes `pixels_per_unit` in flam3)
|
|
||||||
// Shift the coordinate system to [0, 4], then scale to pixel coordinates
|
|
||||||
const pixelX = Math.floor(((x + 2) * this.size) / 4);
|
|
||||||
const pixelY = Math.floor(((y + 2) * this.size) / 4);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pixelX < 0 ||
|
pixelX < 0 ||
|
||||||
@ -118,16 +139,13 @@ export class RendererFlame extends Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run(quality: number): void {
|
run(quality: number): void {
|
||||||
var x = randomBiUnit();
|
|
||||||
var y = randomBiUnit();
|
|
||||||
|
|
||||||
const iterations = quality * this.size * this.size;
|
const iterations = quality * this.size * this.size;
|
||||||
for (var i = 0; i < iterations; i++) {
|
for (var i = 0; i < iterations; i++) {
|
||||||
const [_, transform] = weightedChoice(this.transforms);
|
const [_, transform] = weightedChoice(this.transforms);
|
||||||
[x, y] = transform.apply(x, y);
|
[this.x, this.y] = transform.apply(this.x, this.y);
|
||||||
|
|
||||||
if (i > 20) {
|
if (i > 20) {
|
||||||
this.plot(x, y);
|
this.plot(this.x, this.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,67 +168,6 @@ export class RendererFlame extends Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Flame {
|
|
||||||
protected x: number = randomBiUnit();
|
|
||||||
protected y: number = randomBiUnit();
|
|
||||||
|
|
||||||
constructor(public readonly transforms: [number, Transform][]) {}
|
|
||||||
|
|
||||||
step(): void {
|
|
||||||
const [_index, transform] = weightedChoice(this.transforms);
|
|
||||||
[this.x, this.y] = transform.apply(this.x, this.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
current(): [number, number] {
|
|
||||||
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) {
|
|
||||||
const [pixelX, pixelY] = camera(x, y, image.width);
|
|
||||||
|
|
||||||
if (
|
|
||||||
pixelX < 0 ||
|
|
||||||
pixelX >= image.width ||
|
|
||||||
pixelY < 0 ||
|
|
||||||
pixelY >= image.height
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = pixelY * (image.width * 4) + pixelX * 4;
|
|
||||||
|
|
||||||
image.data[index + 0] = 0;
|
|
||||||
image.data[index + 1] = 0;
|
|
||||||
image.data[index + 2] = 0;
|
|
||||||
image.data[index + 3] = 0xff;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function render(flame: Flame, quality: number, 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();
|
|
||||||
plot(flameX, flameY, image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const transform1Weight = 0.56453495;
|
export const transform1Weight = 0.56453495;
|
||||||
export const transform1 = new Transform(
|
export const transform1 = new Transform(
|
||||||
{
|
{
|
||||||
@ -253,10 +210,12 @@ export const transform3 = new Transform(
|
|||||||
[[1, pdj(1.09358, 2.13048, 2.54127, 2.37267)]]
|
[[1, pdj(1.09358, 2.13048, 2.54127, 2.37267)]]
|
||||||
);
|
);
|
||||||
|
|
||||||
export function rendererBaseline(size: number) {
|
export const transformAll: [number, Transform][] = [
|
||||||
return new RendererFlame(size, [
|
[transform1Weight, transform1],
|
||||||
[transform1Weight, transform1],
|
[transform2Weight, transform2],
|
||||||
[transform2Weight, transform2],
|
[transform3Weight, transform3],
|
||||||
[transform3Weight, transform3],
|
];
|
||||||
]);
|
|
||||||
|
export function buildBaseline(size: number) {
|
||||||
|
return new RendererFlame(size, transformAll);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
|
import { randomBiUnit, weightedChoice } from "./0-utility";
|
||||||
import {
|
import {
|
||||||
Coefs,
|
Coefs,
|
||||||
Variation,
|
Variation,
|
||||||
Flame,
|
|
||||||
Transform,
|
Transform,
|
||||||
linear,
|
|
||||||
julia,
|
|
||||||
popcorn,
|
|
||||||
pdj,
|
|
||||||
render,
|
|
||||||
transform1Weight,
|
transform1Weight,
|
||||||
transform1,
|
transform1,
|
||||||
transform2Weight,
|
transform2Weight,
|
||||||
@ -15,6 +10,7 @@ import {
|
|||||||
transform3Weight,
|
transform3Weight,
|
||||||
transform3,
|
transform3,
|
||||||
identityCoefs,
|
identityCoefs,
|
||||||
|
RendererFlame,
|
||||||
} from "./2a-variations";
|
} from "./2a-variations";
|
||||||
|
|
||||||
export class TransformPost extends Transform {
|
export class TransformPost extends Transform {
|
||||||
@ -35,16 +31,6 @@ export class TransformPost extends Transform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function variationPost(coefs: Coefs, variation: Variation): Variation {
|
|
||||||
return (x, y, transformCoefs) => {
|
|
||||||
const [varX, varY] = variation(x, y, transformCoefs);
|
|
||||||
return [
|
|
||||||
varX * coefs.a + varY * coefs.b + coefs.c,
|
|
||||||
varX * coefs.d + varY * coefs.e + coefs.f,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const transform1Post = new TransformPost(
|
export const transform1Post = new TransformPost(
|
||||||
transform1.coefs,
|
transform1.coefs,
|
||||||
transform1.variations,
|
transform1.variations,
|
||||||
@ -70,12 +56,12 @@ export const transform3Post = new TransformPost(
|
|||||||
identityCoefs
|
identityCoefs
|
||||||
);
|
);
|
||||||
|
|
||||||
export function renderPost(image: ImageData) {
|
export const transformAllPost: [number, TransformPost][] = [
|
||||||
const flame = new Flame([
|
[transform1Weight, transform1Post],
|
||||||
[transform1Weight, transform1],
|
[transform2Weight, transform2Post],
|
||||||
[transform2Weight, transform2Post],
|
[transform3Weight, transform3Post],
|
||||||
[transform3Weight, transform3],
|
];
|
||||||
]);
|
|
||||||
|
|
||||||
render(flame, 1, image);
|
export function buildPost(size: number) {
|
||||||
|
return new RendererFlame(size, transformAllPost);
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,5 @@
|
|||||||
import {
|
import { julia, identityCoefs, RendererFlame } from "./2a-variations";
|
||||||
Flame,
|
import { TransformPost, transformAllPost } from "./2b-post";
|
||||||
Transform,
|
|
||||||
julia,
|
|
||||||
transform1Weight,
|
|
||||||
transform2Weight,
|
|
||||||
transform3Weight,
|
|
||||||
render,
|
|
||||||
identityCoefs,
|
|
||||||
} from "./2a-variations";
|
|
||||||
import {
|
|
||||||
TransformPost,
|
|
||||||
transform1Post,
|
|
||||||
transform2Post,
|
|
||||||
transform3Post,
|
|
||||||
} from "./2b-post";
|
|
||||||
|
|
||||||
export class FlameFinal extends Flame {
|
|
||||||
didLog: boolean = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
transforms: [number, Transform][],
|
|
||||||
public readonly final: Transform
|
|
||||||
) {
|
|
||||||
super(transforms);
|
|
||||||
}
|
|
||||||
|
|
||||||
override current(): [number, number] {
|
|
||||||
return this.final.apply(this.x, this.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const transformFinal = new TransformPost(
|
export const transformFinal = new TransformPost(
|
||||||
{
|
{
|
||||||
@ -43,15 +14,20 @@ export const transformFinal = new TransformPost(
|
|||||||
identityCoefs
|
identityCoefs
|
||||||
);
|
);
|
||||||
|
|
||||||
export const flameFinal = new FlameFinal(
|
export class RendererFinal extends RendererFlame {
|
||||||
[
|
constructor(
|
||||||
[transform1Weight, transform1Post],
|
size: number,
|
||||||
[transform2Weight, transform2Post],
|
transforms: [number, TransformPost][],
|
||||||
[transform3Weight, transform3Post],
|
public readonly final: TransformPost
|
||||||
],
|
) {
|
||||||
transformFinal
|
super(size, transforms);
|
||||||
);
|
}
|
||||||
|
|
||||||
export function renderFinal(image: ImageData) {
|
plot(x: number, y: number): void {
|
||||||
render(flameFinal, 1, image);
|
super.plot(...this.final.apply(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFinal(size: number) {
|
||||||
|
return new RendererFinal(size, transformAllPost, transformFinal);
|
||||||
}
|
}
|
||||||
|
@ -1,78 +1,72 @@
|
|||||||
import { histIndex, imageIndex } from "./0-utility";
|
import { histIndex, imageIndex } from "./0-utility";
|
||||||
import { camera } from "./2a-variations";
|
import {
|
||||||
import { flameFinal, FlameFinal } from "./2c-final";
|
camera,
|
||||||
|
transform1Weight,
|
||||||
|
transform2Weight,
|
||||||
|
transform3Weight,
|
||||||
|
} from "./2a-variations";
|
||||||
|
import {
|
||||||
|
TransformPost,
|
||||||
|
transform1Post,
|
||||||
|
transform2Post,
|
||||||
|
transform3Post,
|
||||||
|
} from "./2b-post";
|
||||||
|
import { RendererFinal, transformFinal } from "./2c-final";
|
||||||
|
|
||||||
export abstract class Accumulator {
|
export class RendererHistogram extends RendererFinal {
|
||||||
histogram: number[] = [];
|
protected histogram: number[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly width: number,
|
size: number,
|
||||||
protected readonly height: number
|
transforms: [number, TransformPost][],
|
||||||
|
final: TransformPost
|
||||||
) {
|
) {
|
||||||
for (var i = 0; i < width * height; i++) {
|
super(size, transforms, final);
|
||||||
|
|
||||||
|
for (var i = 0; i < this.size * this.size; i++) {
|
||||||
this.histogram.push(0);
|
this.histogram.push(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accumulate(x: number, y: number) {
|
plot(x: number, y: number): void {
|
||||||
const [pixelX, pixelY] = camera(x, y, this.width);
|
[x, y] = this.final.apply(x, y);
|
||||||
|
const [pixelX, pixelY] = camera(x, y, this.size);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pixelX < 0 ||
|
pixelX < 0 ||
|
||||||
pixelX >= this.width ||
|
pixelY >= this.size ||
|
||||||
pixelY < 0 ||
|
pixelY < 0 ||
|
||||||
pixelY >= this.height
|
pixelY >= this.size
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = histIndex(pixelX, pixelY, this.width);
|
const hIndex = histIndex(pixelX, pixelY, this.size);
|
||||||
this.histogram[index] += 1;
|
this.histogram[hIndex] += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract render(image: ImageData): void;
|
render(image: ImageData): void {
|
||||||
}
|
for (var x = 0; x < this.size; x++) {
|
||||||
|
for (var y = 0; y < this.size; y++) {
|
||||||
class AccumulateBinary extends Accumulator {
|
const hIndex = histIndex(x, y, this.size);
|
||||||
render(image: ImageData) {
|
const iIndex = imageIndex(x, y, this.size);
|
||||||
for (var x = 0; x < image.width; x++) {
|
image.data[iIndex + 0] = 0;
|
||||||
for (var y = 0; y < image.height; y++) {
|
image.data[iIndex + 1] = 0;
|
||||||
const index = histIndex(x, y, image.width);
|
image.data[iIndex + 2] = 0;
|
||||||
|
image.data[iIndex + 3] = (this.histogram[hIndex] > 0 ? 1 : 0) * 0xff;
|
||||||
// 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] = value ? 0xff : 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function render(
|
export function buildBinary(size: number) {
|
||||||
flame: FlameFinal,
|
return new RendererHistogram(
|
||||||
quality: number,
|
size,
|
||||||
accumulator: Accumulator,
|
[
|
||||||
image: ImageData
|
[transform1Weight, transform1Post],
|
||||||
) {
|
[transform2Weight, transform2Post],
|
||||||
const iterations = quality * image.width * image.height;
|
[transform3Weight, transform3Post],
|
||||||
|
],
|
||||||
for (var i = 0; i < iterations; i++) {
|
transformFinal
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,25 @@
|
|||||||
import { histIndex, imageIndex } from "./0-utility";
|
import { histIndex, imageIndex } from "./0-utility";
|
||||||
import { flameFinal } from "./2c-final";
|
import { transformAllPost } from "./2b-post";
|
||||||
import { Accumulator, render } from "./3a-binary";
|
import { transformFinal } from "./2c-final";
|
||||||
|
import { RendererHistogram } from "./3a-binary";
|
||||||
|
|
||||||
export class AccumulateLinear extends Accumulator {
|
class RendererLinear extends RendererHistogram {
|
||||||
render(image: ImageData): void {
|
render(image: ImageData): void {
|
||||||
const maxValue = Math.max(...this.histogram);
|
const maxHistogram = Math.max(...this.histogram);
|
||||||
|
|
||||||
for (var x = 0; x < image.width; x++) {
|
for (var x = 0; x < this.size; x++) {
|
||||||
for (var y = 0; y < image.height; y++) {
|
for (var y = 0; y < this.size; y++) {
|
||||||
const index = histIndex(x, y, image.width);
|
const hIndex = histIndex(x, y, this.size);
|
||||||
|
const iIndex = imageIndex(x, y, this.size);
|
||||||
// Color full black if this pixel is maxValue, white if not part
|
image.data[iIndex + 0] = 0;
|
||||||
// of the solution set
|
image.data[iIndex + 1] = 0;
|
||||||
const value = (1 - this.histogram[index] / maxValue) * 0xff;
|
image.data[iIndex + 2] = 0;
|
||||||
|
image.data[iIndex + 3] = (this.histogram[hIndex] / maxHistogram) * 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 - value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderLinear(image: ImageData) {
|
export function buildLinear(size: number) {
|
||||||
const accumulator = new AccumulateLinear(image.width, image.height);
|
return new RendererLinear(size, transformAllPost, transformFinal);
|
||||||
render(flameFinal, 10, accumulator, image);
|
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,28 @@
|
|||||||
import { histIndex, imageIndex } from "./0-utility";
|
import { histIndex, imageIndex } from "./0-utility";
|
||||||
import { flameFinal } from "./2c-final";
|
import { transformAllPost } from "./2b-post";
|
||||||
import { Accumulator, render } from "./3a-binary";
|
import { transformFinal } from "./2c-final";
|
||||||
|
import { RendererHistogram } from "./3a-binary";
|
||||||
|
|
||||||
export class AccumulateLogarithmic extends Accumulator {
|
export class RendererLogarithmic extends RendererHistogram {
|
||||||
render(image: ImageData): void {
|
render(image: ImageData): void {
|
||||||
// Re-scale vibrancy to be log scale...
|
// Because log(0) is -Infinity, all the math actually works out.
|
||||||
for (var i = 0; i < this.histogram.length; i++) {
|
const histogramLog = this.histogram.map(Math.log);
|
||||||
this.histogram[i] = Math.log(this.histogram[i]);
|
const histogramLogMax = Math.max(...histogramLog);
|
||||||
}
|
|
||||||
|
|
||||||
// ...but otherwise render the same way as linear
|
for (var x = 0; x < this.size; x++) {
|
||||||
const maxValue = Math.max(...this.histogram);
|
for (var y = 0; y < this.size; y++) {
|
||||||
|
const hIndex = histIndex(x, y, this.size);
|
||||||
for (var x = 0; x < image.width; x++) {
|
const iIndex = imageIndex(x, y, this.size);
|
||||||
for (var y = 0; y < image.height; y++) {
|
image.data[iIndex + 0] = 0;
|
||||||
const index = histIndex(x, y, image.width);
|
image.data[iIndex + 1] = 0;
|
||||||
|
image.data[iIndex + 2] = 0;
|
||||||
// Color full black if this pixel is maxValue, white if not part
|
image.data[iIndex + 3] =
|
||||||
// of the solution set
|
(histogramLog[hIndex] / histogramLogMax) * 0xff;
|
||||||
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 - value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderLogarithmic(image: ImageData) {
|
export function buildLogarithmic(size: number) {
|
||||||
const accumulator = new AccumulateLogarithmic(image.width, image.height);
|
return new RendererLogarithmic(size, transformAllPost, transformFinal);
|
||||||
render(flameFinal, 20, accumulator, image);
|
|
||||||
}
|
}
|
||||||
|
185
posts/2023/06/flam3/4-color.ts
Normal file
185
posts/2023/06/flam3/4-color.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { histIndex, imageIndex, weightedChoice } from "./0-utility";
|
||||||
|
import {
|
||||||
|
Coefs,
|
||||||
|
Variation,
|
||||||
|
camera,
|
||||||
|
transform1Weight,
|
||||||
|
transform2Weight,
|
||||||
|
transform3Weight,
|
||||||
|
} from "./2a-variations";
|
||||||
|
import {
|
||||||
|
TransformPost,
|
||||||
|
transform1Post,
|
||||||
|
transform2Post,
|
||||||
|
transform3Post,
|
||||||
|
} from "./2b-post";
|
||||||
|
import { RendererFinal, transformFinal } from "./2c-final";
|
||||||
|
|
||||||
|
export class TransformColor extends TransformPost {
|
||||||
|
constructor(
|
||||||
|
coefs: Coefs,
|
||||||
|
variations: [number, Variation][],
|
||||||
|
post: Coefs,
|
||||||
|
public readonly color: number
|
||||||
|
) {
|
||||||
|
super(coefs, variations, post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RendererColor extends RendererFinal {
|
||||||
|
protected color: number = Math.random();
|
||||||
|
protected red: number[] = [];
|
||||||
|
protected green: number[] = [];
|
||||||
|
protected blue: number[] = [];
|
||||||
|
protected alpha: number[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
size: number,
|
||||||
|
transforms: [number, TransformColor][],
|
||||||
|
final: TransformColor,
|
||||||
|
private readonly palette: number[]
|
||||||
|
) {
|
||||||
|
super(size, transforms, final);
|
||||||
|
for (var i = 0; i < this.size * this.size; i++) {
|
||||||
|
this.red.push(0);
|
||||||
|
this.green.push(0);
|
||||||
|
this.blue.push(0);
|
||||||
|
this.alpha.push(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 * (this.palette.length / 3)) * 3;
|
||||||
|
return [
|
||||||
|
paletteNumber[colorIndex + 0],
|
||||||
|
paletteNumber[colorIndex + 1],
|
||||||
|
paletteNumber[colorIndex + 2],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
plotColor(x: number, y: number, color: number): void {
|
||||||
|
const [finalX, finalY] = this.final.apply(x, y);
|
||||||
|
const [pixelX, pixelY] = camera(finalX, finalY, this.size);
|
||||||
|
if (pixelX < 0 || pixelX > this.size || pixelY < 0 || pixelY > this.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: The reference parameters use a final `symmetry` of 1,
|
||||||
|
// which effectively disables the final transform's contribution
|
||||||
|
// to color mixing (see the `color_speed` in flam3).
|
||||||
|
// While we'd normally want to apply the same color transformation
|
||||||
|
// like we do in the `run` method, it is skipped here so the output
|
||||||
|
// image matches the reference image
|
||||||
|
//
|
||||||
|
// const finalColor = (color + (final as TransformColor).color) / 2
|
||||||
|
const finalColor = color;
|
||||||
|
const [r, g, b] = this.colorFromIndex(finalColor);
|
||||||
|
|
||||||
|
const hIndex = histIndex(pixelX, pixelY, this.size);
|
||||||
|
this.red[hIndex] += r;
|
||||||
|
this.green[hIndex] += g;
|
||||||
|
this.blue[hIndex] += b;
|
||||||
|
this.alpha[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);
|
||||||
|
this.color = (this.color + (transform as TransformColor).color) / 2;
|
||||||
|
|
||||||
|
if (i > 20) {
|
||||||
|
this.plotColor(this.x, this.y, this.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(image: ImageData): void {
|
||||||
|
for (var x = 0; x < image.width; x++) {
|
||||||
|
for (var y = 0; y < image.height; y++) {
|
||||||
|
const hIndex = histIndex(x, y, image.width);
|
||||||
|
|
||||||
|
// NOTE: Calculating the scaling factor for accumulated color value to final
|
||||||
|
// pixel coloring is very involved (gamma, vibrancy, etc.). This scaling implementation
|
||||||
|
// is only intended to approximate the reference parameters.
|
||||||
|
const aScale =
|
||||||
|
Math.log10(this.alpha[hIndex]) / (this.alpha[hIndex] * 1.5);
|
||||||
|
|
||||||
|
const iIdx = imageIndex(x, y, this.size);
|
||||||
|
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 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copied from the reference parameters
|
||||||
|
export const paletteHex =
|
||||||
|
"7E3037762C45722B496E2A4E6A29506728536527546326565C265C5724595322574D2155482153462050451F4E441E4D431E4C3F1E473F1E453F1E433F1E3F3F1E3B3E1E393E1E37421D36431C38451C3A471B3B491B3C4A1A3C4B1A3D4D1A3E4F19405318435517445817465A16475D15495E154960154A65134E6812506B12526E1153711055720F55740F55770E577A0E59810C58840B58880A588B09588F08589107569307559A05539D0451A1034FA5024BA90147AA0046AC0045B00242B4043DBB0634BE082EC20A29C30B27C50C26C90F1DCC1116D32110D6280EDA300CDC380ADF4109E04508E24A08E45106E75704EA6402EC6B01EE7300EE7600EF7A00F07E00F18300F29000F29300F39600F39900F39C00F3A000F3A100F3A201F2A502F1A805F0A906EFAA08EEA909EEA80AEDA60CEBA50FE5A313E1A113DD9F13DB9E13D99D14D49C15D09815CC9518C79318BE8B1ABB891BB9871DB4811FB07D1FAB7621A671239C6227975C289256299053298E502A89482C853F2D803A2E7E3037762C45742B47722B496E2A4E6A29516728536326565C265C5724595322575022564E2255482153452050451F4E431E4C3F1E473E1D463D1D453F1E43411E413F1E3B3E1E37421D36421D38431D3B451C3A471B3A491B3C4B1A3D4D1A3E4F19405318435418445518455817465A16475D154960154A65134E66124F6812506B12526E1153711055740F55770E577A0E597E0D57810C58840B58880A588B09588F08589307559A05539C04529E0452A1034FA5024BA90147AC0045B00242B4043DB7053ABB0634BE0831C20A29C50C26C90F1DCC1116D01711D32110D72A0EDA300CDD390ADF4109E24A08E45106E75704E95F03EA6402EC6C01EE7300EF7A00F07E00F18300F28900F29000F39300F39600F39C00F3A000F3A100F3A201F2A502F2A503F1A805F0A807EFAA08EEA80AEDA60CEBA50FE9A411E5A313E1A113DD9F13D99D14D49C15D09815CC9518C79318C38F1ABE8B1AB9871DB4811FB07D1FAB7621A67123A16A249C6227975E289256298E502A89482C853F2D803A2E";
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/34356351
|
||||||
|
export 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 paletteBytes = hexToBytes(paletteHex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-scale pixel color values to the range [0, 1], done to match
|
||||||
|
* 'flam3_get_palette'
|
||||||
|
*/
|
||||||
|
export const paletteNumber = paletteBytes.map((b) => b / 0xff);
|
||||||
|
|
||||||
|
export function buildColor(size: number) {
|
||||||
|
return new RendererColor(
|
||||||
|
size,
|
||||||
|
[
|
||||||
|
[transform1Weight, transform1Color],
|
||||||
|
[transform2Weight, transform2Color],
|
||||||
|
[transform3Weight, transform3Color],
|
||||||
|
],
|
||||||
|
transformFinalColor,
|
||||||
|
paletteNumber
|
||||||
|
);
|
||||||
|
}
|
@ -1,86 +0,0 @@
|
|||||||
import { weightedChoice } from "./0-utility";
|
|
||||||
import {
|
|
||||||
transform1,
|
|
||||||
transform1Weight,
|
|
||||||
transform2Weight,
|
|
||||||
transform3,
|
|
||||||
transform3Weight,
|
|
||||||
} from "./2a-variations";
|
|
||||||
import { transform2Post } from "./2b-post";
|
|
||||||
import { FlameFinal, transformFinal } from "./2c-final";
|
|
||||||
import { AccumulateLogarithmic } from "./3c-logarithmic";
|
|
||||||
|
|
||||||
export class AccumulateSolo extends AccumulateLogarithmic {
|
|
||||||
constructor(
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
public readonly soloTransform: number
|
|
||||||
) {
|
|
||||||
super(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
accumulateWithIndex(x: number, y: number, index: number) {
|
|
||||||
if (index === this.soloTransform) {
|
|
||||||
super.accumulate(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FlameIndex extends FlameFinal {
|
|
||||||
protected index: number = -1;
|
|
||||||
|
|
||||||
step() {
|
|
||||||
const [index, transform] = weightedChoice(this.transforms);
|
|
||||||
this.index = index;
|
|
||||||
[this.x, this.y] = transform.apply(this.x, this.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentWithIndex(): [number, number, number] {
|
|
||||||
const [finalX, finalY] = this.final.apply(this.x, this.y);
|
|
||||||
return [finalX, finalY, this.index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function render(
|
|
||||||
flame: FlameIndex,
|
|
||||||
quality: number,
|
|
||||||
accumulator: AccumulateSolo,
|
|
||||||
image: ImageData
|
|
||||||
) {
|
|
||||||
const iterations = quality * image.width * image.height;
|
|
||||||
|
|
||||||
for (var i = 0; i < iterations; i++) {
|
|
||||||
flame.step();
|
|
||||||
|
|
||||||
if (i > 20) {
|
|
||||||
const [flameX, flameY, index] = flame.currentWithIndex();
|
|
||||||
accumulator.accumulateWithIndex(flameX, flameY, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accumulator.render(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flameIndex = new FlameIndex(
|
|
||||||
[
|
|
||||||
[transform1Weight, transform1],
|
|
||||||
[transform2Weight, transform2Post],
|
|
||||||
[transform3Weight, transform3],
|
|
||||||
],
|
|
||||||
transformFinal
|
|
||||||
);
|
|
||||||
|
|
||||||
export function renderTransform1(image: ImageData) {
|
|
||||||
const accumulateTransform1 = new AccumulateSolo(image.width, image.height, 0);
|
|
||||||
render(flameIndex, 10, accumulateTransform1, image);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderTransform2(image: ImageData) {
|
|
||||||
const accumulateTransform2 = new AccumulateSolo(image.width, image.height, 1);
|
|
||||||
render(flameIndex, 10, accumulateTransform2, image);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderTransform3(image: ImageData) {
|
|
||||||
const accumulateTransform3 = new AccumulateSolo(image.width, image.height, 2);
|
|
||||||
render(flameIndex, 10, accumulateTransform3, image);
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
48
posts/2023/06/flam3/5a-gasket.ts
Normal file
48
posts/2023/06/flam3/5a-gasket.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { RendererFlame, Transform, linear } from "./2a-variations";
|
||||||
|
import { RendererLogarithmic } from "./3c-logarithmic";
|
||||||
|
|
||||||
|
export const transformGasket1 = new Transform(
|
||||||
|
{
|
||||||
|
a: 0.5,
|
||||||
|
b: 0,
|
||||||
|
c: 0,
|
||||||
|
d: 0,
|
||||||
|
e: 0.5,
|
||||||
|
f: 0,
|
||||||
|
},
|
||||||
|
[[1, linear]]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const transformGasket2 = new Transform(
|
||||||
|
{
|
||||||
|
a: 0.5,
|
||||||
|
b: 0,
|
||||||
|
c: 0.5,
|
||||||
|
d: 0,
|
||||||
|
e: 0.5,
|
||||||
|
f: 0,
|
||||||
|
},
|
||||||
|
[[1, linear]]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const transformGasket3 = new Transform(
|
||||||
|
{
|
||||||
|
a: 0.5,
|
||||||
|
b: 0,
|
||||||
|
c: 0,
|
||||||
|
d: 0,
|
||||||
|
e: 0.5,
|
||||||
|
f: 0.5,
|
||||||
|
},
|
||||||
|
[[1, linear]]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const transformGasket: [number, Transform][] = [
|
||||||
|
[1 / 3, transformGasket1],
|
||||||
|
[1 / 3, transformGasket2],
|
||||||
|
[1 / 3, transformGasket3],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function buildGasketFlame(size: number) {
|
||||||
|
return new RendererFlame(size, transformGasket);
|
||||||
|
}
|
40
posts/2023/06/flam3/5b-solo.ts
Normal file
40
posts/2023/06/flam3/5b-solo.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { weightedChoice } from "./0-utility";
|
||||||
|
import { TransformPost, transformAllPost } from "./2b-post";
|
||||||
|
import { transformFinal } from "./2c-final";
|
||||||
|
import { RendererLogarithmic } from "./3c-logarithmic";
|
||||||
|
|
||||||
|
export class RendererSolo extends RendererLogarithmic {
|
||||||
|
constructor(
|
||||||
|
size: number,
|
||||||
|
transforms: [number, TransformPost][],
|
||||||
|
final: TransformPost,
|
||||||
|
private readonly transformSolo: number
|
||||||
|
) {
|
||||||
|
super(size, transforms, final);
|
||||||
|
}
|
||||||
|
|
||||||
|
run(quality: number): void {
|
||||||
|
const iterations = quality * this.size * this.size;
|
||||||
|
for (var i = 0; i < iterations; i++) {
|
||||||
|
const [transformIndex, transform] = weightedChoice(this.transforms);
|
||||||
|
[this.x, this.y] = transform.apply(this.x, this.y);
|
||||||
|
|
||||||
|
// NOTE: Only plot if the current point is from the solo transform
|
||||||
|
if (i > 20 && transformIndex == this.transformSolo) {
|
||||||
|
this.plot(this.x, this.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSolo1(size: number) {
|
||||||
|
return new RendererSolo(size, transformAllPost, transformFinal, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSolo2(size: number) {
|
||||||
|
return new RendererSolo(size, transformAllPost, transformFinal, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSolo3(size: number) {
|
||||||
|
return new RendererSolo(size, transformAllPost, transformFinal, 2);
|
||||||
|
}
|
@ -1,19 +1,16 @@
|
|||||||
|
import { build } from "vite";
|
||||||
import Blog from "../../../LayoutBlog";
|
import Blog from "../../../LayoutBlog";
|
||||||
|
|
||||||
import { Canvas, CanvasRenderer } from "./0-canvas";
|
import { CanvasRenderer } from "./0-canvas";
|
||||||
import { RendererGasket } from "./1-gasket";
|
import { buildBaseline } from "./2a-variations";
|
||||||
import { rendererBaseline } from "./2a-variations";
|
import { buildPost } from "./2b-post";
|
||||||
import { renderPost } from "./2b-post";
|
import { buildFinal } from "./2c-final";
|
||||||
import { renderFinal } from "./2c-final";
|
import { buildBinary } from "./3a-binary";
|
||||||
import { renderBinary } from "./3a-binary";
|
import { buildColor } from "./4-color";
|
||||||
import { renderLinear } from "./3b-linear";
|
import { buildLinear } from "./3b-linear";
|
||||||
import { renderLogarithmic } from "./3c-logarithmic";
|
import { buildLogarithmic } from "./3c-logarithmic";
|
||||||
import {
|
import { buildSolo1, buildSolo2, buildSolo3 } from "./5b-solo";
|
||||||
renderTransform1,
|
import { buildGasketFlame } from "./5a-gasket";
|
||||||
renderTransform2,
|
|
||||||
renderTransform3,
|
|
||||||
} from "./4a-solo";
|
|
||||||
import { renderColor } from "./4b-color";
|
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const Layout = Blog({
|
const Layout = Blog({
|
||||||
@ -32,27 +29,96 @@ export default function () {
|
|||||||
qualityStep: 0.1,
|
qualityStep: 0.1,
|
||||||
}}
|
}}
|
||||||
/> */}
|
/> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildBaseline(size),
|
||||||
|
qualityMax: 1,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildPost(size),
|
||||||
|
qualityMax: 1,
|
||||||
|
qualityStep: 0.1
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildFinal(size),
|
||||||
|
qualityMax: 1,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildBinary(size),
|
||||||
|
qualityMax: 1,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildLinear(size),
|
||||||
|
qualityMax: 5,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildLogarithmic(size),
|
||||||
|
qualityMax: 5,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildColor(size),
|
||||||
|
qualityMax: 50,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildSolo1(size),
|
||||||
|
qualityMax: 5,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildSolo2(size),
|
||||||
|
qualityMax: 5,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
|
{/* <CanvasRenderer
|
||||||
|
params={{
|
||||||
|
defaultUrl: "",
|
||||||
|
size: 400,
|
||||||
|
renderer: (size) => buildSolo3(size),
|
||||||
|
qualityMax: 5,
|
||||||
|
qualityStep: 0.1,
|
||||||
|
}} /> */}
|
||||||
<CanvasRenderer
|
<CanvasRenderer
|
||||||
params={{
|
params={{
|
||||||
defaultUrl: "",
|
defaultUrl: "",
|
||||||
size: 400,
|
size: 400,
|
||||||
renderer: (size) => rendererBaseline(size),
|
renderer: (size) => buildGasketFlame(size),
|
||||||
qualityMax: 1,
|
qualityMax: 0.3,
|
||||||
qualityStep: 0.1,
|
qualityStep: 0.1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <div>
|
|
||||||
<Canvas f={gasket} />
|
|
||||||
<Canvas f={renderBaseline} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Canvas f={renderPost} />
|
|
||||||
<Canvas f={renderFinal} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Canvas f={renderLogarithmic} />
|
|
||||||
<Canvas f={renderColor} />
|
|
||||||
</div> */}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user