mirror of
https://github.com/bspeice/speice.io
synced 2025-06-30 21:36:38 -04:00
Mass formatting, fix mobile display, fix issues with image wrap-around
This commit is contained in:
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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)
|
||||
];
|
||||
}
|
@ -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} />);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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!
|
||||
|
@ -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;
|
||||
|
@ -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;
|
Reference in New Issue
Block a user