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

View File

@ -1,49 +1,49 @@
import {useEffect, useState, useContext, useRef} from "react"; import { useContext, useEffect, useState } from "react";
import {PainterContext} from "../src/Canvas"; import { PainterContext } from "../src/Canvas";
import {chaosGameWeighted} from "./chaosGameWeighted"; 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]; type Transform = (x: number, y: number) => [number, number];
export default function GasketWeighted() { export default function GasketWeighted() {
const [f0Weight, setF0Weight] = useState<number>(1); const [f0Weight, setF0Weight] = useState<number>(1);
const [f1Weight, setF1Weight] = useState<number>(1); const [f1Weight, setF1Weight] = useState<number>(1);
const [f2Weight, setF2Weight] = useState<number>(1); const [f2Weight, setF2Weight] = useState<number>(1);
const f0: Transform = (x, y) => [x / 2, y / 2]; const f0: Transform = (x, y) => [x / 2, y / 2];
const f1: Transform = (x, y) => [(x + 1) / 2, y / 2]; const f1: Transform = (x, y) => [(x + 1) / 2, y / 2];
const f2: Transform = (x, y) => [x / 2, (y + 1) / 2]; const f2: Transform = (x, y) => [x / 2, (y + 1) / 2];
const {width, height, setPainter} = useContext(PainterContext); const { width, height, setPainter } = useContext(PainterContext);
useEffect(() => { useEffect(() => {
const transforms: [number, Transform][] = [ const transforms: [number, Transform][] = [
[f0Weight, f0], [f0Weight, f0],
[f1Weight, f1], [f1Weight, f1],
[f2Weight, f2] [f2Weight, f2]
]; ];
setPainter(chaosGameWeighted({width, height, transforms})); setPainter(chaosGameWeighted({ width, height, transforms }));
}, [f0Weight, f1Weight, f2Weight]); }, [f0Weight, f1Weight, f2Weight]);
const weightInput = (title, weight, setWeight) => ( const weightInput = (title, weight, setWeight) => (
<> <>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<p><TeX>{title}</TeX>: {weight}</p> <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))}/> onInput={e => setWeight(Number(e.currentTarget.value))} />
</div> </div>
</> </>
) );
return ( 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_0", f0Weight, setF0Weight)}
{weightInput("F_1", f1Weight, setF1Weight)} {weightInput("F_1", f1Weight, setF1Weight)}
{weightInput("F_2", f2Weight, setF2Weight)} {weightInput("F_2", f2Weight, setF2Weight)}
</div> </div>
</> </>
) );
} }

View File

@ -1,10 +1,10 @@
export function camera( export function camera(
size: number, x: number,
x: number, y: number,
y: number size: number
): [number, number] { ): [number, number] {
return [ return [
Math.floor(x * size), Math.floor(x * size),
Math.floor(y * size) Math.floor(y * size)
]; ];
} }

View File

@ -6,28 +6,29 @@ const xforms = [
(x, y) => [x / 2, y / 2], (x, y) => [x / 2, y / 2],
(x, y) => [(x + 1) / 2, y / 2], (x, y) => [(x + 1) / 2, y / 2],
(x, y) => [x / 2, (y + 1) / 2] (x, y) => [x / 2, (y + 1) / 2]
] ];
function* chaosGame({width, height}) { function* chaosGame({ width, height }) {
const step = 1000; let img =
let img = new ImageData(width, height); new ImageData(width, height);
let [x, y] = [ let [x, y] = [
randomBiUnit(), randomBiUnit(),
randomBiUnit() randomBiUnit()
]; ];
for (let c = 0; c < iterations; c++) { for (let i = 0; i < iterations; i++) {
const i = randomInteger(0, xforms.length); const index =
[x, y] = xforms[i](x, y); randomInteger(0, xforms.length);
[x, y] = xforms[index](x, y);
if (c > 20) if (i > 20)
plot(x, y, img); plot(x, y, img);
if (c % step === 0) if (i % 1000 === 0)
yield img; yield img;
} }
yield img; yield img;
} }
render(<Gasket f={chaosGame}/>) render(<Gasket f={chaosGame} />);

View File

@ -1,37 +1,41 @@
// hidden-start // hidden-start
import { randomBiUnit } from "../src/randomBiUnit"; import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice"; import { randomChoice } from "../src/randomChoice";
import { plot } from "./plot" import { plot } from "./plot";
import {Transform} from "../src/transform"; import { Transform } from "../src/transform";
const quality = 0.5; const quality = 0.5;
const step = 1000; const step = 1000;
// hidden-end // hidden-end
export type Props = { export type Props = {
width: number, width: number,
height: number, height: number,
transforms: [number, Transform][] transforms: [number, Transform][]
} }
export function* chaosGameWeighted( 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] = [ let [x, y] = [
randomBiUnit(), randomBiUnit(),
randomBiUnit() randomBiUnit()
]; ];
const iterations = width * height * quality; const pixels = width * height;
for (let c = 0; c < iterations; c++) { const iterations = quality * pixels;
for (let i = 0; i < iterations; i++) {
// highlight-start // highlight-start
const [_, xform] = randomChoice(transforms); const [_, xform] =
randomChoice(transforms);
// highlight-end // highlight-end
[x, y] = xform(x, y); [x, y] = xform(x, y);
if (c > 20) if (i > 20)
plot(x, y, img); plot(x, y, img);
if (c % step === 0) if (i % step === 0)
yield img; 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: 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" import transformSource from "!!raw-loader!../src/transform"
@ -102,8 +102,7 @@ c &= 0.5 \\
d &= 0 \\ d &= 0 \\
e &= 1 \\ e &= 1 \\
f &= 1.5 \\ 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) &= (1 \cdot x + 0.5, 1 \cdot y + 1.5)
F_{shift}(x, y) &= (x + 0.5, y + 1.5)
\end{align*} \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$. > of finite compositions $F_{i_1...i_p}$ of members of $F$.
:::note :::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: 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. ...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. 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 algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will
degenerate and produce a bad image. 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. 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). 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> </details>
This is still a bit vague, so let's work through an example. This is still a bit vague, so let's work through an example.
@ -272,8 +271,8 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
<small> <small>
The image here is slightly different than in the paper. 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 I think the paper has an error, so I'm plotting the image
the same way as the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441). like the [reference implementation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L440-L441).
</small> </small>
### Weights ### Weights
@ -292,7 +291,10 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
For Sierpinski's Gasket, we start with equal weighting, For Sierpinski's Gasket, we start with equal weighting,
but changing the transform weights affects how often 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 :::tip
Double-click the image if you want to save a copy! Double-click the image if you want to save a copy!

View File

