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

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

View File

@ -0,0 +1,4 @@
{
"tabWidth": 2,
"semi": true
}

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);
export function Render({ f }) {
const { width, height, setPainter } = useContext(PainterContext);
useEffect(() => {
if (width && height) {
const painter = f({width, height});
const painter = f({ width, height });
setPainter(painter);
}
}, [width, height]);
return <></>;
}
export default function Gasket({f}) {
export default function Gasket({ f }) {
return (
<SquareCanvas name={"gasket"}>
<Render f={f}/>
<Render f={f} />
</SquareCanvas>
)
);
}

View File

@ -1,9 +1,9 @@
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];
@ -16,7 +16,7 @@ export default function GasketWeighted() {
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][] = [
@ -24,26 +24,26 @@ export default function GasketWeighted() {
[f1Weight, f1],
[f2Weight, f2]
];
setPainter(chaosGameWeighted({width, height, transforms}));
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))}/>
<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'}}>
<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,7 +1,7 @@
export function camera(
size: number,
x: number,
y: number
y: number,
size: number
): [number, number] {
return [
Math.floor(x * 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,8 +1,8 @@
// 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;
@ -12,26 +12,30 @@ export type Props = {
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()
];
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,
}
randomInteger
};
export default Scope;

View File

@ -1,5 +1,5 @@
import TeX from "@matejmazur/react-katex";
import {Coefs} from "../src/coefs";
import { Coefs } from "../src/transform";
import styles from "../src/css/styles.module.css";
@ -10,42 +10,43 @@ export interface Props {
setCoefs: (coefs: Coefs) => void;
resetCoefs: () => void;
}
export const CoefEditor = ({title, isPost, coefs, setCoefs, resetCoefs}: Props) => {
const resetButton = <button className={styles.inputReset} onClick={resetCoefs}>Reset</button>
export const CoefEditor = ({ title, isPost, coefs, setCoefs, resetCoefs }: Props) => {
const resetButton = <button className={styles.inputReset} onClick={resetCoefs}>Reset</button>;
return (
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr'}}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p>
<div className={styles.inputGroup} style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
<p className={styles.inputTitle} style={{ gridColumn: "1/-1" }}>{title} {resetButton}</p>
<div className={styles.inputElement}>
<p>{isPost ? <TeX>\alpha</TeX> : 'a'}: {coefs.a}</p>
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.a}
onInput={e => setCoefs({...coefs, a: Number(e.currentTarget.value)})}/>
<p>{isPost ? <TeX>\alpha</TeX> : "a"}: {coefs.a}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={coefs.a}
onInput={e => setCoefs({ ...coefs, a: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<p>{isPost ? <TeX>\beta</TeX> : 'b'}: {coefs.b}</p>
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.b}
onInput={e => setCoefs({...coefs, b: Number(e.currentTarget.value)})}/>
<p>{isPost ? <TeX>\beta</TeX> : "b"}: {coefs.b}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={coefs.b}
onInput={e => setCoefs({ ...coefs, b: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<p>{isPost ? <TeX>\gamma</TeX> : 'c'}: {coefs.c}</p>
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.c}
onInput={e => setCoefs({...coefs, c: Number(e.currentTarget.value)})}/>
<p>{isPost ? <TeX>\gamma</TeX> : "c"}: {coefs.c}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={coefs.c}
onInput={e => setCoefs({ ...coefs, c: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<p>{isPost ? <TeX>\delta</TeX> : 'd'}: {coefs.d}</p>
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.d}
onInput={e => setCoefs({...coefs, d: Number(e.currentTarget.value)})}/>
<p>{isPost ? <TeX>\delta</TeX> : "d"}: {coefs.d}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={coefs.d}
onInput={e => setCoefs({ ...coefs, d: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<p>{isPost ? <TeX>\epsilon</TeX> : 'e'}: {coefs.e}</p>
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.e}
onInput={e => setCoefs({...coefs, e: Number(e.currentTarget.value)})}/>
<p>{isPost ? <TeX>\epsilon</TeX> : "e"}: {coefs.e}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={coefs.e}
onInput={e => setCoefs({ ...coefs, e: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<p>{isPost ? <TeX>\zeta</TeX> : 'f'}: {coefs.f}</p>
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.f}
onInput={e => setCoefs({...coefs, f: Number(e.currentTarget.value)})}/>
<p>{isPost ? <TeX>\zeta</TeX> : "f"}: {coefs.f}</p>
<input type={"range"} min={-2} max={2} step={0.01} value={coefs.f}
onInput={e => setCoefs({ ...coefs, f: Number(e.currentTarget.value) })} />
</div>
</div>
)
}
);
};

View File

@ -1,22 +1,22 @@
import {useContext, useEffect, useState} from "react";
import {Transform} from "../src/transform";
import * as params from "../src/params"
import {PainterContext} from "../src/Canvas"
import {chaosGameFinal} from "./chaosGameFinal"
import {VariationEditor, VariationProps} from "./VariationEditor"
import {applyTransform} from "../src/applyTransform";
import {buildBlend} from "./buildBlend";
import { useContext, useEffect, useState } from "react";
import { Transform } from "../src/transform";
import * as params from "../src/params";
import { PainterContext } from "../src/Canvas";
import { chaosGameFinal } from "./chaosGameFinal";
import { VariationEditor, VariationProps } from "./VariationEditor";
import { applyTransform } from "../src/applyTransform";
import { buildBlend } from "./buildBlend";
export default function FlameBlend() {
const {width, height, setPainter} = useContext(PainterContext);
const { width, height, setPainter } = useContext(PainterContext);
const xform1VariationsDefault: VariationProps = {
linear: 0,
julia: 1,
popcorn: 0,
pdj: 0,
}
const [xform1Variations, setXform1Variations] = useState(xform1VariationsDefault)
pdj: 0
};
const [xform1Variations, setXform1Variations] = useState(xform1VariationsDefault);
const resetXform1Variations = () => setXform1Variations(xform1VariationsDefault);
const xform2VariationsDefault: VariationProps = {
@ -24,8 +24,8 @@ export default function FlameBlend() {
julia: 0,
popcorn: 1,
pdj: 0
}
const [xform2Variations, setXform2Variations] = useState(xform2VariationsDefault)
};
const [xform2Variations, setXform2Variations] = useState(xform2VariationsDefault);
const resetXform2Variations = () => setXform2Variations(xform2VariationsDefault);
const xform3VariationsDefault: VariationProps = {
@ -33,8 +33,8 @@ export default function FlameBlend() {
julia: 0,
popcorn: 0,
pdj: 1
}
const [xform3Variations, setXform3Variations] = useState(xform3VariationsDefault)
};
const [xform3Variations, setXform3Variations] = useState(xform3VariationsDefault);
const resetXform3Variations = () => setXform3Variations(xform3VariationsDefault);
const identityXform: Transform = (x, y) => [x, y];
@ -44,25 +44,25 @@ export default function FlameBlend() {
[params.xform1Weight, applyTransform(params.xform1Coefs, buildBlend(params.xform1Coefs, xform1Variations))],
[params.xform2Weight, applyTransform(params.xform2Coefs, buildBlend(params.xform2Coefs, xform2Variations))],
[params.xform3Weight, applyTransform(params.xform3Coefs, buildBlend(params.xform3Coefs, xform3Variations))]
]
];
const gameParams = {
width,
height,
transforms,
final: identityXform
}
};
setPainter(chaosGameFinal(gameParams));
}, [xform1Variations, xform2Variations, xform3Variations]);
return (
<>
<VariationEditor title={"Transform 1"} variations={xform1Variations} setVariations={setXform1Variations}
resetVariations={resetXform1Variations}/>
resetVariations={resetXform1Variations} />
<VariationEditor title={"Transform 2"} variations={xform2Variations} setVariations={setXform2Variations}
resetVariations={resetXform2Variations}/>
resetVariations={resetXform2Variations} />
<VariationEditor title={"Transform 3"} variations={xform3Variations} setVariations={setXform3Variations}
resetVariations={resetXform3Variations}/>
resetVariations={resetXform3Variations} />
</>
)
);
}

View File

@ -1,15 +1,15 @@
import {useContext, useEffect, useState} from "react";
import {Coefs} from "../src/coefs"
import { useContext, useEffect, useState } from "react";
import { Coefs } from "../src/transform";
import * as params from "../src/params";
import {PainterContext} from "../src/Canvas"
import {buildBlend} from "./buildBlend";
import {chaosGameFinal} from "./chaosGameFinal"
import {VariationEditor, VariationProps} from "./VariationEditor";
import {CoefEditor} from "./CoefEditor";
import {applyPost, applyTransform} from "../src/applyTransform";
import { PainterContext } from "../src/Canvas";
import { buildBlend } from "./buildBlend";
import { chaosGameFinal } from "./chaosGameFinal";
import { VariationEditor, VariationProps } from "./VariationEditor";
import { CoefEditor } from "./CoefEditor";
import { applyPost, applyTransform } from "../src/applyTransform";
export default function FlameFinal() {
const {width, height, setPainter} = useContext(PainterContext);
const { width, height, setPainter } = useContext(PainterContext);
const [xformFinalCoefs, setXformFinalCoefs] = useState<Coefs>(params.xformFinalCoefs);
const resetXformFinalCoefs = () => setXformFinalCoefs(params.xformFinalCoefs);
@ -19,7 +19,7 @@ export default function FlameFinal() {
julia: 1,
popcorn: 0,
pdj: 0
}
};
const [xformFinalVariations, setXformFinalVariations] = useState<VariationProps>(xformFinalVariationsDefault);
const resetXformFinalVariations = () => setXformFinalVariations(xformFinalVariationsDefault);
@ -30,17 +30,17 @@ export default function FlameFinal() {
const finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
const finalXform = applyPost(xformFinalCoefsPost, applyTransform(xformFinalCoefs, finalBlend));
setPainter(chaosGameFinal({width, height, transforms: params.xforms, final: finalXform}));
setPainter(chaosGameFinal({ width, height, transforms: params.xforms, final: finalXform }));
}, [xformFinalCoefs, xformFinalVariations, xformFinalCoefsPost]);
return (
<>
<CoefEditor title={"Final Transform"} isPost={false} coefs={xformFinalCoefs} setCoefs={setXformFinalCoefs}
resetCoefs={resetXformFinalCoefs}/>
resetCoefs={resetXformFinalCoefs} />
<VariationEditor title={"Final Transform Variations"} variations={xformFinalVariations}
setVariations={setXformFinalVariations} resetVariations={resetXformFinalVariations}/>
setVariations={setXformFinalVariations} resetVariations={resetXformFinalVariations} />
<CoefEditor title={"Final Transform Post"} isPost={true} coefs={xformFinalCoefsPost}
setCoefs={setXformFinalCoefsPost} resetCoefs={resetXformFinalCoefsPost}/>
setCoefs={setXformFinalCoefsPost} resetCoefs={resetXformFinalCoefsPost} />
</>
)
);
}

