mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Mass formatting, fix mobile display, fix issues with image wrap-around
This commit is contained in:
parent
1ce6137c17
commit
456c3a66e5
4
blog/2024-11-15-playing-with-fire/.prettierrc
Normal file
4
blog/2024-11-15-playing-with-fire/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import {SquareCanvas, PainterContext} from "../src/Canvas";
|
||||
import { PainterContext, SquareCanvas } from "../src/Canvas";
|
||||
import { useContext, useEffect } from "react";
|
||||
|
||||
export function Render({ f }) {
|
||||
@ -17,5 +17,5 @@ export default function Gasket({f}) {
|
||||
<SquareCanvas name={"gasket"}>
|
||||
<Render f={f} />
|
||||
</SquareCanvas>
|
||||
)
|
||||
);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import {useEffect, useState, useContext, useRef} from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { PainterContext } from "../src/Canvas";
|
||||
import { chaosGameWeighted } from "./chaosGameWeighted";
|
||||
import TeX from '@matejmazur/react-katex';
|
||||
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];
|
||||
|
||||
@ -31,19 +31,19 @@ export default function GasketWeighted() {
|
||||
<>
|
||||
<div className={styles.inputElement}>
|
||||
<p><TeX>{title}</TeX>: {weight}</p>
|
||||
<input type={'range'} min={0} max={1} step={.01} value={weight}
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
export function camera(
|
||||
size: number,
|
||||
x: number,
|
||||
y: number
|
||||
y: number,
|
||||
size: number
|
||||
): [number, number] {
|
||||
return [
|
||||
Math.floor(x * size),
|
||||
|
@ -6,28 +6,29 @@ const xforms = [
|
||||
(x, y) => [x / 2, y / 2],
|
||||
(x, y) => [(x + 1) / 2, y / 2],
|
||||
(x, y) => [x / 2, (y + 1) / 2]
|
||||
]
|
||||
];
|
||||
|
||||
function* chaosGame({ width, height }) {
|
||||
const step = 1000;
|
||||
let img = new ImageData(width, height);
|
||||
let img =
|
||||
new ImageData(width, height);
|
||||
let [x, y] = [
|
||||
randomBiUnit(),
|
||||
randomBiUnit()
|
||||
];
|
||||
|
||||
for (let c = 0; c < iterations; c++) {
|
||||
const i = randomInteger(0, xforms.length);
|
||||
[x, y] = xforms[i](x, y);
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const index =
|
||||
randomInteger(0, xforms.length);
|
||||
[x, y] = xforms[index](x, y);
|
||||
|
||||
if (c > 20)
|
||||
if (i > 20)
|
||||
plot(x, y, img);
|
||||
|
||||
if (c % step === 0)
|
||||
if (i % 1000 === 0)
|
||||
yield img;
|
||||
}
|
||||
|
||||
yield img;
|
||||
}
|
||||
|
||||
render(<Gasket f={chaosGame}/>)
|
||||
render(<Gasket f={chaosGame} />);
|
||||
|
@ -1,7 +1,7 @@
|
||||
// hidden-start
|
||||
import { randomBiUnit } from "../src/randomBiUnit";
|
||||
import { randomChoice } from "../src/randomChoice";
|
||||
import { plot } from "./plot"
|
||||
import { plot } from "./plot";
|
||||
import { Transform } from "../src/transform";
|
||||
|
||||
const quality = 0.5;
|
||||
@ -12,26 +12,30 @@ export type Props = {
|
||||
height: number,
|
||||
transforms: [number, Transform][]
|
||||
}
|
||||
|
||||
export function* chaosGameWeighted(
|
||||
{ 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;
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transfor
|
||||
The general form of an affine transformation is:
|
||||
|
||||
$$
|
||||
F_i(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i)
|
||||
F_i(a_i x + b_i y + c_i, d_i x + e_i y + f_i)
|
||||
$$
|
||||
|
||||
import transformSource from "!!raw-loader!../src/transform"
|
||||
@ -102,8 +102,7 @@ c &= 0.5 \\
|
||||
d &= 0 \\
|
||||
e &= 1 \\
|
||||
f &= 1.5 \\
|
||||
F_{shift}(x,y) &= (1 \cdot x + 0 \cdot y + 0.5, 0 \cdot x + 1 \cdot y + 0.5) \\
|
||||
F_{shift}(x, y) &= (x + 0.5, y + 1.5)
|
||||
F_{shift}(x, y) &= (1 \cdot x + 0.5, 1 \cdot y + 1.5)
|
||||
\end{align*}
|
||||
$$
|
||||
|
||||
@ -155,7 +154,7 @@ explaining the mathematics of iterated function systems:
|
||||
> of finite compositions $F_{i_1...i_p}$ of members of $F$.
|
||||
|
||||
:::note
|
||||
I've tweaked the wording slightly to match the conventions of the Fractal Flame paper
|
||||
I've tweaked the conventions of that paper slightly to match the Fractal Flame paper
|
||||
:::
|
||||
|
||||
Before your eyes glaze over, let's unpack this:
|
||||
@ -174,15 +173,15 @@ Thus, by applying the functions to fixed points of our system, we will find the
|
||||
...then there are some extra details I've glossed over so far.
|
||||
|
||||
First, the Hutchinson paper requires that the functions $F_i$ be _contractive_ for the solution set to exist.
|
||||
That is, applying the function to a point must bring it closer to other points. However, as the Fractal Flame
|
||||
That is, applying the function to a point must bring it closer to other points. However, as the fractal flame
|
||||
algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will
|
||||
degenerate and produce a bad image.
|
||||
|
||||
Second, we're focused $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper
|
||||
Second, we're focused on $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper
|
||||
allows for arbitrary dimensions; you could also have 3-dimensional fractal flames.
|
||||
|
||||
Finally, there's a close relationship between fractal flames and [attractors](https://en.wikipedia.org/wiki/Attractor).
|
||||
Specifically, the fixed points of $S$ act as attractors for the chaos game (explained in detail below).
|
||||
Specifically, the fixed points of $S$ act as attractors for the chaos game (explained in more detail below).
|
||||
</details>
|
||||
|
||||
This is still a bit vague, so let's work through an example.
|
||||
@ -272,8 +271,8 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
|
||||
|
||||
<small>
|
||||
The image here is slightly different than in the paper.
|
||||
I think the paper has an error, so I'm choosing to plot the image
|
||||
the same way as the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
|
||||
I think the paper has an error, so I'm plotting the image
|
||||
like the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
|
||||
</small>
|
||||
|
||||
### Weights
|
||||
@ -292,7 +291,10 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
|
||||
|
||||
For Sierpinski's Gasket, we start with equal weighting,
|
||||
but changing the transform weights affects how often
|
||||
the chaos game "visits" parts of the image:
|
||||
the chaos game "visits" parts of the image. If the chaos
|
||||
were to run forever, we'd get the same image;
|
||||
but because the iteration count is limited,
|
||||
some parts may be missing below.
|
||||
|
||||
:::tip
|
||||
Double-click the image if you want to save a copy!
|
||||
|
@ -1,10 +1,11 @@
|
||||
// hidden-start
|
||||
import {camera} from "./cameraGasket"
|
||||
import { camera } from "./cameraGasket";
|
||||
|
||||
// hidden-end
|
||||
function imageIndex(
|
||||
width: number,
|
||||
x: number,
|
||||
y: number
|
||||
y: number,
|
||||
width: number
|
||||
) {
|
||||
return y * (width * 4) + x * 4;
|
||||
}
|
||||
@ -14,24 +15,26 @@ export function plot(
|
||||
y: number,
|
||||
img: ImageData
|
||||
) {
|
||||
let [pixelX, pixelY] = camera(img.width, x, y);
|
||||
let [pixelX, pixelY] =
|
||||
camera(x, y, img.width);
|
||||
|
||||
// Skip coordinates outside the display
|
||||
if (
|
||||
pixelX < 0 ||
|
||||
pixelX >= img.width ||
|
||||
pixelY < 0 ||
|
||||
pixelY >= img.height
|
||||
)
|
||||
return;
|
||||
|
||||
const i = imageIndex(
|
||||
img.width,
|
||||
pixelX,
|
||||
pixelY
|
||||
pixelY,
|
||||
img.width
|
||||
);
|
||||
|
||||
// Skip pixels outside the display range
|
||||
if (
|
||||
i < 0 ||
|
||||
i > img.data.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the pixel to black by writing 0
|
||||
// to the first three elements at the index
|
||||
// Set the pixel to black by setting
|
||||
// the first three elements to 0
|
||||
// (red, green, and blue, respectively),
|
||||
// and 255 to the last element (alpha)
|
||||
img.data[i] = 0;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import Gasket from "./Gasket";
|
||||
import { plot } from './plot';
|
||||
import { randomBiUnit } from '../src/randomBiUnit';
|
||||
import { randomInteger } from '../src/randomInteger';
|
||||
import { plot } from "./plot";
|
||||
import { randomBiUnit } from "../src/randomBiUnit";
|
||||
import { randomInteger } from "../src/randomInteger";
|
||||
|
||||
const Scope = {
|
||||
Gasket,
|
||||
plot,
|
||||
randomBiUnit,
|
||||
randomInteger,
|
||||
}
|
||||
randomInteger
|
||||
};
|
||||
export default Scope;
|
@ -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>
|
||||
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}
|
||||
<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}
|
||||
<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}
|
||||
<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}
|
||||
<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}
|
||||
<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}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
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 * 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";
|
||||
|
||||
@ -14,9 +14,9 @@ export default function FlameBlend() {
|
||||
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,14 +44,14 @@ 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]);
|
||||
|
||||
@ -64,5 +64,5 @@ export default function FlameBlend() {
|
||||
<VariationEditor title={"Transform 3"} variations={xform3Variations} setVariations={setXform3Variations}
|
||||
resetVariations={resetXform3Variations} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import {Coefs} from "../src/coefs"
|
||||
import { Coefs } from "../src/transform";
|
||||
import * as params from "../src/params";
|
||||
import {PainterContext} from "../src/Canvas"
|
||||
import { PainterContext } from "../src/Canvas";
|
||||
import { buildBlend } from "./buildBlend";
|
||||
import {chaosGameFinal} from "./chaosGameFinal"
|
||||
import { chaosGameFinal } from "./chaosGameFinal";
|
||||
import { VariationEditor, VariationProps } from "./VariationEditor";
|
||||
import { CoefEditor } from "./CoefEditor";
|
||||
import { applyPost, applyTransform } from "../src/applyTransform";
|
||||
@ -19,7 +19,7 @@ export default function FlameFinal() {
|
||||
julia: 1,
|
||||
popcorn: 0,
|
||||
pdj: 0
|
||||
}
|
||||
};
|
||||
const [xformFinalVariations, setXformFinalVariations] = useState<VariationProps>(xformFinalVariationsDefault);
|
||||
const resetXformFinalVariations = () => setXformFinalVariations(xformFinalVariationsDefault);
|
||||
|
||||
@ -42,5 +42,5 @@ export default function FlameFinal() {
|
||||
<CoefEditor title={"Final Transform Post"} isPost={true} coefs={xformFinalCoefsPost}
|
||||
setCoefs={setXformFinalCoefsPost} resetCoefs={resetXformFinalCoefsPost} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import {Coefs} from "../src/coefs"
|
||||
import {Transform} from "../src/transform";
|
||||
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);
|
||||
@ -25,12 +25,12 @@ 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 (
|
||||
@ -42,5 +42,5 @@ export default function FlamePost() {
|
||||
<CoefEditor title={"Transform 3 Post"} isPost={true} coefs={xform3CoefsPost} setCoefs={setXform3CoefsPost}
|
||||
resetCoefs={resetXform3CoefsPost} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
@ -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;
|
||||
@ -15,31 +15,31 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const VariationEditor = ({ title, variations, setVariations, resetVariations }: Props) => {
|
||||
const resetButton = <button className={styles.inputReset} onClick={resetVariations}>Reset</button>
|
||||
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}
|
||||
<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}
|
||||
<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}
|
||||
<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}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
@ -1,17 +1,17 @@
|
||||
import {Coefs} from "../src/coefs";
|
||||
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 {VariationBlend} from "../src/blend";
|
||||
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)]
|
||||
]
|
||||
];
|
||||
}
|
@ -1,23 +1,37 @@
|
||||
// hidden-start
|
||||
import { randomBiUnit } from "../src/randomBiUnit";
|
||||
import { randomChoice } from "../src/randomChoice";
|
||||
import { plotBinary as plot } from "../src/plotBinary"
|
||||
import { plotBinary as plot } from "../src/plotBinary";
|
||||
import { Transform } from "../src/transform";
|
||||
import {Props as ChaosGameWeightedProps} from "../1-introduction/chaosGameWeighted";
|
||||
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;
|
||||
}
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
@ -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}}>
|
||||
<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,8 +71,16 @@ 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)})`;
|
||||
@ -81,35 +89,34 @@ const ColorEditor: React.FC<ColorEditorProps> = ({title, palette, transformColor
|
||||
|
||||
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}
|
||||
<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}
|
||||
<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) {
|
||||
export default function FlameColor({ children }: Props) {
|
||||
const { width, height, setPainter } = useContext(PainterContext);
|
||||
|
||||
const xform1ColorDefault: TransformColor = { color: params.xform1Color, colorSpeed: 0.5 };
|
||||
@ -137,7 +144,7 @@ export default function FlameColor({quality, children}: Props) {
|
||||
palette: params.palette,
|
||||
colors: [xform1Color, xform2Color, xform3Color],
|
||||
finalColor: xformFinalColor
|
||||
}
|
||||
};
|
||||
setPainter(chaosGameColor(gameParams));
|
||||
}, [xform1Color, xform2Color, xform3Color, xformFinalColor]);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import {xforms as transforms, xformFinal as final} from "../src/params";
|
||||
import { xformFinal as final, xforms as transforms } from "../src/params";
|
||||
import { PainterContext } from "../src/Canvas";
|
||||
import { chaosGameHistogram } from "./chaosGameHistogram";
|
||||
|
||||
@ -17,7 +17,7 @@ export default function FlameHistogram({paint, children}: Props) {
|
||||
transforms,
|
||||
final,
|
||||
paint
|
||||
}
|
||||
};
|
||||
setPainter(chaosGameHistogram(gameParams));
|
||||
}, [width, height]);
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
@ -7,39 +7,72 @@ 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);
|
||||
}
|
@ -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
|
||||
];
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
@ -1,16 +1,17 @@
|
||||
// hidden-start
|
||||
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;
|
||||
}
|
||||
|
@ -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)
|
||||
]
|
||||
}
|
||||
];
|
||||
};
|
@ -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];
|
@ -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 { 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[] = [];
|
||||
|
@ -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)
|
||||
]
|
||||
}
|
@ -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;
|
||||
|
@ -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))
|
||||
];
|
||||
}
|
@ -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];
|
||||
|
@ -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)
|
||||
]
|
||||
];
|
||||
}
|
@ -1 +1,4 @@
|
||||
export type Variation = (x: number, y: number) => [number, number];
|
||||
export type Variation = (
|
||||
x: number,
|
||||
y: number
|
||||
) => [number, number];
|
Loading…
Reference in New Issue
Block a user