Mass formatting, fix mobile display, fix issues with image wrap-around

This commit is contained in:
2024-12-15 21:19:09 -05:00
parent 1ce6137c17
commit 456c3a66e5
41 changed files with 1026 additions and 817 deletions

View File

@ -1,21 +1,21 @@
import {SquareCanvas, PainterContext} from "../src/Canvas";
import {useContext, useEffect} from "react";
import { PainterContext, SquareCanvas } from "../src/Canvas";
import { useContext, useEffect } from "react";
export function Render({f}) {
const {width, height, setPainter} = useContext(PainterContext);
useEffect(() => {
if (width && height) {
const painter = f({width, height});
setPainter(painter);
}
}, [width, height]);
return <></>;
export function Render({ f }) {
const { width, height, setPainter } = useContext(PainterContext);
useEffect(() => {
if (width && height) {
const painter = f({ width, height });
setPainter(painter);
}
}, [width, height]);
return <></>;
}
export default function Gasket({f}) {
return (
<SquareCanvas name={"gasket"}>
<Render f={f}/>
</SquareCanvas>
)
export default function Gasket({ f }) {
return (
<SquareCanvas name={"gasket"}>
<Render f={f} />
</SquareCanvas>
);
}

View File

@ -1,49 +1,49 @@
import {useEffect, useState, useContext, useRef} from "react";
import {PainterContext} from "../src/Canvas";
import {chaosGameWeighted} from "./chaosGameWeighted";
import TeX from '@matejmazur/react-katex';
import { useContext, useEffect, useState } from "react";
import { PainterContext } from "../src/Canvas";
import { chaosGameWeighted } from "./chaosGameWeighted";
import TeX from "@matejmazur/react-katex";
import styles from "../src/css/styles.module.css"
import styles from "../src/css/styles.module.css";
type Transform = (x: number, y: number) => [number, number];
export default function GasketWeighted() {
const [f0Weight, setF0Weight] = useState<number>(1);
const [f1Weight, setF1Weight] = useState<number>(1);
const [f2Weight, setF2Weight] = useState<number>(1);
const [f0Weight, setF0Weight] = useState<number>(1);
const [f1Weight, setF1Weight] = useState<number>(1);
const [f2Weight, setF2Weight] = useState<number>(1);
const f0: Transform = (x, y) => [x / 2, y / 2];
const f1: Transform = (x, y) => [(x + 1) / 2, y / 2];
const f2: Transform = (x, y) => [x / 2, (y + 1) / 2];
const f0: Transform = (x, y) => [x / 2, y / 2];
const f1: Transform = (x, y) => [(x + 1) / 2, y / 2];
const f2: Transform = (x, y) => [x / 2, (y + 1) / 2];
const {width, height, setPainter} = useContext(PainterContext);
const { width, height, setPainter } = useContext(PainterContext);
useEffect(() => {
const transforms: [number, Transform][] = [
[f0Weight, f0],
[f1Weight, f1],
[f2Weight, f2]
];
setPainter(chaosGameWeighted({width, height, transforms}));
}, [f0Weight, f1Weight, f2Weight]);
useEffect(() => {
const transforms: [number, Transform][] = [
[f0Weight, f0],
[f1Weight, f1],
[f2Weight, f2]
];
setPainter(chaosGameWeighted({ width, height, transforms }));
}, [f0Weight, f1Weight, f2Weight]);
const weightInput = (title, weight, setWeight) => (
<>
<div className={styles.inputElement}>
<p><TeX>{title}</TeX>: {weight}</p>
<input type={'range'} min={0} max={1} step={.01} value={weight}
onInput={e => setWeight(Number(e.currentTarget.value))}/>
</div>
</>
)
const weightInput = (title, weight, setWeight) => (
<>
<div className={styles.inputElement}>
<p><TeX>{title}</TeX>: {weight}</p>
<input type={"range"} min={0} max={1} step={.01} value={weight}
onInput={e => setWeight(Number(e.currentTarget.value))} />
</div>
</>
);
return (
<>
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr'}}>
{weightInput("F_0", f0Weight, setF0Weight)}
{weightInput("F_1", f1Weight, setF1Weight)}
{weightInput("F_2", f2Weight, setF2Weight)}
</div>
</>
)
return (
<>
<div className={styles.inputGroup} style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
{weightInput("F_0", f0Weight, setF0Weight)}
{weightInput("F_1", f1Weight, setF1Weight)}
{weightInput("F_2", f2Weight, setF2Weight)}
</div>
</>
);
}

View File

@ -1,10 +1,10 @@
export function camera(
size: number,
x: number,
y: number
x: number,
y: number,
size: number
): [number, number] {
return [
Math.floor(x * size),
Math.floor(y * size)
];
return [
Math.floor(x * size),
Math.floor(y * size)
];
}

View File

@ -6,28 +6,29 @@ const xforms = [
(x, y) => [x / 2, y / 2],
(x, y) => [(x + 1) / 2, y / 2],
(x, y) => [x / 2, (y + 1) / 2]
]
];
function* chaosGame({width, height}) {
const step = 1000;
let img = new ImageData(width, height);
function* chaosGame({ width, height }) {
let img =
new ImageData(width, height);
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
for (let c = 0; c < iterations; c++) {
const i = randomInteger(0, xforms.length);
[x, y] = xforms[i](x, y);
for (let i = 0; i < iterations; i++) {
const index =
randomInteger(0, xforms.length);
[x, y] = xforms[index](x, y);
if (c > 20)
if (i > 20)
plot(x, y, img);
if (c % step === 0)
if (i % 1000 === 0)
yield img;
}
yield img;
}
render(<Gasket f={chaosGame}/>)
render(<Gasket f={chaosGame} />);

View File

@ -1,37 +1,41 @@
// hidden-start
import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { plot } from "./plot"
import {Transform} from "../src/transform";
import { plot } from "./plot";
import { Transform } from "../src/transform";
const quality = 0.5;
const step = 1000;
// hidden-end
export type Props = {
width: number,
height: number,
transforms: [number, Transform][]
width: number,
height: number,
transforms: [number, Transform][]
}
export function* chaosGameWeighted(
{width, height, transforms}: Props
{ width, height, transforms }: Props
) {
let img = new ImageData(width, height);
let img =
new ImageData(width, height);
let [x, y] = [
randomBiUnit(),
randomBiUnit()
randomBiUnit(),
randomBiUnit()
];
const iterations = width * height * quality;
for (let c = 0; c < iterations; c++) {
const pixels = width * height;
const iterations = quality * pixels;
for (let i = 0; i < iterations; i++) {
// highlight-start
const [_, xform] = randomChoice(transforms);
const [_, xform] =
randomChoice(transforms);
// highlight-end
[x, y] = xform(x, y);
if (c > 20)
if (i > 20)
plot(x, y, img);
if (c % step === 0)
if (i % step === 0)
yield img;
}

View File

@ -83,7 +83,7 @@ called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transfor
The general form of an affine transformation is:
$$
F_i(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i)
F_i(a_i x + b_i y + c_i, d_i x + e_i y + f_i)
$$
import transformSource from "!!raw-loader!../src/transform"
@ -102,8 +102,7 @@ c &= 0.5 \\
d &= 0 \\
e &= 1 \\
f &= 1.5 \\
F_{shift}(x,y) &= (1 \cdot x + 0 \cdot y + 0.5, 0 \cdot x + 1 \cdot y + 0.5) \\
F_{shift}(x, y) &= (x + 0.5, y + 1.5)
F_{shift}(x, y) &= (1 \cdot x + 0.5, 1 \cdot y + 1.5)
\end{align*}
$$
@ -155,7 +154,7 @@ explaining the mathematics of iterated function systems:
> of finite compositions $F_{i_1...i_p}$ of members of $F$.
:::note
I've tweaked the wording slightly to match the conventions of the Fractal Flame paper
I've tweaked the conventions of that paper slightly to match the Fractal Flame paper
:::
Before your eyes glaze over, let's unpack this:
@ -174,15 +173,15 @@ Thus, by applying the functions to fixed points of our system, we will find the
...then there are some extra details I've glossed over so far.
First, the Hutchinson paper requires that the functions $F_i$ be _contractive_ for the solution set to exist.
That is, applying the function to a point must bring it closer to other points. However, as the Fractal Flame
That is, applying the function to a point must bring it closer to other points. However, as the fractal flame
algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will
degenerate and produce a bad image.
Second, we're focused $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper
Second, we're focused on $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper
allows for arbitrary dimensions; you could also have 3-dimensional fractal flames.
Finally, there's a close relationship between fractal flames and [attractors](https://en.wikipedia.org/wiki/Attractor).
Specifically, the fixed points of $S$ act as attractors for the chaos game (explained in detail below).
Specifically, the fixed points of $S$ act as attractors for the chaos game (explained in more detail below).
</details>
This is still a bit vague, so let's work through an example.
@ -272,8 +271,8 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
<small>
The image here is slightly different than in the paper.
I think the paper has an error, so I'm choosing to plot the image
the same way as the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
I think the paper has an error, so I'm plotting the image
like the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
</small>
### Weights
@ -292,7 +291,10 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
For Sierpinski's Gasket, we start with equal weighting,
but changing the transform weights affects how often
the chaos game "visits" parts of the image:
the chaos game "visits" parts of the image. If the chaos
were to run forever, we'd get the same image;
but because the iteration count is limited,
some parts may be missing below.
:::tip
Double-click the image if you want to save a copy!

View File

@ -1,10 +1,11 @@
// hidden-start
import {camera} from "./cameraGasket"
import { camera } from "./cameraGasket";
// hidden-end
function imageIndex(
width: number,
x: number,
y: number
y: number,
width: number
) {
return y * (width * 4) + x * 4;
}
@ -14,24 +15,26 @@ export function plot(
y: number,
img: ImageData
) {
let [pixelX, pixelY] = camera(img.width, x, y);
let [pixelX, pixelY] =
camera(x, y, img.width);
// Skip coordinates outside the display
if (
pixelX < 0 ||
pixelX >= img.width ||
pixelY < 0 ||
pixelY >= img.height
)
return;
const i = imageIndex(
img.width,
pixelX,
pixelY
pixelY,
img.width
);
// Skip pixels outside the display range
if (
i < 0 ||
i > img.data.length
) {
return;
}
// Set the pixel to black by writing 0
// to the first three elements at the index
// Set the pixel to black by setting
// the first three elements to 0
// (red, green, and blue, respectively),
// and 255 to the last element (alpha)
img.data[i] = 0;

View File

@ -1,12 +1,12 @@
import Gasket from "./Gasket";
import { plot } from './plot';
import { randomBiUnit } from '../src/randomBiUnit';
import { randomInteger } from '../src/randomInteger';
import { plot } from "./plot";
import { randomBiUnit } from "../src/randomBiUnit";
import { randomInteger } from "../src/randomInteger";
const Scope = {
Gasket,
plot,
randomBiUnit,
randomInteger,
}
Gasket,
plot,
randomBiUnit,
randomInteger
};
export default Scope;