View File

@ -1,14 +1,14 @@
import {useContext, useEffect, useState} from "react";
import {Coefs} from "../src/coefs"
import {Transform} from "../src/transform";
import { useContext, useEffect, useState } from "react";
import { applyTransform } from "../src/applyTransform";
import { Coefs, Transform } from "../src/transform";
import * as params from "../src/params";
import {PainterContext} from "../src/Canvas"
import {chaosGameFinal, Props as ChaosGameFinalProps} from "./chaosGameFinal"
import {CoefEditor} from "./CoefEditor"
import {applyPost, applyTransform} from "@site/blog/2024-11-15-playing-with-fire/src/applyTransform";
import { PainterContext } from "../src/Canvas";
import { chaosGameFinal, Props as ChaosGameFinalProps } from "./chaosGameFinal";
import { CoefEditor } from "./CoefEditor";
import { transformPost } from "./post";
export default function FlamePost() {
const {width, height, setPainter} = useContext(PainterContext);
const { width, height, setPainter } = useContext(PainterContext);
const [xform1CoefsPost, setXform1CoefsPost] = useState<Coefs>(params.xform1CoefsPost);
const resetXform1CoefsPost = () => setXform1CoefsPost(params.xform1CoefsPost);
@ -25,22 +25,22 @@ export default function FlamePost() {
width,
height,
transforms: [
[params.xform1Weight, applyPost(xform1CoefsPost, applyTransform(params.xform1Coefs, params.xform1Variations))],
[params.xform2Weight, applyPost(xform2CoefsPost, applyTransform(params.xform2Coefs, params.xform2Variations))],
[params.xform3Weight, applyPost(xform3CoefsPost, applyTransform(params.xform3Coefs, params.xform3Variations))],
[params.xform1Weight, transformPost(applyTransform(params.xform1Coefs, params.xform1Variations), xform1CoefsPost)],
[params.xform2Weight, transformPost(applyTransform(params.xform2Coefs, params.xform2Variations), xform2CoefsPost)],
[params.xform3Weight, transformPost(applyTransform(params.xform3Coefs, params.xform3Variations), xform3CoefsPost)]
],
final: identityXform
}
};
useEffect(() => setPainter(chaosGameFinal(gameParams)), [xform1CoefsPost, xform2CoefsPost, xform3CoefsPost]);
return (
<>
<CoefEditor title={"Transform 1 Post"} isPost={true} coefs={xform1CoefsPost} setCoefs={setXform1CoefsPost}
resetCoefs={resetXform1CoefsPost}/>
resetCoefs={resetXform1CoefsPost} />
<CoefEditor title={"Transform 2 Post"} isPost={true} coefs={xform2CoefsPost} setCoefs={setXform2CoefsPost}
resetCoefs={resetXform2CoefsPost}/>
resetCoefs={resetXform2CoefsPost} />
<CoefEditor title={"Transform 3 Post"} isPost={true} coefs={xform3CoefsPost} setCoefs={setXform3CoefsPost}
resetCoefs={resetXform3CoefsPost}/>
resetCoefs={resetXform3CoefsPost} />
</>
)
);
}

