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);
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;

View File

@ -1,51 +1,52 @@
import TeX from "@matejmazur/react-katex";
import {Coefs} from "../src/coefs";
import { Coefs } from "../src/transform";
import styles from "../src/css/styles.module.css";
export interface Props {
title: String;
isPost: boolean;
coefs: Coefs;
setCoefs: (coefs: Coefs) => void;
resetCoefs: () => void;
title: String;
isPost: boolean;
coefs: Coefs;
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>
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.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)})}/>
</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)})}/>
</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)})}/>
</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)})}/>
</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)})}/>
</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)})}/>
</div>
</div>
)
}
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.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) })} />
</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) })} />
</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) })} />
</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) })} />
</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) })} />
</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) })} />
</div>
</div>
);
};

View File

@ -1,68 +1,68 @@
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)
const resetXform1Variations = () => setXform1Variations(xform1VariationsDefault);
const xform1VariationsDefault: VariationProps = {
linear: 0,
julia: 1,
popcorn: 0,
pdj: 0
};
const [xform1Variations, setXform1Variations] = useState(xform1VariationsDefault);
const resetXform1Variations = () => setXform1Variations(xform1VariationsDefault);
const xform2VariationsDefault: VariationProps = {
linear: 1,
julia: 0,
popcorn: 1,
pdj: 0
}
const [xform2Variations, setXform2Variations] = useState(xform2VariationsDefault)
const resetXform2Variations = () => setXform2Variations(xform2VariationsDefault);
const xform2VariationsDefault: VariationProps = {
linear: 1,
julia: 0,
popcorn: 1,
pdj: 0
};
const [xform2Variations, setXform2Variations] = useState(xform2VariationsDefault);
const resetXform2Variations = () => setXform2Variations(xform2VariationsDefault);
const xform3VariationsDefault: VariationProps = {
linear: 0,
julia: 0,
popcorn: 0,
pdj: 1
}
const [xform3Variations, setXform3Variations] = useState(xform3VariationsDefault)
const resetXform3Variations = () => setXform3Variations(xform3VariationsDefault);
const xform3VariationsDefault: VariationProps = {
linear: 0,
julia: 0,
popcorn: 0,
pdj: 1
};
const [xform3Variations, setXform3Variations] = useState(xform3VariationsDefault);
const resetXform3Variations = () => setXform3Variations(xform3VariationsDefault);
const identityXform: Transform = (x, y) => [x, y];
const identityXform: Transform = (x, y) => [x, y];
useEffect(() => {
const transforms: [number, Transform][] = [
[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))]
]
useEffect(() => {
const transforms: [number, Transform][] = [
[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]);
const gameParams = {
width,
height,
transforms,
final: identityXform
};
setPainter(chaosGameFinal(gameParams));
}, [xform1Variations, xform2Variations, xform3Variations]);
return (
<>
<VariationEditor title={"Transform 1"} variations={xform1Variations} setVariations={setXform1Variations}
resetVariations={resetXform1Variations}/>
<VariationEditor title={"Transform 2"} variations={xform2Variations} setVariations={setXform2Variations}
resetVariations={resetXform2Variations}/>
<VariationEditor title={"Transform 3"} variations={xform3Variations} setVariations={setXform3Variations}
resetVariations={resetXform3Variations}/>
</>
)
return (
<>
<VariationEditor title={"Transform 1"} variations={xform1Variations} setVariations={setXform1Variations}
resetVariations={resetXform1Variations} />
<VariationEditor title={"Transform 2"} variations={xform2Variations} setVariations={setXform2Variations}
resetVariations={resetXform2Variations} />
<VariationEditor title={"Transform 3"} variations={xform3Variations} setVariations={setXform3Variations}
resetVariations={resetXform3Variations} />
</>
);
}

View File