@ -1,10 +1,11 @@
// hidden-start // hidden-start
import {camera} from "./cameraGasket" import { camera } from "./cameraGasket";
// hidden-end // hidden-end
function imageIndex( function imageIndex(
width: number,
x: number, x: number,
y: number y: number,
width: number
) { ) {
return y * (width * 4) + x * 4; return y * (width * 4) + x * 4;
} }
@ -14,24 +15,26 @@ export function plot(
y: number, y: number,
img: ImageData 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( const i = imageIndex(
img.width,
pixelX, pixelX,
pixelY pixelY,
img.width
); );
// Skip pixels outside the display range // Set the pixel to black by setting
if ( // the first three elements to 0
i < 0 ||
i > img.data.length
) {
return;
}
// Set the pixel to black by writing 0
// to the first three elements at the index
// (red, green, and blue, respectively), // (red, green, and blue, respectively),
// and 255 to the last element (alpha) // and 255 to the last element (alpha)
img.data[i] = 0; img.data[i] = 0;

View File

@ -1,12 +1,12 @@
import Gasket from "./Gasket"; import Gasket from "./Gasket";
import { plot } from './plot'; import { plot } from "./plot";
import { randomBiUnit } from '../src/randomBiUnit'; import { randomBiUnit } from "../src/randomBiUnit";
import { randomInteger } from '../src/randomInteger'; import { randomInteger } from "../src/randomInteger";
const Scope = { const Scope = {
Gasket, Gasket,
plot, plot,
randomBiUnit, randomBiUnit,
randomInteger, randomInteger
} };
export default Scope; export default Scope;

View File

@ -1,51 +1,52 @@
import TeX from "@matejmazur/react-katex"; import TeX from "@matejmazur/react-katex";
import {Coefs} from "../src/coefs"; import { Coefs } from "../src/transform";
import styles from "../src/css/styles.module.css"; import styles from "../src/css/styles.module.css";
export interface Props { export interface Props {
title: String; title: String;
isPost: boolean; isPost: boolean;
coefs: Coefs; coefs: Coefs;
setCoefs: (coefs: Coefs) => void; setCoefs: (coefs: Coefs) => void;
resetCoefs: () => void; resetCoefs: () => void;
} }
export const CoefEditor = ({title, isPost, coefs, setCoefs, resetCoefs}: Props) => {
const resetButton = <button className={styles.inputReset} onClick={resetCoefs}>Reset</button>
return ( export const CoefEditor = ({ title, isPost, coefs, setCoefs, resetCoefs }: Props) => {
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr'}}> const resetButton = <button className={styles.inputReset} onClick={resetCoefs}>Reset</button>;
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p>
<div className={styles.inputElement}> return (
<p>{isPost ? <TeX>\alpha</TeX> : 'a'}: {coefs.a}</p> <div className={styles.inputGroup} style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.a} <p className={styles.inputTitle} style={{ gridColumn: "1/-1" }}>{title} {resetButton}</p>
onInput={e => setCoefs({...coefs, a: Number(e.currentTarget.value)})}/> <div className={styles.inputElement}>
</div> <p>{isPost ? <TeX>\alpha</TeX> : "a"}: {coefs.a}</p>
<div className={styles.inputElement}> <input type={"range"} min={-2} max={2} step={0.01} value={coefs.a}
<p>{isPost ? <TeX>\beta</TeX> : 'b'}: {coefs.b}</p> onInput={e => setCoefs({ ...coefs, a: Number(e.currentTarget.value) })} />
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.b} </div>
onInput={e => setCoefs({...coefs, b: Number(e.currentTarget.value)})}/> <div className={styles.inputElement}>
</div> <p>{isPost ? <TeX>\beta</TeX> : "b"}: {coefs.b}</p>
<div className={styles.inputElement}> <input type={"range"} min={-2} max={2} step={0.01} value={coefs.b}
<p>{isPost ? <TeX>\gamma</TeX> : 'c'}: {coefs.c}</p> onInput={e => setCoefs({ ...coefs, b: Number(e.currentTarget.value) })} />
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.c} </div>
onInput={e => setCoefs({...coefs, c: Number(e.currentTarget.value)})}/> <div className={styles.inputElement}>
</div> <p>{isPost ? <TeX>\gamma</TeX> : "c"}: {coefs.c}</p>
<div className={styles.inputElement}> <input type={"range"} min={-2} max={2} step={0.01} value={coefs.c}
<p>{isPost ? <TeX>\delta</TeX> : 'd'}: {coefs.d}</p> onInput={e => setCoefs({ ...coefs, c: Number(e.currentTarget.value) })} />
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.d} </div>
onInput={e => setCoefs({...coefs, d: Number(e.currentTarget.value)})}/> <div className={styles.inputElement}>
</div> <p>{isPost ? <TeX>\delta</TeX> : "d"}: {coefs.d}</p>
<div className={styles.inputElement}> <input type={"range"} min={-2} max={2} step={0.01} value={coefs.d}
<p>{isPost ? <TeX>\epsilon</TeX> : 'e'}: {coefs.e}</p> onInput={e => setCoefs({ ...coefs, d: Number(e.currentTarget.value) })} />
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.e} </div>
onInput={e => setCoefs({...coefs, e: Number(e.currentTarget.value)})}/> <div className={styles.inputElement}>
</div> <p>{isPost ? <TeX>\epsilon</TeX> : "e"}: {coefs.e}</p>
<div className={styles.inputElement}> <input type={"range"} min={-2} max={2} step={0.01} value={coefs.e}
<p>{isPost ? <TeX>\zeta</TeX> : 'f'}: {coefs.f}</p> onInput={e => setCoefs({ ...coefs, e: Number(e.currentTarget.value) })} />
<input type={'range'} min={-2} max={2} step={0.01} value={coefs.f} </div>
onInput={e => setCoefs({...coefs, f: Number(e.currentTarget.value)})}/> <div className={styles.inputElement}>
</div> <p>{isPost ? <TeX>\zeta</TeX> : "f"}: {coefs.f}</p>
</div> <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 { useContext, useEffect, useState } from "react";
import {Transform} from "../src/transform"; import { Transform } from "../src/transform";
import * as params from "../src/params" import * as params from "../src/params";
import {PainterContext} from "../src/Canvas" import { PainterContext } from "../src/Canvas";
import {chaosGameFinal} from "./chaosGameFinal" import { chaosGameFinal } from "./chaosGameFinal";
import {VariationEditor, VariationProps} from "./VariationEditor" import { VariationEditor, VariationProps } from "./VariationEditor";
import {applyTransform} from "../src/applyTransform"; import { applyTransform } from "../src/applyTransform";
import {buildBlend} from "./buildBlend"; import { buildBlend } from "./buildBlend";
export default function FlameBlend() { export default function FlameBlend() {
const {width, height, setPainter} = useContext(PainterContext); const { width, height, setPainter } = useContext(PainterContext);
const xform1VariationsDefault: VariationProps = { const xform1VariationsDefault: VariationProps = {
linear: 0, linear: 0,
julia: 1, julia: 1,
popcorn: 0, popcorn: 0,
pdj: 0, pdj: 0
} };
const [xform1Variations, setXform1Variations] = useState(xform1VariationsDefault) const [xform1Variations, setXform1Variations] = useState(xform1VariationsDefault);
const resetXform1Variations = () => setXform1Variations(xform1VariationsDefault); const resetXform1Variations = () => setXform1Variations(xform1VariationsDefault);
const xform2VariationsDefault: VariationProps = { const xform2VariationsDefault: VariationProps = {
linear: 1, linear: 1,
julia: 0, julia: 0,
popcorn: 1, popcorn: 1,
pdj: 0 pdj: 0
} };
const [xform2Variations, setXform2Variations] = useState(xform2VariationsDefault) const [xform2Variations, setXform2Variations] = useState(xform2VariationsDefault);
const resetXform2Variations = () => setXform2Variations(xform2VariationsDefault); const resetXform2Variations = () => setXform2Variations(xform2VariationsDefault);
const xform3VariationsDefault: VariationProps = { const xform3VariationsDefault: VariationProps = {
linear: 0, linear: 0,
julia: 0, julia: 0,
popcorn: 0, popcorn: 0,
pdj: 1 pdj: 1
} };
const [xform3Variations, setXform3Variations] = useState(xform3VariationsDefault) const [xform3Variations, setXform3Variations] = useState(xform3VariationsDefault);
const resetXform3Variations = () => setXform3Variations(xform3VariationsDefault); const resetXform3Variations = () => setXform3Variations(xform3VariationsDefault);
const identityXform: Transform = (x, y) => [x, y]; const identityXform: Transform = (x, y) => [x, y];
useEffect(() => { useEffect(() => {
const transforms: [number, Transform][] = [ const transforms: [number, Transform][] = [
[params.xform1Weight, applyTransform(params.xform1Coefs, buildBlend(params.xform1Coefs, xform1Variations))], [params.xform1Weight, applyTransform(params.xform1Coefs, buildBlend(params.xform1Coefs, xform1Variations))],
[params.xform2Weight, applyTransform(params.xform2Coefs, buildBlend(params.xform2Coefs, xform2Variations))], [params.xform2Weight, applyTransform(params.xform2Coefs, buildBlend(params.xform2Coefs, xform2Variations))],
[params.xform3Weight, applyTransform(params.xform3Coefs, buildBlend(params.xform3Coefs, xform3Variations))] [params.xform3Weight, applyTransform(params.xform3Coefs, buildBlend(params.xform3Coefs, xform3Variations))]
] ];
const gameParams = { const gameParams = {
width, width,
height, height,
transforms, transforms,
final: identityXform final: identityXform
} };
setPainter(chaosGameFinal(gameParams)); setPainter(chaosGameFinal(gameParams));
}, [xform1Variations, xform2Variations, xform3Variations]); }, [xform1Variations, xform2Variations, xform3Variations]);
return ( return (
<> <>
<VariationEditor title={"Transform 1"} variations={xform1Variations} setVariations={setXform1Variations} <VariationEditor title={"Transform 1"} variations={xform1Variations} setVariations={setXform1Variations}
resetVariations={resetXform1Variations}/> resetVariations={resetXform1Variations} />
<VariationEditor title={"Transform 2"} variations={xform2Variations} setVariations={setXform2Variations} <VariationEditor title={"Transform 2"} variations={xform2Variations} setVariations={setXform2Variations}
resetVariations={resetXform2Variations}/> resetVariations={resetXform2Variations} />
<VariationEditor title={"Transform 3"} variations={xform3Variations} setVariations={setXform3Variations} <VariationEditor title={"Transform 3"} variations={xform3Variations} setVariations={setXform3Variations}
resetVariations={resetXform3Variations}/> resetVariations={resetXform3Variations} />
</> </>
) );
} }