View File

@ -1,4 +1,4 @@
import styles from "../src/css/styles.module.css"
import styles from "../src/css/styles.module.css";
export interface VariationProps {
linear: number;
@ -14,32 +14,32 @@ export interface Props {
resetVariations: () => void;
}
export const VariationEditor = ({title, variations, setVariations, resetVariations}: Props) => {
const resetButton = <button className={styles.inputReset} onClick={resetVariations}>Reset</button>
export const VariationEditor = ({ title, variations, setVariations, resetVariations }: Props) => {
const resetButton = <button className={styles.inputReset} onClick={resetVariations}>Reset</button>;
return (
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr'}}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p>
<div className={styles.inputGroup} style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<p className={styles.inputTitle} style={{ gridColumn: "1/-1" }}>{title} {resetButton}</p>
<div className={styles.inputElement}>
<span>Linear: {variations.linear}</span>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.linear}
onInput={e => setVariations({...variations, linear: Number(e.currentTarget.value)})}/>
<input type={"range"} min={0} max={1} step={0.01} style={{ width: "100%" }} value={variations.linear}
onInput={e => setVariations({ ...variations, linear: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<span>Julia: {variations.julia}</span>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.julia}
onInput={e => setVariations({...variations, julia: Number(e.currentTarget.value)})}/>
<input type={"range"} min={0} max={1} step={0.01} style={{ width: "100%" }} value={variations.julia}
onInput={e => setVariations({ ...variations, julia: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<span>Popcorn: {variations.popcorn}</span>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.popcorn}
onInput={e => setVariations({...variations, popcorn: Number(e.currentTarget.value)})}/>
<input type={"range"} min={0} max={1} step={0.01} style={{ width: "100%" }} value={variations.popcorn}
onInput={e => setVariations({ ...variations, popcorn: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<span>PDJ: {variations.pdj}</span>
<input type={'range'} min={0} max={1} step={0.01} style={{width: '100%'}} value={variations.pdj}
onInput={e => setVariations({...variations, pdj: Number(e.currentTarget.value)})}/>
<input type={"range"} min={0} max={1} step={0.01} style={{ width: "100%" }} value={variations.pdj}
onInput={e => setVariations({ ...variations, pdj: Number(e.currentTarget.value) })} />
</div>
</div>
)
}
);
};

View File

@ -1,17 +1,17 @@
import {Coefs} from "../src/coefs";
import {VariationProps} from "./VariationEditor";
import {linear} from "../src/linear";
import {julia} from "../src/julia";
import {popcorn} from "../src/popcorn";
import {pdj} from "../src/pdj";
import {pdjParams} from "../src/params";
import {VariationBlend} from "../src/blend";
import { Coefs } from "../src/transform";
import { VariationProps } from "./VariationEditor";
import { linear } from "../src/linear";
import { julia } from "../src/julia";
import { popcorn } from "../src/popcorn";
import { pdj } from "../src/pdj";
import { pdjParams } from "../src/params";
import { Blend } from "../src/blend";
export function buildBlend(coefs: Coefs, variations: VariationProps): VariationBlend {
export function buildBlend(coefs: Coefs, variations: VariationProps): Blend {
return [
[variations.linear, linear],
[variations.julia, julia],
[variations.popcorn, popcorn(coefs)],
[variations.pdj, pdj(pdjParams)]
]
];
}

View File

@ -1,23 +1,37 @@
// hidden-start
import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { plotBinary as plot } from "../src/plotBinary"
import {Transform} from "../src/transform";
import {Props as ChaosGameWeightedProps} from "../1-introduction/chaosGameWeighted";
import { plotBinary as plot } from "../src/plotBinary";
import { Transform } from "../src/transform";
import { Props as WeightedProps } from "../1-introduction/chaosGameWeighted";
const quality = 0.5;
const step = 1000;
// hidden-end
export type Props = ChaosGameWeightedProps & {
export type Props = WeightedProps & {
final: Transform,
}
export function* chaosGameFinal({width, height, transforms, final}: Props) {
let image = new ImageData(width, height);
let [x, y] = [randomBiUnit(), randomBiUnit()];
const iterations = width * height * quality;
export function* chaosGameFinal(
{
width,
height,
transforms,
final
}: Props
) {
let img =
new ImageData(width, height);
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
const pixels = width * height;
const iterations = quality * pixels;
for (let i = 0; i < iterations; i++) {
const [_, transform] = randomChoice(transforms);
const [_, transform] =
randomChoice(transforms);
[x, y] = transform(x, y);
// highlight-start
@ -26,12 +40,12 @@ export function* chaosGameFinal({width, height, transforms, final}: Props) {
if (i > 20)
// highlight-start
plot(finalX, finalY, image);
plot(finalX, finalY, img);
// highlight-end
if (i % step === 0)
yield image;
yield img;
}
yield image;
yield img;
}

View File

@ -34,7 +34,7 @@ This leads us to the first big innovation of the Fractal Flame algorithm: adding
after the affine transform. These functions are called "variations":
$$
F_i(x, y) = V_j(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i)
F_i(x, y) = V_j(a_i x + b_i y + c_i, d_i x + e_i y + f_i)
$$
import variationSource from '!!raw-loader!../src/variation'
@ -96,7 +96,7 @@ Some variations rely on knowing the transform's affine coefficients; they're cal
For the popcorn variation, we use $c$ and $f$:
$$
V_{17}(x,y) = (x + c \cdot \text{sin}(\text{tan }3y), y + f \cdot \text{sin}(\text{tan }3x))
V_{17}(x,y) = (x + c\ \text{sin}(\text{tan }3y), y + f\ \text{sin}(\text{tan }3x))
$$
import popcornSrc from '!!raw-loader!../src/popcorn'
@ -109,8 +109,8 @@ Some variations have extra parameters; they're called "parametric variations."
For the PDJ variation, there are four extra parameters we can choose:
$$
p_1 = \text{pdj.a} \hspace{0.2cm} p_2 = \text{pdj.b} \hspace{0.2cm} p_3 = \text{pdj.c} \hspace{0.2cm} p_4 = \text{pdj.d} \\
V_{24} = (\text{sin}(p_1 \cdot y) - \text{cos}(p_2 \cdot x), \text{sin}(p_3 \cdot x) - \text{cos}(p_4 \cdot y))
p_1 = \text{pdj.a} \hspace{0.1cm} p_2 = \text{pdj.b} \hspace{0.1cm} p_3 = \text{pdj.c} \hspace{0.1cm} p_4 = \text{pdj.d} \\
V_{24} = (\text{sin}(p_1 y) - \text{cos}(p_2 x), \text{sin}(p_3 x) - \text{cos}(p_4 y))
$$
import pdjSrc from '!!raw-loader!../src/pdj'
@ -124,7 +124,7 @@ Each variation receives the same $x$ and $y$ inputs, and we add together each va
We'll also give each variation a weight ($v_{ij}$) that changes how much it contributes to the result:
$$
F_i(x,y) = \sum_{j} v_{ij} V_j(a_i \cdot x + b_i \cdot y + c_i, \hspace{0.2cm} d_i \cdot x + e_i \cdot y + f_i)
F_i(x,y) = \sum_{j} v_{ij} V_j(x, y)
$$
The formula looks intimidating, but it's not hard to implement:

View File

@ -1,7 +1,11 @@
// hidden-start
import {Coefs} from "../src/coefs";
import {Transform} from "../src/transform";
import {applyCoefs} from "../src/coefs";
import { applyCoefs, Coefs, Transform } from "../src/transform";
// hidden-end
export const transformPost = (transform: Transform, coefs: Coefs): Transform =>
(x, y) => applyCoefs(...transform(x, y), coefs)
export const transformPost = (
transform: Transform,
coefs: Coefs
): Transform =>
(x, y) => {
[x, y] = transform(x, y);
return applyCoefs(x, y, coefs);
}

View File

@ -1,19 +1,19 @@
import React, {useContext, useEffect, useMemo, useRef, useState} from "react";
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import * as params from "../src/params";
import {PainterContext} from "../src/Canvas";
import {colorFromPalette} from "./colorFromPalette";
import {chaosGameColor, Props as ChaosGameColorProps, TransformColor} from "./chaosGameColor";
import { PainterContext } from "../src/Canvas";
import { colorFromPalette } from "./colorFromPalette";
import { chaosGameColor, Props as ChaosGameColorProps, TransformColor } from "./chaosGameColor";
import styles from "../src/css/styles.module.css";
import {histIndex} from "../src/camera";
import {useColorMode} from "@docusaurus/theme-common";
import { histIndex } from "../src/camera";
import { useColorMode } from "@docusaurus/theme-common";
type PaletteBarProps = {
height: number;
palette: number[];
children?: React.ReactNode;
}
export const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
export const PaletteBar: React.FC<PaletteBarProps> = ({ height, palette, children }) => {
const sizingRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useEffect(() => {
@ -51,17 +51,17 @@ export const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children
}
}, [canvasRef, paletteImage]);
const canvasStyle = {filter: useColorMode().colorMode === 'dark' ? 'invert(1)' : ''};
const canvasStyle = { filter: useColorMode().colorMode === "dark" ? "invert(1)" : "" };
return (
<>
<div ref={sizingRef} style={{width: '100%', height}}>
{width > 0 ? <canvas ref={canvasRef} width={width} height={height} style={canvasStyle}/> : null}
<div ref={sizingRef} style={{ width: "100%", height }}>
{width > 0 ? <canvas ref={canvasRef} width={width} height={height} style={canvasStyle} /> : null}
</div>
{children}
</>
)
}
);
};
type ColorEditorProps = {
title: string;
@ -71,60 +71,67 @@ type ColorEditorProps = {
resetTransformColor: () => void;
children?: React.ReactNode;
}
const ColorEditor: React.FC<ColorEditorProps> = ({title, palette, transformColor, setTransformColor, resetTransformColor, children}) => {
const resetButton = <button className={styles.inputReset} onClick={resetTransformColor}>Reset</button>
const ColorEditor: React.FC<ColorEditorProps> = (
{
title,
palette,
transformColor,
setTransformColor,
resetTransformColor,
children
}) => {
const resetButton = <button className={styles.inputReset} onClick={resetTransformColor}>Reset</button>;
const [r, g, b] = colorFromPalette(palette, transformColor.color);
const colorCss = `rgb(${Math.floor(r * 0xff)},${Math.floor(g * 0xff)},${Math.floor(b * 0xff)})`;
const {colorMode} = useColorMode();
const { colorMode } = useColorMode();
return (
<>
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '2fr 2fr 1fr'}}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p>
<div className={styles.inputGroup} style={{ display: "grid", gridTemplateColumns: "2fr 2fr 1fr" }}>
<p className={styles.inputTitle} style={{ gridColumn: "1/-1" }}>{title} {resetButton}</p>
<div className={styles.inputElement}>
<p>Color: {transformColor.color}</p>
<input type={'range'} min={0} max={1} step={.001} value={transformColor.color}
onInput={e => setTransformColor({...transformColor, color: Number(e.currentTarget.value)})}/>
<input type={"range"} min={0} max={1} step={.001} value={transformColor.color}
onInput={e => setTransformColor({ ...transformColor, color: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement}>
<p>Speed: {transformColor.colorSpeed}</p>
<input type={'range'} min={0} max={1} step={.001} value={transformColor.colorSpeed}
onInput={e => setTransformColor({...transformColor, colorSpeed: Number(e.currentTarget.value)})}/>
<input type={"range"} min={0} max={1} step={.001} value={transformColor.colorSpeed}
onInput={e => setTransformColor({ ...transformColor, colorSpeed: Number(e.currentTarget.value) })} />
</div>
<div className={styles.inputElement} style={{
width: '100%',
height: '100%',
width: "100%",
height: "100%",
backgroundColor: colorCss,
filter: colorMode === 'dark' ? 'invert(1)' : ''
}}/>
filter: colorMode === "dark" ? "invert(1)" : ""
}} />
</div>
{children}
</>
)
}
);
};
type Props = {
quality?: number;
children?: React.ReactElement;
}
export default function FlameColor({quality, children}: Props) {
const {width, height, setPainter} = useContext(PainterContext);
export default function FlameColor({ children }: Props) {
const { width, height, setPainter } = useContext(PainterContext);
const xform1ColorDefault: TransformColor = {color: params.xform1Color, colorSpeed: 0.5};
const xform1ColorDefault: TransformColor = { color: params.xform1Color, colorSpeed: 0.5 };
const [xform1Color, setXform1Color] = useState(xform1ColorDefault);
const resetXform1Color = () => setXform1Color(xform1ColorDefault);
const xform2ColorDefault: TransformColor = {color: params.xform2Color, colorSpeed: 0.5};
const xform2ColorDefault: TransformColor = { color: params.xform2Color, colorSpeed: 0.5 };
const [xform2Color, setXform2Color] = useState(xform2ColorDefault);
const resetXform2Color = () => setXform2Color(xform2ColorDefault);
const xform3ColorDefault: TransformColor = {color: params.xform3Color, colorSpeed: 0.5};
const xform3ColorDefault: TransformColor = { color: params.xform3Color, colorSpeed: 0.5 };
const [xform3Color, setXform3Color] = useState(xform3ColorDefault);
const resetXform3Color = () => setXform3Color(xform3ColorDefault);
const xformFinalColorDefault: TransformColor = {color: params.xformFinalColor, colorSpeed: 0};
const xformFinalColorDefault: TransformColor = { color: params.xformFinalColor, colorSpeed: 0 };
const [xformFinalColor, setXformFinalColor] = useState(xformFinalColorDefault);
const resetXformFinalColor = () => setXformFinalColor(xformFinalColorDefault);
@ -137,37 +144,37 @@ export default function FlameColor({quality, children}: Props) {
palette: params.palette,
colors: [xform1Color, xform2Color, xform3Color],
finalColor: xformFinalColor
}
};
setPainter(chaosGameColor(gameParams));
}, [xform1Color, xform2Color, xform3Color, xformFinalColor]);
return (
<>
<PaletteBar height={40} palette={params.palette}/>
<PaletteBar height={40} palette={params.palette} />
<ColorEditor
title={"Transform 1"}
palette={params.palette}
transformColor={xform1Color}
setTransformColor={setXform1Color}
resetTransformColor={resetXform1Color}/>
resetTransformColor={resetXform1Color} />
<ColorEditor
title={"Transform 2"}
palette={params.palette}
transformColor={xform2Color}
setTransformColor={setXform2Color}
resetTransformColor={resetXform2Color}/>
resetTransformColor={resetXform2Color} />
<ColorEditor
title={"Transform 3"}
palette={params.palette}
transformColor={xform3Color}
setTransformColor={setXform3Color}
resetTransformColor={resetXform3Color}/>
resetTransformColor={resetXform3Color} />
<ColorEditor
title={"Transform Final"}
palette={params.palette}
transformColor={xformFinalColor}
setTransformColor={setXformFinalColor}
resetTransformColor={resetXformFinalColor}/>
resetTransformColor={resetXformFinalColor} />
{children}
</>
);

View File

@ -1,14 +1,14 @@
import React, {useContext, useEffect} from "react";
import {xforms as transforms, xformFinal as final} from "../src/params";
import {PainterContext} from "../src/Canvas";
import {chaosGameHistogram} from "./chaosGameHistogram";
import React, { useContext, useEffect } from "react";
import { xformFinal as final, xforms as transforms } from "../src/params";
import { PainterContext } from "../src/Canvas";
import { chaosGameHistogram } from "./chaosGameHistogram";
type Props = {
paint: (width: number, height: number, histogram: number[]) => ImageData;
children?: React.ReactElement;
}
export default function FlameHistogram({paint, children}: Props) {
const {width, height, setPainter} = useContext(PainterContext);
export default function FlameHistogram({ paint, children }: Props) {
const { width, height, setPainter } = useContext(PainterContext);
useEffect(() => {
const gameParams = {
@ -17,7 +17,7 @@ export default function FlameHistogram({paint, children}: Props) {
transforms,
final,
paint
}
};
setPainter(chaosGameHistogram(gameParams));
}, [width, height]);

View File

@ -1,11 +1,11 @@
// hidden-start
import {Props as ChaosGameFinalProps} from "../2-transforms/chaosGameFinal";
import {randomBiUnit} from "../src/randomBiUnit";
import {randomChoice} from "../src/randomChoice";
import {camera, histIndex} from "../src/camera";
import {colorFromPalette} from "./colorFromPalette";
import {mixColor} from "./mixColor";
import {paintColor} from "./paintColor";
import { Props as ChaosGameFinalProps } from "../2-transforms/chaosGameFinal";
import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { camera, histIndex } from "../src/camera";
import { colorFromPalette } from "./colorFromPalette";
import { mixColor } from "./mixColor";
import { paintColor } from "./paintColor";
const quality = 15;
const step = 100_000;
@ -20,45 +20,116 @@ export type Props = ChaosGameFinalProps & {
colors: TransformColor[];
finalColor: TransformColor;
}
export function* chaosGameColor({width, height, transforms, final, palette, colors, finalColor}: Props) {
const imgRed = Array<number>(width * height).fill(0);
const imgGreen = Array<number>(width * height).fill(0);
const imgBlue = Array<number>(width * height).fill(0);
const imgAlpha = Array<number>(width * height).fill(0);
let [x, y] = [randomBiUnit(), randomBiUnit()];
export function* chaosGameColor(
{
width,
height,
transforms,
final,
palette,
colors,
finalColor
}: Props
) {
const pixels = width * height;
// highlight-start
const imgRed = Array<number>(pixels)
.fill(0);
const imgGreen = Array<number>(pixels)
.fill(0);
const imgBlue = Array<number>(pixels)
.fill(0);
const imgAlpha = Array<number>(pixels)
.fill(0);
const plotColor = (
x: number,
y: number,
c: number
) => {
const [pixelX, pixelY] =
camera(x, y, width);
if (
pixelX < 0 ||
pixelX >= width ||
pixelY < 0 ||
pixelY >= width
)
return;
const hIndex =
histIndex(pixelX, pixelY, width, 1);
const [r, g, b] =
colorFromPalette(palette, c);
imgRed[hIndex] += r;
imgGreen[hIndex] += g;
imgBlue[hIndex] += b;
imgAlpha[hIndex] += 1;
}
// highlight-end
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
let c = Math.random();
const iterations = width * height * quality;
const iterations = quality * pixels;
for (let i = 0; i < iterations; i++) {
const [transformIndex, transform] = randomChoice(transforms);
const [transformIndex, transform] =
randomChoice(transforms);
[x, y] = transform(x, y);
// highlight-start
const transformColor = colors[transformIndex];
c = mixColor(c, transformColor.color, transformColor.colorSpeed);
const transformColor =
colors[transformIndex];
c = mixColor(
c,
transformColor.color,
transformColor.colorSpeed
);
// highlight-end
const [finalX, finalY] = final(x, y);
if (i > 20) {
const [pixelX, pixelY] = camera(finalX, finalY, width);
const pixelIndex = histIndex(pixelX, pixelY, width, 1);
// highlight-start
const finalC = mixColor(
c,
finalColor.color,
finalColor.colorSpeed
);
// highlight-end
if (pixelIndex < 0 || pixelIndex >= imgAlpha.length)
continue;
const colorFinal = mixColor(c, finalColor.color, finalColor.colorSpeed);
const [r, g, b] = colorFromPalette(palette, colorFinal);
imgRed[pixelIndex] += r;
imgGreen[pixelIndex] += g;
imgBlue[pixelIndex] += b;
imgAlpha[pixelIndex] += 1;
}
if (i > 20)
plotColor(
finalX,
finalY,
finalC
)
if (i % step === 0)
yield paintColor(width, height, imgRed, imgGreen, imgBlue, imgAlpha);
yield paintColor(
width,
height,
imgRed,
imgGreen,
imgBlue,
imgAlpha
);
}
yield paintColor(width, height, imgRed, imgGreen, imgBlue, imgAlpha);
yield paintColor(
width,
height,
imgRed,
imgGreen,
imgBlue,
imgAlpha
);
}

View File

@ -1,45 +1,78 @@
// hidden-start
import {randomBiUnit} from "../src/randomBiUnit";
import {randomChoice} from "../src/randomChoice";
import {Props as ChaosGameFinalProps} from "../2-transforms/chaosGameFinal";
import {camera, histIndex} from "../src/camera";
import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { Props as ChaosGameFinalProps } from "../2-transforms/chaosGameFinal";
import { camera, histIndex } from "../src/camera";
const quality = 10;
const step = 100_000;
// hidden-end
export type Props = ChaosGameFinalProps & {
paint: (width: number, height: number, histogram: number[]) => ImageData;
type Props = ChaosGameFinalProps & {
paint: (
width: number,
height: number,
histogram: number[]
) => ImageData;
}
export function* chaosGameHistogram({width, height, transforms, final, paint}: Props) {
let iterations = quality * width * height;
export function* chaosGameHistogram(
{
width,
height,
transforms,
final,
paint
}: Props
) {
const pixels = width * height;
const iterations = quality * pixels;
// highlight-start
const histogram = Array<number>(width * height).fill(0);
const hist = Array<number>(pixels)
.fill(0);
const plotHist = (
x: number,
y: number
) => {
const [pixelX, pixelY] =
camera(x, y, width);
if (
pixelX < 0 ||
pixelX >= width ||
pixelY < 0 ||
pixelY >= height
)
return;
const hIndex =
histIndex(pixelX, pixelY, width, 1);
hist[hIndex] += 1;
};
// highlight-end
let [x, y] = [randomBiUnit(), randomBiUnit()];
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
for (let i = 0; i < iterations; i++) {
const [_, transform] = randomChoice(transforms);
const [_, transform] =
randomChoice(transforms);
[x, y] = transform(x, y);
const [finalX, finalY] = final(x, y);
if (i > 20) {
// highlight-start
const [pixelX, pixelY] = camera(finalX, finalY, width);
const hIndex = histIndex(pixelX, pixelY, width, 1);
if (hIndex < 0 || hIndex >= histogram.length) {
continue;
}
histogram[hIndex] += 1;
plotHist(finalX, finalY);
// highlight-end
}
if (i % step === 0)
yield paint(width, height, histogram);
yield paint(width, height, hist);
}
yield paint(width, height, histogram);
yield paint(width, height, hist);
}

View File

@ -1,4 +1,14 @@
export function colorFromPalette(palette: number[], colorIndex: number): [number, number, number] {
const paletteIndex = Math.floor(colorIndex * (palette.length / 3)) * 3;
return [palette[paletteIndex], palette[paletteIndex + 1], palette[paletteIndex + 2]];
export function colorFromPalette(
palette: number[],
colorIndex: number
): [number, number, number] {
const numColors = palette.length / 3;
const paletteIndex = Math.floor(
colorIndex * (numColors)
) * 3;
return [
palette[paletteIndex], // red
palette[paletteIndex + 1], // green
palette[paletteIndex + 2] // blue
];
}

View File

@ -165,7 +165,7 @@ import colorFromPaletteSource from "!!raw-loader!./colorFromPalette";
<details>
<summary>As an alternative...</summary>
...you could also interpolate between colors in the palette.
...you could interpolate between colors in the palette.
For example, `flam3` uses [linear interpolation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486)
</details>

View File

@ -1,3 +1,8 @@
export function mixColor(color1: number, color2: number, colorSpeed: number) {
return color1 * (1 - colorSpeed) + color2 * colorSpeed;
export function mixColor(
color1: number,
color2: number,
colorSpeed: number
) {
return color1 * (1 - colorSpeed) +
color2 * colorSpeed;
}

View File

@ -1,6 +1,3 @@
// hidden-start
import {colorFromPalette} from "./colorFromPalette";
// hidden-end
export function paintColor(
width: number,
height: number,
@ -9,17 +6,29 @@ export function paintColor(
blue: number[],
alpha: number[]
): ImageData {
const image = new ImageData(width, height);
const pixels = width * height;
const img =
new ImageData(width, height);
for (let i = 0; i < width * height; i++) {
const alphaScale = Math.log10(alpha[i]) / (alpha[i] * 1.5);
for (let i = 0; i < pixels; i++) {
const scale =
Math.log10(alpha[i]) /
(alpha[i] * 1.5);
const pixelIndex = i * 4;
image.data[pixelIndex] = red[i] * alphaScale * 0xff;
image.data[pixelIndex + 1] = green[i] * alphaScale * 0xff;
image.data[pixelIndex + 2] = blue[i] * alphaScale * 0xff;
image.data[pixelIndex + 3] = alpha[i] * alphaScale * 0xff;
const rVal = red[i] * scale * 0xff;
img.data[pixelIndex] = rVal;
const gVal = green[i] * scale * 0xff;
img.data[pixelIndex + 1] = gVal;
const bVal = blue[i] * scale * 0xff;
img.data[pixelIndex + 2] = bVal;
const aVal = alpha[i] * scale * 0xff;
img.data[pixelIndex + 3] = aVal;
}
return image;
return img;
}

View File

@ -1,18 +1,26 @@
export function paintLinear(width: number, height: number, histogram: number[]): ImageData {
const image = new ImageData(width, height);
export function paintLinear(
width: number,
height: number,
hist: number[]
) {
const img =
new ImageData(width, height);
let valueMax = 0;
for (let value of histogram) {
valueMax = Math.max(valueMax, value);
let hMax = 0;
for (let value of hist) {
hMax = Math.max(hMax, value);
}
for (let i = 0; i < histogram.length; i++) {
for (let i = 0; i < hist.length; i++) {
const pixelIndex = i * 4;
image.data[pixelIndex] = 0;
image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0;
image.data[pixelIndex + 3] = histogram[i] / valueMax * 0xff;
img.data[pixelIndex] = 0;
img.data[pixelIndex + 1] = 0;
img.data[pixelIndex + 2] = 0;
const alpha = hist[i] / hMax * 0xff;
img.data[pixelIndex + 3] = alpha;
}
return image;
return img;
}

View File

@ -1,21 +1,29 @@
export function paintLogarithmic(width: number, height: number, histogram: number[]): ImageData {
const image = new ImageData(width, height);
export function paintLogarithmic(
width: number,
height: number,
hist: number[]
) {
const img =
new ImageData(width, height);
const histogramLog: number[] = [];
histogram.forEach(value => histogramLog.push(Math.log(value)));
const histLog = hist.map(Math.log);
let histogramLogMax = -Infinity;
for (let value of histogramLog) {
histogramLogMax = Math.max(histogramLogMax, value);
let hLogMax = -Infinity;
for (let value of histLog) {
hLogMax = Math.max(hLogMax, value);
}
for (let i = 0; i < histogram.length; i++) {
for (let i = 0; i < hist.length; i++) {
const pixelIndex = i * 4;
image.data[pixelIndex] = 0; // red
image.data[pixelIndex + 1] = 0; // green
image.data[pixelIndex + 2] = 0; // blue
image.data[pixelIndex + 3] = histogramLog[i] / histogramLogMax * 0xff;
img.data[pixelIndex] = 0; // red
img.data[pixelIndex + 1] = 0; // green
img.data[pixelIndex + 2] = 0; // blue
const alpha =
histLog[i] / hLogMax * 0xff;
img.data[pixelIndex + 3] = alpha;
}
return image;
return img;
}

View File

@ -1,8 +1,8 @@
<Flames name="params">
<flame name="post xform" version="Apophysis 2.08 beta" size="600 600" center="0 0" scale="150" oversample="1" filter="0.2" quality="1" background="0 0 0" brightness="4" gamma="4" >
<xform weight="0.422330042096567" color="0" pdj="1" coefs="1.51523 0.740356 -3.048677 -1.455964 0.724135 -0.362059" pdj_a="1.09358" pdj_b="2.13048" pdj_c="2.54127" pdj_d="2.37267" />
<xform weight="0.564534951145298" color="0" julia="1" coefs="-1.381068 1.381068 -1.381068 -1.381068 0 0" />
<xform weight="0.564534951145298" color="0" julia="1" coefs="-1.381068 1.381068 -1.381068 -1.381068 0 0" post="1 0 0 1 2 0"/>
<xform weight="0.0131350067581356" color="0" linear="1" popcorn="1" coefs="0.031393 -0.031367 0.031367 0.031393 0 0" post="1 0 0 1 0.24 0.27" />
<xform weight="0.422330042096567" color="0" pdj="1" coefs="1.51523 0.740356 -3.048677 -1.455964 0.724135 -0.362059" pdj_a="1.09358" pdj_b="2.13048" pdj_c="2.54127" pdj_d="2.37267" />
<palette count="256" format="RGB">
3130323635383B3A3D403F424644484B494D504E52565358
5B585D605D626562686B676D706C737571787B767D807B83
@ -39,9 +39,9 @@
</palette>
</flame>
<flame name="baseline" version="Apophysis 2.08 beta" size="600 600" center="0 0" scale="150" oversample="1" filter="0.2" quality="1" background="0 0 0" brightness="4" gamma="4" >
<xform weight="0.422330042096567" color="0" pdj="1" coefs="1.51523 0.740356 -3.048677 -1.455964 0.724135 -0.362059" pdj_a="1.09358" pdj_b="2.13048" pdj_c="2.54127" pdj_d="2.37267" />
<xform weight="0.564534951145298" color="0.13" julia="1" coefs="-1.381068 1.381068 -1.381068 -1.381068 0 0" />
<xform weight="0.0131350067581356" color="0.844" linear="1" popcorn="1" coefs="0.031393 -0.031367 0.031367 0.031393 0 0" />
<xform weight="0.422330042096567" color="0" pdj="1" coefs="1.51523 0.740356 -3.048677 -1.455964 0.724135 -0.362059" pdj_a="1.09358" pdj_b="2.13048" pdj_c="2.54127" pdj_d="2.37267" />
<palette count="256" format="RGB">
3130323635383B3A3D403F424644484B494D504E52565358
5B585D605D626562686B676D706C737571787B767D807B83
@ -78,9 +78,9 @@
</palette>
</flame>
<flame name="final xform" version="Apophysis 2.08 beta" size="600 600" center="0 0" scale="150" oversample="1" filter="0.2" quality="1" background="1 1 1" brightness="4" gamma="4" >
<xform weight="0.422330042096567" color="0.349" pdj="1" coefs="1.51523 0.740356 -3.048677 -1.455964 0.724135 -0.362059" pdj_a="1.09358" pdj_b="2.13048" pdj_c="2.54127" pdj_d="2.37267" />
<xform weight="0.564534951145298" color="0" julia="1" coefs="-1.381068 1.381068 -1.381068 -1.381068 0 0" />
<xform weight="0.0131350067581356" color="0.766" linear="1" popcorn="1" coefs="0.031393 -0.031367 0.031367 0.031393 0 0" post="1 0 0 1 0.24 0.27" />
<xform weight="0.422330042096567" color="0.349" pdj="1" coefs="1.51523 0.740356 -3.048677 -1.455964 0.724135 -0.362059" pdj_a="1.09358" pdj_b="2.13048" pdj_c="2.54127" pdj_d="2.37267" />
<finalxform color="0" symmetry="1" julia="1" coefs="2 0 0 2 0 0" />
<palette count="256" format="RGB">
3130323635383B3A3D403F424644484B494D504E52565358

View File

@ -11,7 +11,7 @@ export const PainterContext = createContext<PainterProps>(null)
const downloadImage = (name: string) =>
(e: MouseEvent) => {
const link = document.createElement("a");
link.download = "flame.png";
link.download = `${name}.png`;
link.href = (e.target as HTMLCanvasElement).toDataURL("image/png");
link.click();
}

View File

@ -1,8 +1,8 @@
import {Transform, Coefs, applyCoefs} from "./transform";
import {blend, VariationBlend} from "./blend";
import { applyCoefs, Coefs, Transform } from "./transform";
import { blend, Blend } from "./blend";
export const applyTransform = (coefs: Coefs, variations: VariationBlend): Transform =>
(x, y) => blend(...applyCoefs(x, y, coefs), variations);
export const applyTransform = (coefs: Coefs, variations: Blend): Transform =>
(x, y) => blend(...applyCoefs(x, y, coefs), variations)
export const applyPost = (coefsPost: Coefs, transform: Transform): Transform =>
(x, y) => applyCoefs(...transform(x, y), coefsPost);

View File

@ -1,16 +1,17 @@
// hidden-start
import {Variation} from "./variation";
import { Variation } from "./variation";
// hidden-end
export type VariationBlend = [number, Variation][];
export type Blend = [number, Variation][];
export function blend(
x: number,
y: number,
variations: VariationBlend
varFns: Blend
): [number, number] {
let [outX, outY] = [0, 0];
for (const [weight, variation] of variations) {
const [varX, varY] = variation(x, y);
for (const [weight, varFn] of varFns) {
const [varX, varY] = varFn(x, y);
outX += weight * varX;
outY += weight * varY;
}

View File

@ -1,16 +1,21 @@
// hidden-start
import { Variation } from './variation'
import { Variation } from "./variation";
// hidden-end
export const julia: Variation = (x, y) => {
const r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
const omega =
() => Math.random() > 0.5 ? 0 : Math.PI;
export const julia: Variation =
(x, y) => {
const x2 = Math.pow(x, 2);
const y2 = Math.pow(y, 2);
const r = Math.sqrt(x2 + y2);
const theta = Math.atan2(x, y);
const omega = Math.random() > 0.5 ? 0 : Math.PI;
const sqrtR = Math.sqrt(r);
const thetaVal = theta / 2 + omega;
const thetaVal = theta / 2 + omega();
return [
sqrtR * Math.cos(thetaVal),
sqrtR * Math.sin(thetaVal)
]
}
];
};

View File

@ -1,4 +1,5 @@
// hidden-start
import {Variation} from "./variation"
//hidden-end
export const linear: Variation = (x, y) => [x, y];
export const linear: Variation =
(x, y) => [x, y];

View File

@ -3,57 +3,56 @@
* translated into something that's easier to work with.
*/
import { Coefs } from './coefs';
import {VariationBlend} from "./blend";
import { linear } from './linear'
import { julia } from './julia'
import { popcorn } from './popcorn'
import {pdj, PdjParams} from './pdj'
import {Transform} from "./transform";
import {applyPost, applyTransform} from "./applyTransform";
import { Blend } from "./blend";
import { linear } from "./linear";
import { julia } from "./julia";
import { popcorn } from "./popcorn";
import { pdj, PdjParams } from "./pdj";
import { Coefs, Transform } from "./transform";
import { applyPost, applyTransform } from "./applyTransform";
export const identityCoefs: Coefs = {
a: 1, b: 0, c: 0,
d: 0, e: 1, f: 0,
}
d: 0, e: 1, f: 0
};
export const pdjParams: PdjParams = {
a: 1.09358, b: 2.13048, c: 2.54127, d: 2.37267
}
};
export const xform1Weight = 0.56453495;
export const xform1Coefs = {
a: -1.381068, b: -1.381068, c: 0,
d: 1.381068, e: -1.381068, f: 0,
}
d: 1.381068, e: -1.381068, f: 0
};
export const xform1CoefsPost = identityCoefs;
export const xform1Variations: VariationBlend = [
export const xform1Variations: Blend = [
[1, julia]
]
];
export const xform1Color = 0;
export const xform2Weight = 0.013135;
export const xform2Coefs = {
a: 0.031393, b: 0.031367, c: 0,
d: -0.031367, e: 0.031393, f: 0,
}
d: -0.031367, e: 0.031393, f: 0
};
export const xform2CoefsPost = {
a: 1, b: 0, c: 0.24,
d: 0, e: 1, f: 0.27,
}
export const xform2Variations: VariationBlend = [
d: 0, e: 1, f: 0.27
};
export const xform2Variations: Blend = [
[1, linear],
[1, popcorn(xform2Coefs)]
]
];
export const xform2Color = 0.844;
export const xform3Weight = 0.42233;
export const xform3Coefs = {
a: 1.51523, b: -3.048677, c: 0.724135,
d: 0.740356, e: -1.455964, f: -0.362059,
}
d: 0.740356, e: -1.455964, f: -0.362059
};
export const xform3CoefsPost = identityCoefs;
export const xform3Variations: VariationBlend = [
export const xform3Variations: Blend = [
[1, pdj(pdjParams)]
];
export const xform3Color = 0.349;
@ -61,18 +60,18 @@ export const xform3Color = 0.349;
export const xformFinalCoefs = {
a: 2, b: 0, c: 0,
d: 0, e: 2, f: 0
}
};
export const xformFinalCoefsPost = identityCoefs;
export const xformFinalVariations: VariationBlend = [
export const xformFinalVariations: Blend = [
[1, julia]
]
];
export const xformFinalColor = 0;
export const xforms: [number, Transform][] = [
[xform1Weight, applyPost(xform1CoefsPost, applyTransform(xform1Coefs, xform1Variations))],
[xform2Weight, applyPost(xform2CoefsPost, applyTransform(xform2Coefs, xform2Variations))],
[xform3Weight, applyPost(xform3CoefsPost, applyTransform(xform3Coefs, xform3Variations))],
]
[xform3Weight, applyPost(xform3CoefsPost, applyTransform(xform3Coefs, xform3Variations))]
];
export const xformFinal: Transform = applyPost(xformFinalCoefsPost, applyTransform(xformFinalCoefs, xformFinalVariations));
@ -108,7 +107,7 @@ export const paletteString =
"1514091413091413081312081211081110081010070F0F07" +
"0E0E070D0D060C0D060C0C060B0B050A0A05090A05080904" +
"070804060704050704050603040503030403020402010302" +
"0608070C0D0D1112121617171B1C1D2121222626272B2B2D"
"0608070C0D0D1112121617171B1C1D2121222626272B2B2D";
function hexToBytes(hex: string) {
let bytes: number[] = [];

View File

@ -1,10 +1,15 @@
// hidden-start
import { Variation } from './variation'
//hidden-end
export type PdjParams = {a: number, b: number, c: number, d: number};
export function pdj({a, b, c, d}: PdjParams): Variation {
return (x, y) => [
export type PdjParams = {
a: number,
b: number,
c: number,
d: number
};
export const pdj =
({a, b, c, d}: PdjParams): Variation =>
(x, y) => [
Math.sin(a * y) - Math.cos(b * x),
Math.sin(c * x) - Math.cos(d * y)
]
}

View File

@ -1,11 +1,11 @@
import { camera, histIndex } from "./camera"
import { camera, histIndex } from "./camera";
export function plotBinary(x: number, y: number, image: ImageData) {
const [pixelX, pixelY] = camera(x, y, image.width);
const pixelIndex = histIndex(pixelX, pixelY, image.width, 4);
if (pixelIndex < 0 || pixelIndex > image.data.length) {
if (pixelX < 0 || pixelX >= image.width || pixelY < 0 || pixelY >= image.height)
return;
}
const pixelIndex = histIndex(pixelX, pixelY, image.width, 4);
image.data[pixelIndex] = 0;
image.data[pixelIndex + 1] = 0;

View File

@ -1,10 +1,10 @@
// hidden-start
import {Coefs} from './coefs'
import {Variation} from './variation'
import { Coefs } from "./transform";
import { Variation } from "./variation";
// hidden-end
export function popcorn({c, f}: Coefs): Variation {
return (x, y) => [
export const popcorn =
({ c, f }: Coefs): Variation =>
(x, y) => [
x + c * Math.sin(Math.tan(3 * y)),
y + f * Math.sin(Math.tan(3 * x))
];
}

View File

@ -7,7 +7,8 @@ export function randomChoice<T>(
);
let choice = Math.random() * weightSum;
for (const [idx, elem] of choices.entries()) {
for (const entry of choices.entries()) {
const [idx, elem] = entry;
const [weight, t] = elem;
if (choice < weight) {
return [idx, t];

View File

@ -1,13 +1,23 @@
export type Transform = (x: number, y: number) => [number, number];
export type Transform =
(x: number, y: number) =>
[number, number];
export interface Coefs {
a: number, b: number, c: number,
d: number, e: number, f: number
a: number,
b: number,
c: number,
d: number,
e: number,
f: number
}
export function applyCoefs(x: number, y: number, coefs: Coefs): [number, number] {
export function applyCoefs(
x: number,
y: number,
coefs: Coefs
): [number, number] {
return [
(x * coefs.a + y * coefs.b + coefs.c),
(x * coefs.d + y * coefs.e + coefs.f)
]
];
}

View File

@ -1 +1,4 @@
export type Variation = (x: number, y: number) => [number, number];
export type Variation = (
x: number,
y: number
) => [number, number];