@ -1,46 +1,46 @@
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);
const [xformFinalCoefs, setXformFinalCoefs] = useState<Coefs>(params.xformFinalCoefs);
const resetXformFinalCoefs = () => setXformFinalCoefs(params.xformFinalCoefs);
const xformFinalVariationsDefault: VariationProps = {
linear: 0,
julia: 1,
popcorn: 0,
pdj: 0
}
const [xformFinalVariations, setXformFinalVariations] = useState<VariationProps>(xformFinalVariationsDefault);
const resetXformFinalVariations = () => setXformFinalVariations(xformFinalVariationsDefault);
const xformFinalVariationsDefault: VariationProps = {
linear: 0,
julia: 1,
popcorn: 0,
pdj: 0
};
const [xformFinalVariations, setXformFinalVariations] = useState<VariationProps>(xformFinalVariationsDefault);
const resetXformFinalVariations = () => setXformFinalVariations(xformFinalVariationsDefault);
const [xformFinalCoefsPost, setXformFinalCoefsPost] = useState<Coefs>(params.xformFinalCoefsPost);
const resetXformFinalCoefsPost = () => setXformFinalCoefsPost(params.xformFinalCoefsPost);
const [xformFinalCoefsPost, setXformFinalCoefsPost] = useState<Coefs>(params.xformFinalCoefsPost);
const resetXformFinalCoefsPost = () => setXformFinalCoefsPost(params.xformFinalCoefsPost);
useEffect(() => {
const finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
const finalXform = applyPost(xformFinalCoefsPost, applyTransform(xformFinalCoefs, finalBlend));
useEffect(() => {
const finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
const finalXform = applyPost(xformFinalCoefsPost, applyTransform(xformFinalCoefs, finalBlend));
setPainter(chaosGameFinal({width, height, transforms: params.xforms, final: finalXform}));
}, [xformFinalCoefs, xformFinalVariations, xformFinalCoefsPost]);
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}/>
<VariationEditor title={"Final Transform Variations"} variations={xformFinalVariations}
setVariations={setXformFinalVariations} resetVariations={resetXformFinalVariations}/>
<CoefEditor title={"Final Transform Post"} isPost={true} coefs={xformFinalCoefsPost}
setCoefs={setXformFinalCoefsPost} resetCoefs={resetXformFinalCoefsPost}/>
</>
)
return (
<>
<CoefEditor title={"Final Transform"} isPost={false} coefs={xformFinalCoefs} setCoefs={setXformFinalCoefs}
resetCoefs={resetXformFinalCoefs} />
<VariationEditor title={"Final Transform Variations"} variations={xformFinalVariations}
setVariations={setXformFinalVariations} resetVariations={resetXformFinalVariations} />
<CoefEditor title={"Final Transform Post"} isPost={true} coefs={xformFinalCoefsPost}
setCoefs={setXformFinalCoefsPost} resetCoefs={resetXformFinalCoefsPost} />
</>
);
}

View File

@ -1,46 +1,46 @@
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);
const [xform1CoefsPost, setXform1CoefsPost] = useState<Coefs>(params.xform1CoefsPost);
const resetXform1CoefsPost = () => setXform1CoefsPost(params.xform1CoefsPost);
const [xform2CoefsPost, setXform2CoefsPost] = useState<Coefs>(params.xform2CoefsPost);
const resetXform2CoefsPost = () => setXform2CoefsPost(params.xform2CoefsPost);
const [xform2CoefsPost, setXform2CoefsPost] = useState<Coefs>(params.xform2CoefsPost);
const resetXform2CoefsPost = () => setXform2CoefsPost(params.xform2CoefsPost);
const [xform3CoefsPost, setXform3CoefsPost] = useState<Coefs>(params.xform3CoefsPost);
const resetXform3CoefsPost = () => setXform3CoefsPost(params.xform3CoefsPost);
const [xform3CoefsPost, setXform3CoefsPost] = useState<Coefs>(params.xform3CoefsPost);
const resetXform3CoefsPost = () => setXform3CoefsPost(params.xform3CoefsPost);
const identityXform: Transform = (x, y) => [x, y];
const identityXform: Transform = (x, y) => [x, y];
const gameParams: ChaosGameFinalProps = {
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))],
],
final: identityXform
}
useEffect(() => setPainter(chaosGameFinal(gameParams)), [xform1CoefsPost, xform2CoefsPost, xform3CoefsPost]);
const gameParams: ChaosGameFinalProps = {
width,
height,
transforms: [
[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}/>
<CoefEditor title={"Transform 2 Post"} isPost={true} coefs={xform2CoefsPost} setCoefs={setXform2CoefsPost}
resetCoefs={resetXform2CoefsPost}/>
<CoefEditor title={"Transform 3 Post"} isPost={true} coefs={xform3CoefsPost} setCoefs={setXform3CoefsPost}
resetCoefs={resetXform3CoefsPost}/>
</>
)
return (
<>
<CoefEditor title={"Transform 1 Post"} isPost={true} coefs={xform1CoefsPost} setCoefs={setXform1CoefsPost}
resetCoefs={resetXform1CoefsPost} />
<CoefEditor title={"Transform 2 Post"} isPost={true} coefs={xform2CoefsPost} setCoefs={setXform2CoefsPost}
resetCoefs={resetXform2CoefsPost} />
<CoefEditor title={"Transform 3 Post"} isPost={true} coefs={xform3CoefsPost} setCoefs={setXform3CoefsPost}
resetCoefs={resetXform3CoefsPost} />
</>
);
}