View File

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

View File

@ -1,46 +1,46 @@
import {useContext, useEffect, useState} from "react"; import { useContext, useEffect, useState } from "react";
import {Coefs} from "../src/coefs" import { applyTransform } from "../src/applyTransform";
import {Transform} from "../src/transform"; import { Coefs, Transform } from "../src/transform";
import * as params from "../src/params"; import * as params from "../src/params";
import {PainterContext} from "../src/Canvas" import { PainterContext } from "../src/Canvas";
import {chaosGameFinal, Props as ChaosGameFinalProps} from "./chaosGameFinal" import { chaosGameFinal, Props as ChaosGameFinalProps } from "./chaosGameFinal";
import {CoefEditor} from "./CoefEditor" import { CoefEditor } from "./CoefEditor";
import {applyPost, applyTransform} from "@site/blog/2024-11-15-playing-with-fire/src/applyTransform"; import { transformPost } from "./post";
export default function FlamePost() { export default function FlamePost() {
const {width, height, setPainter} = useContext(PainterContext); const { width, height, setPainter } = useContext(PainterContext);
const [xform1CoefsPost, setXform1CoefsPost] = useState<Coefs>(params.xform1CoefsPost); const [xform1CoefsPost, setXform1CoefsPost] = useState<Coefs>(params.xform1CoefsPost);
const resetXform1CoefsPost = () => setXform1CoefsPost(params.xform1CoefsPost); const resetXform1CoefsPost = () => setXform1CoefsPost(params.xform1CoefsPost);
const [xform2CoefsPost, setXform2CoefsPost] = useState<Coefs>(params.xform2CoefsPost); const [xform2CoefsPost, setXform2CoefsPost] = useState<Coefs>(params.xform2CoefsPost);
const resetXform2CoefsPost = () => setXform2CoefsPost(params.xform2CoefsPost); const resetXform2CoefsPost = () => setXform2CoefsPost(params.xform2CoefsPost);
const [xform3CoefsPost, setXform3CoefsPost] = useState<Coefs>(params.xform3CoefsPost); const [xform3CoefsPost, setXform3CoefsPost] = useState<Coefs>(params.xform3CoefsPost);
const resetXform3CoefsPost = () => setXform3CoefsPost(params.xform3CoefsPost); const resetXform3CoefsPost = () => setXform3CoefsPost(params.xform3CoefsPost);
const identityXform: Transform = (x, y) => [x, y]; const identityXform: Transform = (x, y) => [x, y];
const gameParams: ChaosGameFinalProps = { const gameParams: ChaosGameFinalProps = {
width, width,
height, height,
transforms: [ transforms: [
[params.xform1Weight, applyPost(xform1CoefsPost, applyTransform(params.xform1Coefs, params.xform1Variations))], [params.xform1Weight, transformPost(applyTransform(params.xform1Coefs, params.xform1Variations), xform1CoefsPost)],
[params.xform2Weight, applyPost(xform2CoefsPost, applyTransform(params.xform2Coefs, params.xform2Variations))], [params.xform2Weight, transformPost(applyTransform(params.xform2Coefs, params.xform2Variations), xform2CoefsPost)],
[params.xform3Weight, applyPost(xform3CoefsPost, applyTransform(params.xform3Coefs, params.xform3Variations))], [params.xform3Weight, transformPost(applyTransform(params.xform3Coefs, params.xform3Variations), xform3CoefsPost)]
], ],
final: identityXform final: identityXform
} };
useEffect(() => setPainter(chaosGameFinal(gameParams)), [xform1CoefsPost, xform2CoefsPost, xform3CoefsPost]); useEffect(() => setPainter(chaosGameFinal(gameParams)), [xform1CoefsPost, xform2CoefsPost, xform3CoefsPost]);
return ( return (
<> <>
<CoefEditor title={"Transform 1 Post"} isPost={true} coefs={xform1CoefsPost} setCoefs={setXform1CoefsPost} <CoefEditor title={"Transform 1 Post"} isPost={true} coefs={xform1CoefsPost} setCoefs={setXform1CoefsPost}
resetCoefs={resetXform1CoefsPost}/> resetCoefs={resetXform1CoefsPost} />
<CoefEditor title={"Transform 2 Post"} isPost={true} coefs={xform2CoefsPost} setCoefs={setXform2CoefsPost} <CoefEditor title={"Transform 2 Post"} isPost={true} coefs={xform2CoefsPost} setCoefs={setXform2CoefsPost}
resetCoefs={resetXform2CoefsPost}/> resetCoefs={resetXform2CoefsPost} />
<CoefEditor title={"Transform 3 Post"} isPost={true} coefs={xform3CoefsPost} setCoefs={setXform3CoefsPost} <CoefEditor title={"Transform 3 Post"} isPost={true} coefs={xform3CoefsPost} setCoefs={setXform3CoefsPost}
resetCoefs={resetXform3CoefsPost}/> 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 { export interface VariationProps {
linear: number; linear: number;
julia: number; julia: number;
popcorn: number; popcorn: number;
pdj: number; pdj: number;
} }
export interface Props { export interface Props {
title: String; title: String;
variations: VariationProps; variations: VariationProps;
setVariations: (variations: VariationProps) => void; setVariations: (variations: VariationProps) => void;
resetVariations: () => void; resetVariations: () => void;
} }
export const VariationEditor = ({title, variations, setVariations, resetVariations}: 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 ( return (
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr'}}> <div className={styles.inputGroup} style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p> <p className={styles.inputTitle} style={{ gridColumn: "1/-1" }}>{title} {resetButton}</p>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<span>Linear: {variations.linear}</span> <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)})}/> onInput={e => setVariations({ ...variations, linear: Number(e.currentTarget.value) })} />
</div> </div>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<span>Julia: {variations.julia}</span> <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)})}/> onInput={e => setVariations({ ...variations, julia: Number(e.currentTarget.value) })} />
</div> </div>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<span>Popcorn: {variations.popcorn}</span> <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)})}/> onInput={e => setVariations({ ...variations, popcorn: Number(e.currentTarget.value) })} />
</div> </div>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<span>PDJ: {variations.pdj}</span> <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)})}/> onInput={e => setVariations({ ...variations, pdj: Number(e.currentTarget.value) })} />
</div> </div>
</div> </div>
) );
} };

