Post/final transform implementation

This commit is contained in:
Bradlee Speice 2024-11-30 17:35:42 -05:00
parent 1aa45e3f59
commit 6c4d73f081
17 changed files with 360 additions and 120 deletions

View File

@ -28,8 +28,8 @@ export default function GasketWeighted() {
const weightInput = (title, weight, setWeight) => (
<>
<div className={styles.inputDiv}>
<p><TeX>{title}</TeX> weight:<span style={{float: 'right'}}>{weight}</span></p>
<div className={styles.inputElement}>
<p><TeX>{title}</TeX> weight: {weight}</p>
<input type={'range'} min={0} max={1} step={.01} style={{width: '100%'}} value={weight}
onInput={e => setWeight(Number(e.currentTarget.value))}/>
</div>
@ -38,7 +38,7 @@ export default function GasketWeighted() {
return (
<>
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
{weightInput("F_0", f0Weight, setF0Weight)}
{weightInput("F_1", f1Weight, setF1Weight)}
{weightInput("F_2", f2Weight, setF2Weight)}

View File

@ -0,0 +1,48 @@
import TeX from "@matejmazur/react-katex";
import {Coefs} from "../src/coefs";
import styles from "../src/css/styles.module.css";
export interface Props {
title: String;
isPost: boolean;
coefs: Coefs;
setCoefs: (coefs: Coefs) => void;
}
export const CoefEditor = ({title, isPost, coefs, setCoefs}: Props) => {
return (
<div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: 'auto auto auto'}}>
<p className={styles.inputTitle} style={{gridColumn: '1/-1'}}>{title}</p>
<div className={styles.inputElement}>
<p>{isPost ? <TeX>\alpha</TeX> : 'a'}: {coefs.a}</p>
<input type={'range'} min={0} max={2} step={0.01} style={{width: '100%'}} 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={0} max={2} step={0.01} style={{width: '100%'}} 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={0} max={2} step={0.01} style={{width: '100%'}} 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={0} max={2} step={0.01} style={{width: '100%'}} 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={0} max={2} step={0.01} style={{width: '100%'}} 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={0} max={2} step={0.01} style={{width: '100%'}} value={coefs.f}
onInput={e => setCoefs({...coefs, f: Number(e.currentTarget.value)})}/>
</div>
</div>
)
}

View File

@ -1,15 +1,7 @@
import {useContext, useEffect, useState} from "react";
import { blend } from "./blend";
import { applyCoefs, Coefs } from "../src/coefs"
import {randomBiUnit} from "../src/randomBiUnit";
import {linear} from "../src/linear";
import {julia} from "../src/julia";
import {popcorn} from "../src/popcorn";
import {pdj} from "../src/pdj";
import {Variation} from "../src/variation";
import {Transform} from "../src/transform";
import {
pdjParams,
identityCoefs,
xform1Coefs,
xform1Weight,
xform2Coefs,
@ -17,26 +9,15 @@ import {
xform3Coefs,
xform3Weight
} from "../src/params";
import {randomChoice} from "../src/randomChoice";
import {plotBinary} from "../src/plotBinary"
import {PainterContext} from "../src/Canvas"
import styles from "../src/css/styles.module.css"
type VariationBlend = {
linear: number,
julia: number,
popcorn: number,
pdj: number
}
import {buildBlend, buildTransform} from "./buildTransform"
import {chaosGameFinal} from "./chaosGameFinal"
import {VariationEditor, VariationProps} from "./VariationEditor"
export default function FlameBlend() {
const quality = 2;
const step = 5000;
const {width, height, setPainter} = useContext(PainterContext);
const xform1Default: VariationBlend = {
const xform1Default: VariationProps = {
linear: 0,
julia: 1,
popcorn: 0,
@ -44,7 +25,7 @@ export default function FlameBlend() {
}
const [xform1Variations, setXform1Variations] = useState(xform1Default)
const xform2Default: VariationBlend = {
const xform2Default: VariationProps = {
linear: 1,
julia: 0,
popcorn: 1,
@ -52,7 +33,7 @@ export default function FlameBlend() {
}
const [xform2Variations, setXform2Variations] = useState(xform2Default)
const xform3Default: VariationBlend = {
const xform3Default: VariationProps = {
linear: 0,
julia: 0,
popcorn: 0,
@ -60,80 +41,21 @@ export default function FlameBlend() {
}
const [xform3Variations, setXform3Variations] = useState(xform3Default)
function buildTransform(coefs: Coefs, variations: VariationBlend): Transform {
return (x: number, y: number) => {
const [varX, varY] = applyCoefs(x, y, coefs);
const varFunctions: [number, Variation][] = [
[variations.linear, linear],
[variations.julia, julia],
[variations.popcorn, popcorn(coefs)],
[variations.pdj, pdj(pdjParams)]
]
// Cheating a bit here; for purposes of code re-use, use the post- and final-transform-enabled chaos game,
// and swap in identity components for each
const identityXform: Transform = (x, y) => [x, y];
return blend(varX, varY, varFunctions);
}
}
useEffect(() => setPainter(chaosGameFinal(width, height, [
[xform1Weight, buildTransform(xform1Coefs, buildBlend(xform1Coefs, xform1Variations))],
[xform2Weight, buildTransform(xform2Coefs, buildBlend(xform2Coefs, xform2Variations))],
[xform3Weight, buildTransform(xform3Coefs, buildBlend(xform3Coefs, xform3Variations))]
], identityXform)), [xform1Variations, xform2Variations, xform3Variations]);
const image = new ImageData(width, height);
function* chaosGame() {
let [x, y] = [randomBiUnit(), randomBiUnit()];
const transforms: [number, Transform][] = [
[xform1Weight, buildTransform(xform1Coefs, xform1Variations)],
[xform2Weight, buildTransform(xform2Coefs, xform2Variations)],
[xform3Weight, buildTransform(xform3Coefs, xform3Variations)]
]
const iterations = quality * image.width * image.height;
for (let i = 0; i < iterations; i++) {
let [_, transform] = randomChoice(transforms);
[x, y] = transform(x, y);
if (i > 20)
plotBinary(x, y, image);
if (i % step === 0) {
console.log(`Checking in; iterations=${i}`)
yield image;
}
}
yield image;
}
useEffect(() => setPainter(chaosGame()), [xform1Variations, xform2Variations, xform3Variations]);
const variationEditor = (title, variations, setVariations) => {
return (
<>
<p style={{gridColumn: '1/-1'}}>{title}:</p>
<div className={styles.inputDiv}>
<p>Linear: {variations.linear}</p>
<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.inputDiv}>
<p>Julia: {variations.julia}</p>
<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.inputDiv}>
<p>Popcorn: {variations.popcorn}</p>
<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.inputDiv}>
<p>PDJ: {variations.pdj}</p>
<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>
<VariationEditor title={"Transform 1"} variations={xform1Variations} setVariations={setXform1Variations}/>
<VariationEditor title={"Transform 2"} variations={xform2Variations} setVariations={setXform2Variations}/>
<VariationEditor title={"Transform 3"} variations={xform3Variations} setVariations={setXform3Variations}/>
</>
)
}
return (
<div style={{paddingTop: '1em', display: 'grid', gridTemplateColumns: 'auto auto auto auto'}}>
{variationEditor("Transform 1", xform1Variations, setXform1Variations)}
{variationEditor("Transform 2", xform2Variations, setXform2Variations)}
{variationEditor("Transform 3", xform3Variations, setXform3Variations)}
</div>
)
}

View File

@ -0,0 +1,64 @@
import {useContext, useEffect, useState} from "react";
import {Coefs} from "../src/coefs"
import {
xform1Coefs,
xform1Weight,
xform1Variations,
xform1CoefsPost,
xform2Coefs,
xform2Weight,
xform2Variations,
xform2CoefsPost,
xform3Coefs,
xform3Weight,
xform3Variations,
xform3CoefsPost,
xformFinalCoefs as xformFinalCoefsDefault,
xformFinalCoefsPost as xformFinalCoefsPostDefault,
} from "../src/params";
import {PainterContext} from "../src/Canvas"
import {buildBlend, buildTransform} from "./buildTransform";
import {transformPost} from "./post";
import {chaosGameFinal} from "./chaosGameFinal"
import {VariationEditor, VariationProps} from "./VariationEditor";
import {CoefEditor} from "./CoefEditor";
import {Transform} from "../src/transform";
export default function FlameFinal() {
const {width, height, setPainter} = useContext(PainterContext);
const [xformFinalCoefs, setXformFinalCoefs] = useState<Coefs>(xformFinalCoefsDefault);
const xformFinalVariationsDefault: VariationProps = {
linear: 0,
julia: 1,
popcorn: 0,
pdj: 0
}
const [xformFinalVariations, setXformFinalVariations] = useState<VariationProps>(xformFinalVariationsDefault);
const [xformFinalCoefsPost, setXformFinalCoefsPost] = useState<Coefs>(xformFinalCoefsPostDefault);
useEffect(() => {
const transforms: [number, Transform][] = [
[xform1Weight, transformPost(buildTransform(xform1Coefs, xform1Variations), xform1CoefsPost)],
[xform2Weight, transformPost(buildTransform(xform2Coefs, xform2Variations), xform2CoefsPost)],
[xform3Weight, transformPost(buildTransform(xform3Coefs, xform3Variations), xform3CoefsPost)]
];
const finalBlend = buildBlend(xformFinalCoefs, xformFinalVariations);
const finalTransform = buildTransform(xformFinalCoefs, finalBlend);
const finalPost = transformPost(finalTransform, xformFinalCoefsPost);
setPainter(chaosGameFinal(width, height, transforms, finalPost));
}, [xformFinalCoefs, xformFinalVariations, xformFinalCoefsPost]);
return (
<>
<CoefEditor title={"Final Transform"} isPost={false} coefs={xformFinalCoefs} setCoefs={setXformFinalCoefs}/>
<VariationEditor title={"Final Transform Variations"} variations={xformFinalVariations}
setVariations={setXformFinalVariations}/>
<CoefEditor title={"Final Transform Post"} isPost={true} coefs={xformFinalCoefsPost} setCoefs={setXformFinalCoefsPost}/>
</>
)
}

View File

@ -0,0 +1,46 @@
import {useContext, useEffect, useState} from "react";
import {Coefs} from "../src/coefs"
import {Transform} from "../src/transform";
import {
xform1Coefs,
xform1Weight,
xform1Variations,
xform1CoefsPost as xform1CoefsPostDefault,
xform2Coefs,
xform2Weight,
xform2Variations,
xform2CoefsPost as xform2CoefsPostDefault,
xform3Coefs,
xform3Weight,
xform3Variations,
xform3CoefsPost as xform3CoefsPostDefault
} from "../src/params";
import {PainterContext} from "../src/Canvas"
import {chaosGameFinal} from "./chaosGameFinal"
import {CoefEditor} from "./CoefEditor"
import {transformPost} from "./post";
import {buildTransform} from "./buildTransform";
export default function FlamePost() {
const {width, height, setPainter} = useContext(PainterContext);
const [xform1CoefsPost, setXform1PostCoefs] = useState<Coefs>(xform1CoefsPostDefault);
const [xform2CoefsPost, setXform2PostCoefs] = useState<Coefs>(xform2CoefsPostDefault);
const [xform3CoefsPost, setXform3PostCoefs] = useState<Coefs>(xform3CoefsPostDefault);
const identityXform: Transform = (x, y) => [x, y];
useEffect(() => setPainter(chaosGameFinal(width, height, [
[xform1Weight, transformPost(buildTransform(xform1Coefs, xform1Variations), xform1CoefsPost)],
[xform2Weight, transformPost(buildTransform(xform2Coefs, xform2Variations), xform2CoefsPost)],
[xform3Weight, transformPost(buildTransform(xform3Coefs, xform3Variations), xform3CoefsPost)]
], identityXform)), [xform1CoefsPost, xform2CoefsPost, xform3CoefsPost]);
return (
<>
<CoefEditor title={"Transform 1 Post"} isPost={true} coefs={xform1CoefsPost} setCoefs={setXform1PostCoefs}/>
<CoefEditor title={"Transform 2 Post"} isPost={true} coefs={xform2CoefsPost} setCoefs={setXform2PostCoefs}/>
<CoefEditor title={"Transform 3 Post"} isPost={true} coefs={xform3CoefsPost} setCoefs={setXform3PostCoefs}/>
</>
)
}

View File

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

View File

@ -1,7 +1,10 @@
// hidden-start
import { Variation } from "../src/variation"
import {VariationBlend} from "../src/variationBlend";
// hidden-end
export function blend(x: number, y: number, variations: [number, Variation][]): [number, number] {
export function blend(
x: number,
y: number,
variations: VariationBlend): [number, number] {
let [finalX, finalY] = [0, 0];
for (const [weight, variation] of variations) {

View File

@ -0,0 +1,26 @@
import {applyCoefs, Coefs} from "../src/coefs";
import {VariationProps} from "./VariationEditor";
import {Transform} from "../src/transform";
import {linear} from "../src/linear";
import {julia} from "../src/julia";
import {popcorn} from "../src/popcorn";
import {pdj} from "../src/pdj";
import {pdjParams} from "../src/params";
import {blend} from "./blend";
import {VariationBlend} from "../src/variationBlend";
export function buildBlend(coefs: Coefs, variations: VariationProps): VariationBlend {
return [
[variations.linear, linear],
[variations.julia, julia],
[variations.popcorn, popcorn(coefs)],
[variations.pdj, pdj(pdjParams)]
]
}
export function buildTransform(coefs: Coefs, variations: VariationBlend): Transform {
return (x: number, y: number) => {
[x, y] = applyCoefs(x, y, coefs);
return blend(x, y, variations);
}
}

View File

@ -0,0 +1,29 @@
// hidden-start
import { randomBiUnit } from "../src/randomBiUnit";
import { randomChoice } from "../src/randomChoice";
import { plotBinary as plot } from "../src/plotBinary"
import {Transform} from "../src/transform";
const iterations = 500_000;
const step = 1000;
// hidden-end
export function* chaosGameFinal(width: number, height: number, transforms: [number, Transform][], final: Transform) {
let image = new ImageData(width, height);
let [x, y] = [randomBiUnit(), randomBiUnit()];
for (let i = 0; i < iterations; i++) {
const [_, transform] = randomChoice(transforms);
[x, y] = transform(x, y);
// highlight-start
[x, y] = final(x, y);
// highlight-end
if (i > 20)
plot(x, y, image);
if (i % step === 0)
yield image;
}
yield image;
}

View File

@ -37,12 +37,14 @@ the general format. For example:
$$
\begin{align*}
F_0(x,y) &= \left({x \over 2}, {y \over 2}\right) \\
&= (a_0 \cdot x + b_0 \cdot y + c_o, d_0 \cdot x + e_0 \cdot y + f_0) \\
&= (a_0 \cdot x + b_0 \cdot y + c_0, d_0 \cdot x + e_0 \cdot y + f_0) \\
& a_0 = 0.5 \hspace{0.2cm} b_0 = 0 \hspace{0.2cm} c_0 = 0 \\
& d_0 = 0 \hspace{0.2cm} e_0 = 0.5 \hspace{0.2cm} f_0 = 0
\end{align*}
$$
TODO: Explain the applyCoefs function
However, these transforms are pretty boring. We can build more exciting images by using additional functions
within the transform. These "sub-functions" are called "variations":
@ -123,8 +125,7 @@ import pdjSrc from '!!raw-loader!../src/pdj'
Now, one variation is fun, but we can also combine variations in a single transform by "blending."
Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs.
We'll also give each variation a weight ($v_j$) that scales the output, which allows us to control
how much each variation contributes to the transform:
We'll also give each variation a weight ($v_{ij}$) to control how much it contributes to the transform:
$$
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)
@ -136,7 +137,10 @@ import blendSource from "!!raw-loader!./blend";
<CodeBlock language={'typescript'}>{blendSource}</CodeBlock>
And with that in place, we have enough to render a first full fractal flame:
And with that in place, we have enough to render a first full fractal flame.
The sliders below change the variation weights for each transform (the $v_{ij}$ parameters);
try changing them around to see which parts of the image are controlled by
each transform.
import BrowserOnly from "@docusaurus/BrowserOnly";
import Canvas from "../src/Canvas";
@ -145,3 +149,28 @@ import FlameBlend from "./FlameBlend";
<Canvas width={500} height={500}>
<BrowserOnly>{() => <FlameBlend/>}</BrowserOnly>
</Canvas>
## Post transforms
After variation blending, we apply a second set of transform coordinates.
The fractal flame below starts with the same initial transforms/variations as the previous fractal flame,
but allows modifying the post-transform coefficients.
$$
P_i(x, y) = (\alpha_i x + \beta_i y + \gamma_i, \delta_i x + \epsilon_i y + \zeta_i)
$$
import FlamePost from "./FlamePost";
<Canvas width={500} height={500}>
<BrowserOnly>{() => <FlamePost/>}</BrowserOnly>
</Canvas>
## Final transform
import FlameFinal from "./FlameFinal";
<Canvas width={500} height={500}>
<BrowserOnly>{() => <FlameFinal/>}</BrowserOnly>
</Canvas>

View File

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

View File

@ -3,7 +3,7 @@ export interface Coefs {
d: number, e: number, f: number
}
export function applyCoefs(x: number, y: number, coefs: Coefs) {
export function applyCoefs(x: number, y: number, coefs: Coefs): [number, number] {
return [
(x * coefs.a + y * coefs.b + coefs.c),
(x * coefs.d + y * coefs.e + coefs.f)

View File

@ -1,4 +1,23 @@
.inputDiv {
.inputGroup {
padding: .5em;
margin: .5em;
border: 1px solid;
border-radius: var(--ifm-global-radius);
border-color: var(--ifm-color-emphasis-500);
}
.inputTitle {
border: 0 solid;
border-bottom: 1px solid;
border-color: var(--ifm-color-emphasis-500);
margin-bottom: .5em;
}
.inputElement {
padding-left: .5em;
padding-right: 1em;
}
.inputElement > p {
margin: 0
}

View File

@ -3,11 +3,14 @@ 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 theta = Math.atan2(x, y);
const omega = Math.random() > 0.5 ? 0 : Math.PI;
const sqrtR = Math.sqrt(r);
const thetaVal = theta / 2 + omega;
return [
r * Math.cos(theta / 2 + omega),
r * Math.sin(theta / 2 + omega)
sqrtR * Math.cos(thetaVal),
sqrtR * Math.sin(thetaVal)
]
}

View File

@ -4,11 +4,11 @@
*/
import { Coefs } from './coefs';
import {VariationBlend} from "./variationBlend";
import { linear } from './linear'
import { julia } from './julia'
import { popcorn } from './popcorn'
import {pdj, PdjParams} from './pdj'
import {Variation} from "./variation"
export const identityCoefs: Coefs = {
a: 1, b: 0, c: 0,
@ -25,7 +25,7 @@ export const xform1Coefs = {
d: 1.381068, e: -1.381068, f: 0,
}
export const xform1CoefsPost = identityCoefs;
export const xform1Variations = [
export const xform1Variations: VariationBlend = [
[1, julia]
]
export const xform1Color = 0;
@ -39,7 +39,7 @@ export const xform2CoefsPost = {
a: 1, b: 0, c: 0.241352,
d: 0, e: 1, f: 0.271521,
}
export const xform2Variations = [
export const xform2Variations: VariationBlend = [
[1, linear],
[1, popcorn(xform2Coefs)]
]
@ -51,7 +51,7 @@ export const xform3Coefs = {
d: 0.740356, e: -1.455964, f: -0.362059,
}
export const xform3CoefsPost = identityCoefs;
export const xform3Variations = [
export const xform3Variations: VariationBlend = [
[1, pdj(pdjParams)]
];
export const xform3Color = 0.349;
@ -61,7 +61,7 @@ export const xformFinalCoefs = {
d: 0, e: 2, f: 0
}
export const xformFinalCoefsPost = identityCoefs;
export const xformFinalVariations = [
export const xformFinalVariations: VariationBlend = [
[1, julia]
]
export const xformFinalColor = 0;

View File

@ -0,0 +1,2 @@
import {Variation} from "./variation";
export type VariationBlend = [number, Variation][];