View File

@ -1,45 +1,45 @@
import styles from "../src/css/styles.module.css"
import styles from "../src/css/styles.module.css";
export interface VariationProps {
linear: number;
julia: number;
popcorn: number;
pdj: number;
linear: number;
julia: number;
popcorn: number;
pdj: number;
}
export interface Props {
title: String;
variations: VariationProps;
setVariations: (variations: VariationProps) => void;
resetVariations: () => void;
title: String;
variations: VariationProps;
setVariations: (variations: VariationProps) => void;
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.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)})}/>
</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)})}/>
</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)})}/>
</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)})}/>
</div>
</div>
)
}
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.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) })} />
</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) })} />
</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) })} />
</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) })} />
</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 {
return [
[variations.linear, linear],
[variations.julia, julia],
[variations.popcorn, popcorn(coefs)],
[variations.pdj, pdj(pdjParams)]
]
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,37 +1,51 @@
// 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 & {
final: Transform,
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;
for (let i = 0; i < iterations; i++) {
const [_, transform] = randomChoice(transforms);
[x, y] = transform(x, y);
export function* chaosGameFinal(
{
width,
height,
transforms,
final
}: Props
) {
let img =
new ImageData(width, height);
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
// highlight-start
const [finalX, finalY] = final(x, y);
// highlight-end
const pixels = width * height;
const iterations = quality * pixels;
for (let i = 0; i < iterations; i++) {
const [_, transform] =
randomChoice(transforms);
[x, y] = transform(x, y);
if (i > 20)
// highlight-start
plot(finalX, finalY, image);
// highlight-end
// highlight-start
const [finalX, finalY] = final(x, y);
// highlight-end
if (i % step === 0)
yield image;
}
if (i > 20)
// highlight-start
plot(finalX, finalY, img);
// highlight-end
yield image;
if (i % step === 0)
yield img;
}
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,174 +1,181 @@
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;
height: number;
palette: number[];
children?: React.ReactNode;
}
export const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => {
const sizingRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useEffect(() => {
if (sizingRef) {
setWidth(sizingRef.current.offsetWidth);
}
}, [sizingRef]);
export const PaletteBar: React.FC<PaletteBarProps> = ({ height, palette, children }) => {
const sizingRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useEffect(() => {
if (sizingRef) {
setWidth(sizingRef.current.offsetWidth);
}
}, [sizingRef]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const paletteImage = useMemo(() => {
if (width === 0) {
return;
}
const canvasRef = useRef<HTMLCanvasElement>(null);
const paletteImage = useMemo(() => {
if (width === 0) {
return;
}
const image = new ImageData(width, height);
for (let x = 0; x < width; x++) {
const colorIndex = x / width;
const [r, g, b] = colorFromPalette(palette, colorIndex);
const image = new ImageData(width, height);
for (let x = 0; x < width; x++) {
const colorIndex = x / width;
const [r, g, b] = colorFromPalette(palette, colorIndex);
for (let y = 0; y < height; y++) {
const pixelIndex = histIndex(x, y, width, 4);
image.data[pixelIndex] = r * 0xff;
image.data[pixelIndex + 1] = g * 0xff;
image.data[pixelIndex + 2] = b * 0xff;
image.data[pixelIndex + 3] = 0xff;
}
}
for (let y = 0; y < height; y++) {
const pixelIndex = histIndex(x, y, width, 4);
image.data[pixelIndex] = r * 0xff;
image.data[pixelIndex + 1] = g * 0xff;
image.data[pixelIndex + 2] = b * 0xff;
image.data[pixelIndex + 3] = 0xff;
}
}
return image;
}, [width, height, palette]);
return image;
}, [width, height, palette]);
useEffect(() => {
if (canvasRef && paletteImage) {
canvasRef.current.getContext("2d").putImageData(paletteImage, 0, 0);
}
}, [canvasRef, paletteImage]);
useEffect(() => {
if (canvasRef && paletteImage) {
canvasRef.current.getContext("2d").putImageData(paletteImage, 0, 0);
}
}, [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>
{children}
</>
)
}
return (
<>
<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;
palette: number[];
transformColor: TransformColor;
setTransformColor: (transformColor: TransformColor) => void;
resetTransformColor: () => void;
children?: React.ReactNode;
title: string;
palette: number[];
transformColor: TransformColor;
setTransformColor: (transformColor: TransformColor) => void;
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 [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.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)})}/>
</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)})}/>
</div>
<div className={styles.inputElement} style={{
width: '100%',
height: '100%',
backgroundColor: colorCss,
filter: colorMode === 'dark' ? 'invert(1)' : ''
}}/>
</div>
{children}
</>
)
}
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.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) })} />
</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) })} />
</div>
<div className={styles.inputElement} style={{
width: "100%",
height: "100%",
backgroundColor: colorCss,
filter: colorMode === "dark" ? "invert(1)" : ""
}} />
</div>
{children}
</>
);
};
type Props = {
quality?: number;
children?: React.ReactElement;
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 [xform1Color, setXform1Color] = useState(xform1ColorDefault);
const resetXform1Color = () => setXform1Color(xform1ColorDefault);
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 [xform2Color, setXform2Color] = useState(xform2ColorDefault);
const resetXform2Color = () => setXform2Color(xform2ColorDefault);
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 [xform3Color, setXform3Color] = useState(xform3ColorDefault);
const resetXform3Color = () => setXform3Color(xform3ColorDefault);
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 [xformFinalColor, setXformFinalColor] = useState(xformFinalColorDefault);
const resetXformFinalColor = () => setXformFinalColor(xformFinalColorDefault);
const xformFinalColorDefault: TransformColor = { color: params.xformFinalColor, colorSpeed: 0 };
const [xformFinalColor, setXformFinalColor] = useState(xformFinalColorDefault);
const resetXformFinalColor = () => setXformFinalColor(xformFinalColorDefault);
useEffect(() => {
const gameParams: ChaosGameColorProps = {
width,
height,
transforms: params.xforms,
final: params.xformFinal,
palette: params.palette,
colors: [xform1Color, xform2Color, xform3Color],
finalColor: xformFinalColor
}
setPainter(chaosGameColor(gameParams));
}, [xform1Color, xform2Color, xform3Color, xformFinalColor]);
useEffect(() => {
const gameParams: ChaosGameColorProps = {
width,
height,
transforms: params.xforms,
final: params.xformFinal,
palette: params.palette,
colors: [xform1Color, xform2Color, xform3Color],
finalColor: xformFinalColor
};
setPainter(chaosGameColor(gameParams));
}, [xform1Color, xform2Color, xform3Color, xformFinalColor]);
return (
<>
<PaletteBar height={40} palette={params.palette}/>
<ColorEditor
title={"Transform 1"}
palette={params.palette}
transformColor={xform1Color}
setTransformColor={setXform1Color}
resetTransformColor={resetXform1Color}/>
<ColorEditor
title={"Transform 2"}
palette={params.palette}
transformColor={xform2Color}
setTransformColor={setXform2Color}
resetTransformColor={resetXform2Color}/>
<ColorEditor
title={"Transform 3"}
palette={params.palette}
transformColor={xform3Color}
setTransformColor={setXform3Color}
resetTransformColor={resetXform3Color}/>
<ColorEditor
title={"Transform Final"}
palette={params.palette}
transformColor={xformFinalColor}
setTransformColor={setXformFinalColor}
resetTransformColor={resetXformFinalColor}/>
{children}
</>
);
return (
<>
<PaletteBar height={40} palette={params.palette} />
<ColorEditor
title={"Transform 1"}
palette={params.palette}
transformColor={xform1Color}
setTransformColor={setXform1Color}
resetTransformColor={resetXform1Color} />
<ColorEditor
title={"Transform 2"}
palette={params.palette}
transformColor={xform2Color}
setTransformColor={setXform2Color}
resetTransformColor={resetXform2Color} />
<ColorEditor
title={"Transform 3"}
palette={params.palette}
transformColor={xform3Color}
setTransformColor={setXform3Color}
resetTransformColor={resetXform3Color} />
<ColorEditor
title={"Transform Final"}
palette={params.palette}
transformColor={xformFinalColor}
setTransformColor={setXformFinalColor}
resetTransformColor={resetXformFinalColor} />
{children}
</>
);
}