View File

@ -1,17 +1,17 @@
import {Coefs} from "../src/coefs"; import { Coefs } from "../src/transform";
import {VariationProps} from "./VariationEditor"; import { VariationProps } from "./VariationEditor";
import {linear} from "../src/linear"; import { linear } from "../src/linear";
import {julia} from "../src/julia"; import { julia } from "../src/julia";
import {popcorn} from "../src/popcorn"; import { popcorn } from "../src/popcorn";
import {pdj} from "../src/pdj"; import { pdj } from "../src/pdj";
import {pdjParams} from "../src/params"; 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 [ return [
[variations.linear, linear], [variations.linear, linear],
[variations.julia, julia], [variations.julia, julia],
[variations.popcorn, popcorn(coefs)], [variations.popcorn, popcorn(coefs)],
[variations.pdj, pdj(pdjParams)] [variations.pdj, pdj(pdjParams)]
] ];
} }

View File

@ -1,37 +1,51 @@
// hidden-start // hidden-start
import { randomBiUnit } from "../src/randomBiUnit"; import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice"; 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 { 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 quality = 0.5;
const step = 1000; const step = 1000;
// hidden-end // hidden-end
export type Props = ChaosGameWeightedProps & { export type Props = WeightedProps & {
final: Transform, 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(
for (let i = 0; i < iterations; i++) { {
const [_, transform] = randomChoice(transforms); width,
[x, y] = transform(x, y); height,
transforms,
final
}: Props
) {
let img =
new ImageData(width, height);
let [x, y] = [
randomBiUnit(),
randomBiUnit()
];
// highlight-start const pixels = width * height;
const [finalX, finalY] = final(x, y); const iterations = quality * pixels;
// highlight-end for (let i = 0; i < iterations; i++) {
const [_, transform] =
randomChoice(transforms);
[x, y] = transform(x, y);
if (i > 20) // highlight-start
// highlight-start const [finalX, finalY] = final(x, y);
plot(finalX, finalY, image); // highlight-end
// highlight-end
if (i % step === 0) if (i > 20)
yield image; // 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": 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' 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$: 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' 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: 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} \\ 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 \cdot y) - \text{cos}(p_2 \cdot x), \text{sin}(p_3 \cdot x) - \text{cos}(p_4 \cdot y)) 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' 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: 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: The formula looks intimidating, but it's not hard to implement:

View File

@ -1,7 +1,11 @@
// hidden-start // hidden-start
import {Coefs} from "../src/coefs"; import { applyCoefs, Coefs, Transform } from "../src/transform";
import {Transform} from "../src/transform";
import {applyCoefs} from "../src/coefs";
// hidden-end // hidden-end
export const transformPost = (transform: Transform, coefs: Coefs): Transform => export const transformPost = (
(x, y) => applyCoefs(...transform(x, y), coefs) 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 * as params from "../src/params";
import {PainterContext} from "../src/Canvas"; import { PainterContext } from "../src/Canvas";
import {colorFromPalette} from "./colorFromPalette"; import { colorFromPalette } from "./colorFromPalette";
import {chaosGameColor, Props as ChaosGameColorProps, TransformColor} from "./chaosGameColor"; import { chaosGameColor, Props as ChaosGameColorProps, TransformColor } from "./chaosGameColor";
import styles from "../src/css/styles.module.css"; import styles from "../src/css/styles.module.css";
import {histIndex} from "../src/camera"; import { histIndex } from "../src/camera";
import {useColorMode} from "@docusaurus/theme-common"; import { useColorMode } from "@docusaurus/theme-common";
type PaletteBarProps = { type PaletteBarProps = {
height: number; height: number;
palette: number[]; palette: number[];
children?: React.ReactNode; children?: React.ReactNode;
} }
export const PaletteBar: React.FC<PaletteBarProps> = ({height, palette, children}) => { export const PaletteBar: React.FC<PaletteBarProps> = ({ height, palette, children }) => {
const sizingRef = useRef<HTMLDivElement>(null); const sizingRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
useEffect(() => { useEffect(() => {
if (sizingRef) { if (sizingRef) {
setWidth(sizingRef.current.offsetWidth); setWidth(sizingRef.current.offsetWidth);
} }
}, [sizingRef]); }, [sizingRef]);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const paletteImage = useMemo(() => { const paletteImage = useMemo(() => {
if (width === 0) { if (width === 0) {
return; return;
} }
const image = new ImageData(width, height); const image = new ImageData(width, height);
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const colorIndex = x / width; const colorIndex = x / width;
const [r, g, b] = colorFromPalette(palette, colorIndex); const [r, g, b] = colorFromPalette(palette, colorIndex);
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
const pixelIndex = histIndex(x, y, width, 4); const pixelIndex = histIndex(x, y, width, 4);
image.data[pixelIndex] = r * 0xff; image.data[pixelIndex] = r * 0xff;
image.data[pixelIndex + 1] = g * 0xff; image.data[pixelIndex + 1] = g * 0xff;
image.data[pixelIndex + 2] = b * 0xff; image.data[pixelIndex + 2] = b * 0xff;
image.data[pixelIndex + 3] = 0xff; image.data[pixelIndex + 3] = 0xff;
} }
} }
return image; return image;
}, [width, height, palette]); }, [width, height, palette]);
useEffect(() => { useEffect(() => {
if (canvasRef && paletteImage) { if (canvasRef && paletteImage) {
canvasRef.current.getContext("2d").putImageData(paletteImage, 0, 0); canvasRef.current.getContext("2d").putImageData(paletteImage, 0, 0);
} }
}, [canvasRef, paletteImage]); }, [canvasRef, paletteImage]);
const canvasStyle = {filter: useColorMode().colorMode === 'dark' ? 'invert(1)' : ''}; const canvasStyle = { filter: useColorMode().colorMode === "dark" ? "invert(1)" : "" };
return ( 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} {width > 0 ? <canvas ref={canvasRef} width={width} height={height} style={canvasStyle} /> : null}
</div> </div>
{children} {children}
</> </>
) );
} };
type ColorEditorProps = { type ColorEditorProps = {
title: string; title: string;
palette: number[]; palette: number[];
transformColor: TransformColor; transformColor: TransformColor;
setTransformColor: (transformColor: TransformColor) => void; setTransformColor: (transformColor: TransformColor) => void;
resetTransformColor: () => void; resetTransformColor: () => void;
children?: React.ReactNode; children?: React.ReactNode;
} }
const ColorEditor: React.FC<ColorEditorProps> = ({title, palette, transformColor, setTransformColor, resetTransformColor, children}) => { const ColorEditor: React.FC<ColorEditorProps> = (
const resetButton = <button className={styles.inputReset} onClick={resetTransformColor}>Reset</button> {
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 [r, g, b] = colorFromPalette(palette, transformColor.color);
const colorCss = `rgb(${Math.floor(r * 0xff)},${Math.floor(g * 0xff)},${Math.floor(b * 0xff)})`; const colorCss = `rgb(${Math.floor(r * 0xff)},${Math.floor(g * 0xff)},${Math.floor(b * 0xff)})`;
const {colorMode} = useColorMode(); const { colorMode } = useColorMode();
return ( return (
<> <>
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '2fr 2fr 1fr'}}> <div className={styles.inputGroup} style={{ display: "grid", gridTemplateColumns: "2fr 2fr 1fr" }}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title} {resetButton}</p> <p className={styles.inputTitle} style={{ gridColumn: "1/-1" }}>{title} {resetButton}</p>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<p>Color: {transformColor.color}</p> <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)})}/> onInput={e => setTransformColor({ ...transformColor, color: Number(e.currentTarget.value) })} />
</div> </div>
<div className={styles.inputElement}> <div className={styles.inputElement}>
<p>Speed: {transformColor.colorSpeed}</p> <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)})}/> onInput={e => setTransformColor({ ...transformColor, colorSpeed: Number(e.currentTarget.value) })} />
</div> </div>
<div className={styles.inputElement} style={{ <div className={styles.inputElement} style={{
width: '100%', width: "100%",
height: '100%', height: "100%",
backgroundColor: colorCss, backgroundColor: colorCss,
filter: colorMode === 'dark' ? 'invert(1)' : '' filter: colorMode === "dark" ? "invert(1)" : ""
}}/> }} />
</div> </div>
{children} {children}
</> </>
) );
} };
type Props = { type Props = {
quality?: number; children?: React.ReactElement;
children?: React.ReactElement;
} }
export default function FlameColor({quality, children}: Props) { export default function FlameColor({ children }: Props) {
const {width, height, setPainter} = useContext(PainterContext); const { width, height, setPainter } = useContext(PainterContext);
const xform1ColorDefault: TransformColor = {color: params.xform1Color, colorSpeed: 0.5}; const xform1ColorDefault: TransformColor = { color: params.xform1Color, colorSpeed: 0.5 };
const [xform1Color, setXform1Color] = useState(xform1ColorDefault); const [xform1Color, setXform1Color] = useState(xform1ColorDefault);
const resetXform1Color = () => setXform1Color(xform1ColorDefault); const resetXform1Color = () => setXform1Color(xform1ColorDefault);
const xform2ColorDefault: TransformColor = {color: params.xform2Color, colorSpeed: 0.5}; const xform2ColorDefault: TransformColor = { color: params.xform2Color, colorSpeed: 0.5 };
const [xform2Color, setXform2Color] = useState(xform2ColorDefault); const [xform2Color, setXform2Color] = useState(xform2ColorDefault);
const resetXform2Color = () => setXform2Color(xform2ColorDefault); const resetXform2Color = () => setXform2Color(xform2ColorDefault);
const xform3ColorDefault: TransformColor = {color: params.xform3Color, colorSpeed: 0.5}; const xform3ColorDefault: TransformColor = { color: params.xform3Color, colorSpeed: 0.5 };
const [xform3Color, setXform3Color] = useState(xform3ColorDefault); const [xform3Color, setXform3Color] = useState(xform3ColorDefault);
const resetXform3Color = () => setXform3Color(xform3ColorDefault); const resetXform3Color = () => setXform3Color(xform3ColorDefault);
const xformFinalColorDefault: TransformColor = {color: params.xformFinalColor, colorSpeed: 0}; const xformFinalColorDefault: TransformColor = { color: params.xformFinalColor, colorSpeed: 0 };
const [xformFinalColor, setXformFinalColor] = useState(xformFinalColorDefault); const [xformFinalColor, setXformFinalColor] = useState(xformFinalColorDefault);
const resetXformFinalColor = () => setXformFinalColor(xformFinalColorDefault); const resetXformFinalColor = () => setXformFinalColor(xformFinalColorDefault);
useEffect(() => { useEffect(() => {
const gameParams: ChaosGameColorProps = { const gameParams: ChaosGameColorProps = {
width, width,
height, height,
transforms: params.xforms, transforms: params.xforms,
final: params.xformFinal, final: params.xformFinal,
palette: params.palette, palette: params.palette,
colors: [xform1Color, xform2Color, xform3Color], colors: [xform1Color, xform2Color, xform3Color],
finalColor: xformFinalColor finalColor: xformFinalColor
} };
setPainter(chaosGameColor(gameParams)); setPainter(chaosGameColor(gameParams));
}, [xform1Color, xform2Color, xform3Color, xformFinalColor]); }, [xform1Color, xform2Color, xform3Color, xformFinalColor]);
return ( return (
<> <>
<PaletteBar height={40} palette={params.palette}/> <PaletteBar height={40} palette={params.palette} />
<ColorEditor <ColorEditor
title={"Transform 1"} title={"Transform 1"}
palette={params.palette} palette={params.palette}
transformColor={xform1Color} transformColor={xform1Color}
setTransformColor={setXform1Color} setTransformColor={setXform1Color}
resetTransformColor={resetXform1Color}/> resetTransformColor={resetXform1Color} />
<ColorEditor <ColorEditor
title={"Transform 2"} title={"Transform 2"}
palette={params.palette} palette={params.palette}
transformColor={xform2Color} transformColor={xform2Color}
setTransformColor={setXform2Color} setTransformColor={setXform2Color}
resetTransformColor={resetXform2Color}/> resetTransformColor={resetXform2Color} />
<ColorEditor <ColorEditor
title={"Transform 3"} title={"Transform 3"}
palette={params.palette} palette={params.palette}
transformColor={xform3Color} transformColor={xform3Color}
setTransformColor={setXform3Color} setTransformColor={setXform3Color}
resetTransformColor={resetXform3Color}/> resetTransformColor={resetXform3Color} />
<ColorEditor <ColorEditor
title={"Transform Final"} title={"Transform Final"}
palette={params.palette} palette={params.palette}
transformColor={xformFinalColor} transformColor={xformFinalColor}
setTransformColor={setXformFinalColor} setTransformColor={setXformFinalColor}
resetTransformColor={resetXformFinalColor}/> resetTransformColor={resetXformFinalColor} />
{children} {children}
</> </>
); );
} }

