mirror of
				https://github.com/bspeice/speice.io
				synced 2025-11-03 18:10:32 -05: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