View File

@ -1,25 +1,25 @@
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;
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 = {
width,
height,
transforms,
final,
paint
}
setPainter(chaosGameHistogram(gameParams));
}, [width, height]);
useEffect(() => {
const gameParams = {
width,
height,
transforms,
final,
paint
};
setPainter(chaosGameHistogram(gameParams));
}, [width, height]);
return children;
return children;
}

View File

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

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;
// highlight-start
const histogram = Array<number>(width * height).fill(0);
// highlight-end
export function* chaosGameHistogram(
{
width,
height,
transforms,
final,
paint
}: Props
) {
const pixels = width * height;
const iterations = quality * pixels;
let [x, y] = [randomBiUnit(), randomBiUnit()];
// highlight-start
const hist = Array<number>(pixels)
.fill(0);
for (let i = 0; i < iterations; i++) {
const [_, transform] = randomChoice(transforms);
[x, y] = transform(x, y);
const [finalX, finalY] = final(x, y);
const plotHist = (
x: number,
y: number
) => {
const [pixelX, pixelY] =
camera(x, y, width);
if (i > 20) {
// highlight-start
const [pixelX, pixelY] = camera(finalX, finalY, width);
const hIndex = histIndex(pixelX, pixelY, width, 1);
if (
pixelX < 0 ||
pixelX >= width ||
pixelY < 0 ||
pixelY >= height
)
return;
if (hIndex < 0 || hIndex >= histogram.length) {
continue;
}
const hIndex =
histIndex(pixelX, pixelY, width, 1);
histogram[hIndex] += 1;
// highlight-end
}
hist[hIndex] += 1;
};
// highlight-end
if (i % step === 0)
yield paint(width, height, histogram);
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
for (let i = 0; i < iterations; i++) {
const [_, transform] =
randomChoice(transforms);
[x, y] = transform(x, y);
const [finalX, finalY] = final(x, y);
if (i > 20) {
// highlight-start
plotHist(finalX, finalY);
// highlight-end
}
yield paint(width, height, histogram);
if (i % step === 0)
yield paint(width, height, hist);
}
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,25 +1,34 @@
// hidden-start
import {colorFromPalette} from "./colorFromPalette";
// hidden-end
export function paintColor(
width: number,
height: number,
red: number[],
green: number[],
blue: number[],
alpha: number[]
width: number,
height: number,
red: number[],
green: number[],
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 pixelIndex = i * 4;
return image;
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 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++) {
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;
}
for (let i = 0; i < hist.length; i++) {
const pixelIndex = i * 4;
return image;
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 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++) {
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;
}
for (let i = 0; i < hist.length; i++) {
const pixelIndex = i * 4;
return image;
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 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);
(x, y) => applyCoefs(...transform(x, y), coefsPost);

View File

@ -1,19 +1,20 @@
// 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
x: number,
y: number,
varFns: Blend
): [number, number] {
let [outX, outY] = [0, 0];
let [outX, outY] = [0, 0];
for (const [weight, variation] of variations) {
const [varX, varY] = variation(x, y);
outX += weight * varX;
outY += weight * varY;
}
for (const [weight, varFn] of varFns) {
const [varX, varY] = varFn(x, y);
outX += weight * varX;
outY += weight * varY;
}
return [outX, outY];
return [outX, outY];
}

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)
]
}
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,119 +3,118 @@
* 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,
}
a: 1, b: 0, c: 0,
d: 0, e: 1, f: 0
};
export const pdjParams: PdjParams = {
a: 1.09358, b: 2.13048, c: 2.54127, d: 2.37267
}
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,
}
a: -1.381068, b: -1.381068, c: 0,
d: 1.381068, e: -1.381068, f: 0
};
export const xform1CoefsPost = identityCoefs;
export const xform1Variations: VariationBlend = [
[1, julia]
]
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,
}
a: 0.031393, b: 0.031367, c: 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 = [
[1, linear],
[1, popcorn(xform2Coefs)]
]
a: 1, b: 0, c: 0.24,
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,
}
a: 1.51523, b: -3.048677, c: 0.724135,
d: 0.740356, e: -1.455964, f: -0.362059
};
export const xform3CoefsPost = identityCoefs;
export const xform3Variations: VariationBlend = [
[1, pdj(pdjParams)]
export const xform3Variations: Blend = [
[1, pdj(pdjParams)]
];
export const xform3Color = 0.349;
export const xformFinalCoefs = {
a: 2, b: 0, c: 0,
d: 0, e: 2, f: 0
}
a: 2, b: 0, c: 0,
d: 0, e: 2, f: 0
};
export const xformFinalCoefsPost = identityCoefs;
export const xformFinalVariations: VariationBlend = [
[1, julia]
]
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))],
]
[xform1Weight, applyPost(xform1CoefsPost, applyTransform(xform1Coefs, xform1Variations))],
[xform2Weight, applyPost(xform2CoefsPost, applyTransform(xform2Coefs, xform2Variations))],
[xform3Weight, applyPost(xform3CoefsPost, applyTransform(xform3Coefs, xform3Variations))]
];
export const xformFinal: Transform = applyPost(xformFinalCoefsPost, applyTransform(xformFinalCoefs, xformFinalVariations));
export const paletteString =
"3130323635383B3A3D403F424644484B494D504E52565358" +
"5B585D605D626562686B676D706C737571787B767D807B83" +
"8580888A858D908A93958F989A949DA099A3A59EA8AAA3AD" +
"AFA8B3B5ADB8BAB2BEBFB7C3C5BCC8CAC1CECFC6D3D4CBD8" +
"DAD0DEDFD5E3DFD2E0DFCEDDE0CBDAE0C8D7E0C4D3E0C1D0" +
"E1BECDE1BBCAE1B7C7E1B4C4E1B1C1E2ADBEE2AABAE2A7B7" +
"E2A3B4E2A0B1E39DAEE399ABE396A8E393A5E490A1E48C9E" +
"E4899BE48698E48295E57F92E57C8FE5788CE57589E57285" +
"E66E82E66B7FE6687CE66479E76176E75E73E75B70E7576C" +
"E75469E85166E84D63E84A60E4495EE0485CDC475BD84659" +
"D44557D04455CB4353C74252C34150BF404EBB3F4CB73E4B" +
"B33D49AF3C47AB3B45A73A43A339429F38409B373E97363C" +
"92353A8E34398A33378632358231337E30327A2F30762E2E" +
"722D2C6E2C2A6A2B29662A276229255E2823592721552620" +
"51251E4D241C49231A4522194121173D20153C1F153A1F14" +
"391E14381E14361D14351C13341C13321B13311B132F1A12" +
"2E19122D19122B18122A1811291711271611261611251510" +
"23151022141021140F1F130F1E120F1C120F1B110E1A110E" +
"18100E170F0E160F0D140E0D130E0D120D0D100C0C0F0C0C" +
"0E0B0C0C0B0C0B0A0B09090B08090B07080B05080A04070A" +
"0606090804090A03088C46728A457087446D85436B824369" +
"8042667D41647B4061793F5F763E5D743D5A713D586F3C56" +
"6C3B536A3A5168394F65384C63374A6037485E36455B3543" +
"59344057333E54323C5231394F31374D30354A2F32482E30" +
"462D2E432C2B412B293E2B273C2A2439292237281F35271D" +
"32261B3025182D25162B241428231126220F25210F24210E" +
"23200E221F0E221E0D211E0D201D0D1F1C0D1E1B0C1D1B0C" +
"1C1A0C1B190B1B180B1A180B19170A18160A17150A161509" +
"1514091413091413081312081211081110081010070F0F07" +
"0E0E070D0D060C0D060C0C060B0B050A0A05090A05080904" +
"070804060704050704050603040503030403020402010302" +
"0608070C0D0D1112121617171B1C1D2121222626272B2B2D"
"3130323635383B3A3D403F424644484B494D504E52565358" +
"5B585D605D626562686B676D706C737571787B767D807B83" +
"8580888A858D908A93958F989A949DA099A3A59EA8AAA3AD" +
"AFA8B3B5ADB8BAB2BEBFB7C3C5BCC8CAC1CECFC6D3D4CBD8" +
"DAD0DEDFD5E3DFD2E0DFCEDDE0CBDAE0C8D7E0C4D3E0C1D0" +
"E1BECDE1BBCAE1B7C7E1B4C4E1B1C1E2ADBEE2AABAE2A7B7" +
"E2A3B4E2A0B1E39DAEE399ABE396A8E393A5E490A1E48C9E" +
"E4899BE48698E48295E57F92E57C8FE5788CE57589E57285" +
"E66E82E66B7FE6687CE66479E76176E75E73E75B70E7576C" +
"E75469E85166E84D63E84A60E4495EE0485CDC475BD84659" +
"D44557D04455CB4353C74252C34150BF404EBB3F4CB73E4B" +
"B33D49AF3C47AB3B45A73A43A339429F38409B373E97363C" +
"92353A8E34398A33378632358231337E30327A2F30762E2E" +
"722D2C6E2C2A6A2B29662A276229255E2823592721552620" +
"51251E4D241C49231A4522194121173D20153C1F153A1F14" +
"391E14381E14361D14351C13341C13321B13311B132F1A12" +
"2E19122D19122B18122A1811291711271611261611251510" +
"23151022141021140F1F130F1E120F1C120F1B110E1A110E" +
"18100E170F0E160F0D140E0D130E0D120D0D100C0C0F0C0C" +
"0E0B0C0C0B0C0B0A0B09090B08090B07080B05080A04070A" +
"0606090804090A03088C46728A457087446D85436B824369" +
"8042667D41647B4061793F5F763E5D743D5A713D586F3C56" +
"6C3B536A3A5168394F65384C63374A6037485E36455B3543" +
"59344057333E54323C5231394F31374D30354A2F32482E30" +
"462D2E432C2B412B293E2B273C2A2439292237281F35271D" +
"32261B3025182D25162B241428231126220F25210F24210E" +
"23200E221F0E221E0D211E0D201D0D1F1C0D1E1B0C1D1B0C" +
"1C1A0C1B190B1B180B1A180B19170A18160A17150A161509" +
"1514091413091413081312081211081110081010070F0F07" +
"0E0E070D0D060C0D060C0C060B0B050A0A05090A05080904" +
"070804060704050704050603040503030403020402010302" +
"0608070C0D0D1112121617171B1C1D2121222626272B2B2D";
function hexToBytes(hex: string) {
let bytes: number[] = [];
for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substring(i, i + 2), 16));
}
return bytes;
let bytes: number[] = [];
for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substring(i, i + 2), 16));
}
return bytes;
}
export const palette = hexToBytes(paletteString).map(value => value / 0xff);

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,14 +1,14 @@
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) {
return;
}
const [pixelX, pixelY] = camera(x, y, image.width);
if (pixelX < 0 || pixelX >= image.width || pixelY < 0 || pixelY >= image.height)
return;
image.data[pixelIndex] = 0;
image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0;
image.data[pixelIndex + 3] = 0xff;
const pixelIndex = histIndex(pixelX, pixelY, image.width, 4);
image.data[pixelIndex] = 0;
image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0;
image.data[pixelIndex + 3] = 0xff;
}

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) => [
x + c * Math.sin(Math.tan(3 * y)),
y + f * Math.sin(Math.tan(3 * x))
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] {
return [
(x * coefs.a + y * coefs.b + coefs.c),
(x * coefs.d + y * coefs.e + coefs.f)
]
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];