View File

@ -1,25 +1,25 @@
import React, {useContext, useEffect} from "react"; 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 { PainterContext } from "../src/Canvas";
import {chaosGameHistogram} from "./chaosGameHistogram"; import { chaosGameHistogram } from "./chaosGameHistogram";
type Props = { type Props = {
paint: (width: number, height: number, histogram: number[]) => ImageData; paint: (width: number, height: number, histogram: number[]) => ImageData;
children?: React.ReactElement; children?: React.ReactElement;
} }
export default function FlameHistogram({paint, children}: Props) { export default function FlameHistogram({ paint, children }: Props) {
const {width, height, setPainter} = useContext(PainterContext); const { width, height, setPainter } = useContext(PainterContext);
useEffect(() => { useEffect(() => {
const gameParams = { const gameParams = {
width, width,
height, height,
transforms, transforms,
final, final,
paint paint
} };
setPainter(chaosGameHistogram(gameParams)); setPainter(chaosGameHistogram(gameParams));
}, [width, height]); }, [width, height]);
return children; return children;
} }

View File

@ -1,64 +1,135 @@
// hidden-start // hidden-start
import {Props as ChaosGameFinalProps} from "../2-transforms/chaosGameFinal"; import { Props as ChaosGameFinalProps } from "../2-transforms/chaosGameFinal";
import {randomBiUnit} from "../src/randomBiUnit"; import { randomBiUnit } from "../src/randomBiUnit";
import {randomChoice} from "../src/randomChoice"; import { randomChoice } from "../src/randomChoice";
import {camera, histIndex} from "../src/camera"; import { camera, histIndex } from "../src/camera";
import {colorFromPalette} from "./colorFromPalette"; import { colorFromPalette } from "./colorFromPalette";
import {mixColor} from "./mixColor"; import { mixColor } from "./mixColor";
import {paintColor} from "./paintColor"; import { paintColor } from "./paintColor";
const quality = 15; const quality = 15;
const step = 100_000; const step = 100_000;
// hidden-end // hidden-end
export type TransformColor = { export type TransformColor = {
color: number; color: number;
colorSpeed: number; colorSpeed: number;
} }
export type Props = ChaosGameFinalProps & { export type Props = ChaosGameFinalProps & {
palette: number[]; palette: number[];
colors: TransformColor[]; colors: TransformColor[];
finalColor: 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(
let c = Math.random(); {
width,
height,
transforms,
final,
palette,
colors,
finalColor
}: Props
) {
const pixels = width * height;
const iterations = width * height * quality; // highlight-start
for (let i = 0; i < iterations; i++) { const imgRed = Array<number>(pixels)
const [transformIndex, transform] = randomChoice(transforms); .fill(0);
[x, y] = transform(x, y); const imgGreen = Array<number>(pixels)
.fill(0);
const imgBlue = Array<number>(pixels)
.fill(0);
const imgAlpha = Array<number>(pixels)
.fill(0);
// highlight-start const plotColor = (
const transformColor = colors[transformIndex]; x: number,
c = mixColor(c, transformColor.color, transformColor.colorSpeed); y: number,
// highlight-end 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 hIndex =
const [pixelX, pixelY] = camera(finalX, finalY, width); histIndex(pixelX, pixelY, width, 1);
const pixelIndex = histIndex(pixelX, pixelY, width, 1);
if (pixelIndex < 0 || pixelIndex >= imgAlpha.length) const [r, g, b] =
continue; colorFromPalette(palette, c);
const colorFinal = mixColor(c, finalColor.color, finalColor.colorSpeed); imgRed[hIndex] += r;
const [r, g, b] = colorFromPalette(palette, colorFinal); imgGreen[hIndex] += g;
imgRed[pixelIndex] += r; imgBlue[hIndex] += b;
imgGreen[pixelIndex] += g; imgAlpha[hIndex] += 1;
imgBlue[pixelIndex] += b; }
imgAlpha[pixelIndex] += 1; // highlight-end
}
if (i % step === 0) let [x, y] = [
yield paintColor(width, height, imgRed, imgGreen, imgBlue, imgAlpha); 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 // hidden-start
import {randomBiUnit} from "../src/randomBiUnit"; import { randomBiUnit } from "../src/randomBiUnit";
import {randomChoice} from "../src/randomChoice"; import { randomChoice } from "../src/randomChoice";
import {Props as ChaosGameFinalProps} from "../2-transforms/chaosGameFinal"; import { Props as ChaosGameFinalProps } from "../2-transforms/chaosGameFinal";
import {camera, histIndex} from "../src/camera"; import { camera, histIndex } from "../src/camera";
const quality = 10; const quality = 10;
const step = 100_000; const step = 100_000;
// hidden-end // hidden-end
export type Props = ChaosGameFinalProps & { type Props = ChaosGameFinalProps & {
paint: (width: number, height: number, histogram: number[]) => ImageData; paint: (
width: number,
height: number,
histogram: number[]
) => ImageData;
} }
export function* chaosGameHistogram({width, height, transforms, final, paint}: Props) {
let iterations = quality * width * height;
// highlight-start export function* chaosGameHistogram(
const histogram = Array<number>(width * height).fill(0); {
// highlight-end 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 plotHist = (
const [_, transform] = randomChoice(transforms); x: number,
[x, y] = transform(x, y); y: number
const [finalX, finalY] = final(x, y); ) => {
const [pixelX, pixelY] =
camera(x, y, width);
if (i > 20) { if (
// highlight-start pixelX < 0 ||
const [pixelX, pixelY] = camera(finalX, finalY, width); pixelX >= width ||
const hIndex = histIndex(pixelX, pixelY, width, 1); pixelY < 0 ||
pixelY >= height
)
return;
if (hIndex < 0 || hIndex >= histogram.length) { const hIndex =
continue; histIndex(pixelX, pixelY, width, 1);
}
histogram[hIndex] += 1; hist[hIndex] += 1;
// highlight-end };
} // highlight-end
if (i % step === 0) let [x, y] = [
yield paint(width, height, histogram); 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] { export function colorFromPalette(
const paletteIndex = Math.floor(colorIndex * (palette.length / 3)) * 3; palette: number[],
return [palette[paletteIndex], palette[paletteIndex + 1], palette[paletteIndex + 2]]; 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> <details>
<summary>As an alternative...</summary> <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) For example, `flam3` uses [linear interpolation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486)
</details> </details>

View File

@ -1,3 +1,8 @@
export function mixColor(color1: number, color2: number, colorSpeed: number) { export function mixColor(
return color1 * (1 - colorSpeed) + color2 * colorSpeed; 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( export function paintColor(
width: number, width: number,
height: number, height: number,
red: number[], red: number[],
green: number[], green: number[],
blue: number[], blue: number[],
alpha: number[] alpha: number[]
): ImageData { ): 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++) { for (let i = 0; i < pixels; i++) {
const alphaScale = Math.log10(alpha[i]) / (alpha[i] * 1.5); const scale =
Math.log10(alpha[i]) /
(alpha[i] * 1.5);
const pixelIndex = i * 4; 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;
}
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 { export function paintLinear(
const image = new ImageData(width, height); width: number,
height: number,
hist: number[]
) {
const img =
new ImageData(width, height);
let valueMax = 0; let hMax = 0;
for (let value of histogram) { for (let value of hist) {
valueMax = Math.max(valueMax, value); 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; 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;
}
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 { export function paintLogarithmic(
const image = new ImageData(width, height); width: number,
height: number,
hist: number[]
) {
const img =
new ImageData(width, height);
const histogramLog: number[] = []; const histLog = hist.map(Math.log);
histogram.forEach(value => histogramLog.push(Math.log(value)));
let histogramLogMax = -Infinity; let hLogMax = -Infinity;
for (let value of histogramLog) { for (let value of histLog) {
histogramLogMax = Math.max(histogramLogMax, value); 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; 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;
}
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"> <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" > <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" post="1 0 0 1 2 0"/>
<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" 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.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"> <palette count="256" format="RGB">
3130323635383B3A3D403F424644484B494D504E52565358 3130323635383B3A3D403F424644484B494D504E52565358
5B585D605D626562686B676D706C737571787B767D807B83 5B585D605D626562686B676D706C737571787B767D807B83
@ -39,9 +39,9 @@
</palette> </palette>
</flame> </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" > <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.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.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"> <palette count="256" format="RGB">
3130323635383B3A3D403F424644484B494D504E52565358 3130323635383B3A3D403F424644484B494D504E52565358
5B585D605D626562686B676D706C737571787B767D807B83 5B585D605D626562686B676D706C737571787B767D807B83
@ -78,9 +78,9 @@
</palette> </palette>
</flame> </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" > <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.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.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" /> <finalxform color="0" symmetry="1" julia="1" coefs="2 0 0 2 0 0" />
<palette count="256" format="RGB"> <palette count="256" format="RGB">
3130323635383B3A3D403F424644484B494D504E52565358 3130323635383B3A3D403F424644484B494D504E52565358

View File

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

View File

@ -1,8 +1,8 @@
import {Transform, Coefs, applyCoefs} from "./transform"; import { applyCoefs, Coefs, Transform } from "./transform";
import {blend, VariationBlend} from "./blend"; import { blend, Blend } from "./blend";
export const applyTransform = (coefs: Coefs, variations: VariationBlend): Transform => export const applyTransform = (coefs: Coefs, variations: Blend): Transform =>
(x, y) => blend(...applyCoefs(x, y, coefs), variations); (x, y) => blend(...applyCoefs(x, y, coefs), variations)
export const applyPost = (coefsPost: Coefs, transform: Transform): Transform => 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 // hidden-start
import {Variation} from "./variation"; import { Variation } from "./variation";
// hidden-end // hidden-end
export type VariationBlend = [number, Variation][]; export type Blend = [number, Variation][];
export function blend( export function blend(
x: number, x: number,
y: number, y: number,
variations: VariationBlend varFns: Blend
): [number, number] { ): [number, number] {
let [outX, outY] = [0, 0]; let [outX, outY] = [0, 0];
for (const [weight, variation] of variations) { for (const [weight, varFn] of varFns) {
const [varX, varY] = variation(x, y); const [varX, varY] = varFn(x, y);
outX += weight * varX; outX += weight * varX;
outY += weight * varY; outY += weight * varY;
} }
return [outX, outY]; return [outX, outY];
} }

View File

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

View File

@ -1,4 +1,5 @@
// hidden-start // hidden-start
import {Variation} from "./variation" import {Variation} from "./variation"
//hidden-end //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. * translated into something that's easier to work with.
*/ */
import { Coefs } from './coefs'; import { Blend } from "./blend";
import {VariationBlend} from "./blend"; import { linear } from "./linear";
import { linear } from './linear' import { julia } from "./julia";
import { julia } from './julia' import { popcorn } from "./popcorn";
import { popcorn } from './popcorn' import { pdj, PdjParams } from "./pdj";
import {pdj, PdjParams} from './pdj' import { Coefs, Transform } from "./transform";
import {Transform} from "./transform"; import { applyPost, applyTransform } from "./applyTransform";
import {applyPost, applyTransform} from "./applyTransform";
export const identityCoefs: Coefs = { export const identityCoefs: Coefs = {
a: 1, b: 0, c: 0, a: 1, b: 0, c: 0,
d: 0, e: 1, f: 0, d: 0, e: 1, f: 0
} };
export const pdjParams: PdjParams = { 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 xform1Weight = 0.56453495;
export const xform1Coefs = { export const xform1Coefs = {
a: -1.381068, b: -1.381068, c: 0, 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 xform1CoefsPost = identityCoefs;
export const xform1Variations: VariationBlend = [ export const xform1Variations: Blend = [
[1, julia] [1, julia]
] ];
export const xform1Color = 0; export const xform1Color = 0;
export const xform2Weight = 0.013135; export const xform2Weight = 0.013135;
export const xform2Coefs = { export const xform2Coefs = {
a: 0.031393, b: 0.031367, c: 0, 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 = { export const xform2CoefsPost = {
a: 1, b: 0, c: 0.24, a: 1, b: 0, c: 0.24,
d: 0, e: 1, f: 0.27, d: 0, e: 1, f: 0.27
} };
export const xform2Variations: VariationBlend = [ export const xform2Variations: Blend = [
[1, linear], [1, linear],
[1, popcorn(xform2Coefs)] [1, popcorn(xform2Coefs)]
] ];
export const xform2Color = 0.844; export const xform2Color = 0.844;
export const xform3Weight = 0.42233; export const xform3Weight = 0.42233;
export const xform3Coefs = { export const xform3Coefs = {
a: 1.51523, b: -3.048677, c: 0.724135, 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 xform3CoefsPost = identityCoefs;
export const xform3Variations: VariationBlend = [ export const xform3Variations: Blend = [
[1, pdj(pdjParams)] [1, pdj(pdjParams)]
]; ];
export const xform3Color = 0.349; export const xform3Color = 0.349;
export const xformFinalCoefs = { export const xformFinalCoefs = {
a: 2, b: 0, c: 0, a: 2, b: 0, c: 0,
d: 0, e: 2, f: 0 d: 0, e: 2, f: 0
} };
export const xformFinalCoefsPost = identityCoefs; export const xformFinalCoefsPost = identityCoefs;
export const xformFinalVariations: VariationBlend = [ export const xformFinalVariations: Blend = [
[1, julia] [1, julia]
] ];
export const xformFinalColor = 0; export const xformFinalColor = 0;
export const xforms: [number, Transform][] = [ export const xforms: [number, Transform][] = [
[xform1Weight, applyPost(xform1CoefsPost, applyTransform(xform1Coefs, xform1Variations))], [xform1Weight, applyPost(xform1CoefsPost, applyTransform(xform1Coefs, xform1Variations))],
[xform2Weight, applyPost(xform2CoefsPost, applyTransform(xform2Coefs, xform2Variations))], [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)); export const xformFinal: Transform = applyPost(xformFinalCoefsPost, applyTransform(xformFinalCoefs, xformFinalVariations));
export const paletteString = export const paletteString =
"3130323635383B3A3D403F424644484B494D504E52565358" + "3130323635383B3A3D403F424644484B494D504E52565358" +
"5B585D605D626562686B676D706C737571787B767D807B83" + "5B585D605D626562686B676D706C737571787B767D807B83" +
"8580888A858D908A93958F989A949DA099A3A59EA8AAA3AD" + "8580888A858D908A93958F989A949DA099A3A59EA8AAA3AD" +
"AFA8B3B5ADB8BAB2BEBFB7C3C5BCC8CAC1CECFC6D3D4CBD8" + "AFA8B3B5ADB8BAB2BEBFB7C3C5BCC8CAC1CECFC6D3D4CBD8" +
"DAD0DEDFD5E3DFD2E0DFCEDDE0CBDAE0C8D7E0C4D3E0C1D0" + "DAD0DEDFD5E3DFD2E0DFCEDDE0CBDAE0C8D7E0C4D3E0C1D0" +
"E1BECDE1BBCAE1B7C7E1B4C4E1B1C1E2ADBEE2AABAE2A7B7" + "E1BECDE1BBCAE1B7C7E1B4C4E1B1C1E2ADBEE2AABAE2A7B7" +
"E2A3B4E2A0B1E39DAEE399ABE396A8E393A5E490A1E48C9E" + "E2A3B4E2A0B1E39DAEE399ABE396A8E393A5E490A1E48C9E" +
"E4899BE48698E48295E57F92E57C8FE5788CE57589E57285" + "E4899BE48698E48295E57F92E57C8FE5788CE57589E57285" +
"E66E82E66B7FE6687CE66479E76176E75E73E75B70E7576C" + "E66E82E66B7FE6687CE66479E76176E75E73E75B70E7576C" +
"E75469E85166E84D63E84A60E4495EE0485CDC475BD84659" + "E75469E85166E84D63E84A60E4495EE0485CDC475BD84659" +
"D44557D04455CB4353C74252C34150BF404EBB3F4CB73E4B" + "D44557D04455CB4353C74252C34150BF404EBB3F4CB73E4B" +
"B33D49AF3C47AB3B45A73A43A339429F38409B373E97363C" + "B33D49AF3C47AB3B45A73A43A339429F38409B373E97363C" +
"92353A8E34398A33378632358231337E30327A2F30762E2E" + "92353A8E34398A33378632358231337E30327A2F30762E2E" +
"722D2C6E2C2A6A2B29662A276229255E2823592721552620" + "722D2C6E2C2A6A2B29662A276229255E2823592721552620" +
"51251E4D241C49231A4522194121173D20153C1F153A1F14" + "51251E4D241C49231A4522194121173D20153C1F153A1F14" +
"391E14381E14361D14351C13341C13321B13311B132F1A12" + "391E14381E14361D14351C13341C13321B13311B132F1A12" +
"2E19122D19122B18122A1811291711271611261611251510" + "2E19122D19122B18122A1811291711271611261611251510" +
"23151022141021140F1F130F1E120F1C120F1B110E1A110E" + "23151022141021140F1F130F1E120F1C120F1B110E1A110E" +
"18100E170F0E160F0D140E0D130E0D120D0D100C0C0F0C0C" + "18100E170F0E160F0D140E0D130E0D120D0D100C0C0F0C0C" +
"0E0B0C0C0B0C0B0A0B09090B08090B07080B05080A04070A" + "0E0B0C0C0B0C0B0A0B09090B08090B07080B05080A04070A" +
"0606090804090A03088C46728A457087446D85436B824369" + "0606090804090A03088C46728A457087446D85436B824369" +
"8042667D41647B4061793F5F763E5D743D5A713D586F3C56" + "8042667D41647B4061793F5F763E5D743D5A713D586F3C56" +
"6C3B536A3A5168394F65384C63374A6037485E36455B3543" + "6C3B536A3A5168394F65384C63374A6037485E36455B3543" +
"59344057333E54323C5231394F31374D30354A2F32482E30" + "59344057333E54323C5231394F31374D30354A2F32482E30" +
"462D2E432C2B412B293E2B273C2A2439292237281F35271D" + "462D2E432C2B412B293E2B273C2A2439292237281F35271D" +
"32261B3025182D25162B241428231126220F25210F24210E" + "32261B3025182D25162B241428231126220F25210F24210E" +
"23200E221F0E221E0D211E0D201D0D1F1C0D1E1B0C1D1B0C" + "23200E221F0E221E0D211E0D201D0D1F1C0D1E1B0C1D1B0C" +
"1C1A0C1B190B1B180B1A180B19170A18160A17150A161509" + "1C1A0C1B190B1B180B1A180B19170A18160A17150A161509" +
"1514091413091413081312081211081110081010070F0F07" + "1514091413091413081312081211081110081010070F0F07" +
"0E0E070D0D060C0D060C0C060B0B050A0A05090A05080904" + "0E0E070D0D060C0D060C0C060B0B050A0A05090A05080904" +
"070804060704050704050603040503030403020402010302" + "070804060704050704050603040503030403020402010302" +
"0608070C0D0D1112121617171B1C1D2121222626272B2B2D" "0608070C0D0D1112121617171B1C1D2121222626272B2B2D";
function hexToBytes(hex: string) { function hexToBytes(hex: string) {
let bytes: number[] = []; let bytes: number[] = [];
for (let i = 0; i < hex.length; i += 2) { for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substring(i, i + 2), 16)); bytes.push(parseInt(hex.substring(i, i + 2), 16));
} }
return bytes; return bytes;
} }
export const palette = hexToBytes(paletteString).map(value => value / 0xff); export const palette = hexToBytes(paletteString).map(value => value / 0xff);

View File

@ -1,10 +1,15 @@
// hidden-start // hidden-start
import { Variation } from './variation' import { Variation } from './variation'
//hidden-end //hidden-end
export type PdjParams = {a: number, b: number, c: number, d: number}; export type PdjParams = {
export function pdj({a, b, c, d}: PdjParams): Variation { a: number,
return (x, y) => [ 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(a * y) - Math.cos(b * x),
Math.sin(c * x) - Math.cos(d * y) 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) { export function plotBinary(x: number, y: number, image: ImageData) {
const [pixelX, pixelY] = camera(x, y, image.width); const [pixelX, pixelY] = camera(x, y, image.width);
const pixelIndex = histIndex(pixelX, pixelY, image.width, 4); if (pixelX < 0 || pixelX >= image.width || pixelY < 0 || pixelY >= image.height)
if (pixelIndex < 0 || pixelIndex > image.data.length) { return;
return;
}
image.data[pixelIndex] = 0; const pixelIndex = histIndex(pixelX, pixelY, image.width, 4);
image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0; image.data[pixelIndex] = 0;
image.data[pixelIndex + 3] = 0xff; image.data[pixelIndex + 1] = 0;
image.data[pixelIndex + 2] = 0;
image.data[pixelIndex + 3] = 0xff;
} }

View File

@ -1,10 +1,10 @@
// hidden-start // hidden-start
import {Coefs} from './coefs' import { Coefs } from "./transform";
import {Variation} from './variation' import { Variation } from "./variation";
// hidden-end // hidden-end
export function popcorn({c, f}: Coefs): Variation { export const popcorn =
return (x, y) => [ ({ c, f }: Coefs): Variation =>
x + c * Math.sin(Math.tan(3 * y)), (x, y) => [
y + f * Math.sin(Math.tan(3 * x)) 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; 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; const [weight, t] = elem;
if (choice < weight) { if (choice < weight) {
return [idx, t]; 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 { export interface Coefs {
a: number, b: number, c: number, a: number,
d: number, e: number, f: 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(
return [ x: number,
(x * coefs.a + y * coefs.b + coefs.c), y: number,
(x * coefs.d + y * coefs.e + coefs.f) 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];