mirror of
https://github.com/bspeice/speice.io
synced 2025-03-10 08:41:28 -04:00
Finish first draft.
This commit is contained in:
parent
59a3e455f0
commit
ced7827d0c
@ -16,14 +16,14 @@ export default function FlameCamera({ children }: Props) {
|
|||||||
|
|
||||||
const [zoom, setZoom] = React.useState(0);
|
const [zoom, setZoom] = React.useState(0);
|
||||||
const [rotate, setRotate] = React.useState(0);
|
const [rotate, setRotate] = React.useState(0);
|
||||||
const [offsetX, setOffsetX] = React.useState(0);
|
const [positionX, setPositionX] = React.useState(0);
|
||||||
const [offsetY, setOffsetY] = React.useState(0);
|
const [positionY, setPositionY] = React.useState(0);
|
||||||
|
|
||||||
const resetCamera = () => {
|
const resetCamera = () => {
|
||||||
setZoom(0);
|
setZoom(0);
|
||||||
setRotate(0);
|
setRotate(0);
|
||||||
setOffsetX(0);
|
setPositionX(0);
|
||||||
setOffsetY(0);
|
setPositionY(0);
|
||||||
}
|
}
|
||||||
const resetButton = <button className={styles.inputReset} onClick={resetCamera}>Reset</button>;
|
const resetButton = <button className={styles.inputReset} onClick={resetCamera}>Reset</button>;
|
||||||
|
|
||||||
@ -42,12 +42,12 @@ export default function FlameCamera({ children }: Props) {
|
|||||||
finalColor: { color: params.xformFinalColor, colorSpeed: 0.5 },
|
finalColor: { color: params.xformFinalColor, colorSpeed: 0.5 },
|
||||||
scale,
|
scale,
|
||||||
zoom,
|
zoom,
|
||||||
rotate: rotate / 180 * Math.PI,
|
rotate: -rotate / 180 * Math.PI,
|
||||||
offsetX,
|
positionX,
|
||||||
offsetY
|
positionY
|
||||||
};
|
};
|
||||||
setPainter(chaosGameCamera(gameParams));
|
setPainter(chaosGameCamera(gameParams));
|
||||||
}, [scale, zoom, rotate, offsetX, offsetY]);
|
}, [scale, zoom, rotate, positionX, positionY]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -64,14 +64,14 @@ export default function FlameCamera({ children }: Props) {
|
|||||||
onInput={e => setRotate(Number(e.currentTarget.value))}/>
|
onInput={e => setRotate(Number(e.currentTarget.value))}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.inputElement}>
|
<div className={styles.inputElement}>
|
||||||
<p>Offset X: {offsetX}</p>
|
<p>Offset X: {positionX}</p>
|
||||||
<input type={"range"} min={-2} max={2} step={0.01} value={offsetX}
|
<input type={"range"} min={-2} max={2} step={0.01} value={positionX}
|
||||||
onInput={e => setOffsetX(Number(e.currentTarget.value))}/>
|
onInput={e => setPositionX(Number(e.currentTarget.value))}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.inputElement}>
|
<div className={styles.inputElement}>
|
||||||
<p>Offset Y: {offsetY}</p>
|
<p>Offset Y: {positionY}</p>
|
||||||
<input type={"range"} min={-2} max={2} step={0.01} value={offsetY}
|
<input type={"range"} min={-2} max={2} step={0.01} value={positionY}
|
||||||
onInput={e => setOffsetY(Number(e.currentTarget.value))}/>
|
onInput={e => setPositionY(Number(e.currentTarget.value))}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
|
@ -3,19 +3,17 @@ export function camera(
|
|||||||
y: number,
|
y: number,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
scale: number,
|
positionX: number,
|
||||||
zoom: number,
|
positionY: number,
|
||||||
rotate: number,
|
rotate: number,
|
||||||
offsetX: number,
|
zoom: number,
|
||||||
offsetY: number,
|
scale: number,
|
||||||
): [number, number] {
|
): [number, number] {
|
||||||
const zoomFactor = Math.pow(2, zoom);
|
// Position, rotation, and zoom are
|
||||||
|
|
||||||
// Zoom, offset, and rotation are
|
|
||||||
// applied in IFS coordinates
|
// applied in IFS coordinates
|
||||||
[x, y] = [
|
[x, y] = [
|
||||||
(x - offsetX) * zoomFactor,
|
(x - positionX),
|
||||||
(y - offsetY) * zoomFactor,
|
(y - positionY),
|
||||||
];
|
];
|
||||||
|
|
||||||
[x, y] = [
|
[x, y] = [
|
||||||
@ -23,7 +21,12 @@ export function camera(
|
|||||||
y * Math.sin(rotate),
|
y * Math.sin(rotate),
|
||||||
x * Math.sin(rotate) +
|
x * Math.sin(rotate) +
|
||||||
y * Math.cos(rotate),
|
y * Math.cos(rotate),
|
||||||
]
|
];
|
||||||
|
|
||||||
|
[x, y] = [
|
||||||
|
x * Math.pow(2, zoom),
|
||||||
|
y * Math.pow(2, zoom)
|
||||||
|
];
|
||||||
|
|
||||||
// Scale transforms IFS coordinates
|
// Scale transforms IFS coordinates
|
||||||
// to pixel coordinates. Shift by half
|
// to pixel coordinates. Shift by half
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// hidden-start
|
|
||||||
import { Props as ChaosGameColorProps } from "../3-log-density/chaosGameColor";
|
import { Props as ChaosGameColorProps } from "../3-log-density/chaosGameColor";
|
||||||
import { randomBiUnit } from "../src/randomBiUnit";
|
import { randomBiUnit } from "../src/randomBiUnit";
|
||||||
import { randomChoice } from "../src/randomChoice";
|
import { randomChoice } from "../src/randomChoice";
|
||||||
@ -10,14 +9,13 @@ import {paintColor} from "../3-log-density/paintColor";
|
|||||||
|
|
||||||
const quality = 10;
|
const quality = 10;
|
||||||
const step = 100_000;
|
const step = 100_000;
|
||||||
// hidden-end
|
|
||||||
|
|
||||||
export type Props = ChaosGameColorProps & {
|
export type Props = ChaosGameColorProps & {
|
||||||
scale: number;
|
positionX: number;
|
||||||
zoom: number,
|
positionY: number;
|
||||||
rotate: number;
|
rotate: number;
|
||||||
offsetX: number;
|
zoom: number,
|
||||||
offsetY: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* chaosGameCamera(
|
export function* chaosGameCamera(
|
||||||
@ -29,16 +27,15 @@ export function* chaosGameCamera(
|
|||||||
palette,
|
palette,
|
||||||
colors,
|
colors,
|
||||||
finalColor,
|
finalColor,
|
||||||
scale,
|
positionX,
|
||||||
zoom,
|
positionY,
|
||||||
rotate,
|
rotate,
|
||||||
offsetX,
|
zoom,
|
||||||
offsetY,
|
scale,
|
||||||
}: Props
|
}: Props
|
||||||
) {
|
) {
|
||||||
const pixels = width * height;
|
const pixels = width * height;
|
||||||
|
|
||||||
// highlight-start
|
|
||||||
const imgRed = Array<number>(pixels)
|
const imgRed = Array<number>(pixels)
|
||||||
.fill(0);
|
.fill(0);
|
||||||
const imgGreen = Array<number>(pixels)
|
const imgGreen = Array<number>(pixels)
|
||||||
@ -54,7 +51,7 @@ export function* chaosGameCamera(
|
|||||||
c: number
|
c: number
|
||||||
) => {
|
) => {
|
||||||
const [pixelX, pixelY] =
|
const [pixelX, pixelY] =
|
||||||
camera(x, y, width, height, scale, zoom, rotate, offsetX, offsetY);
|
camera(x, y, width, height, positionX, positionY, rotate, zoom, scale);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pixelX < 0 ||
|
pixelX < 0 ||
|
||||||
@ -75,7 +72,6 @@ export function* chaosGameCamera(
|
|||||||
imgBlue[hIndex] += b;
|
imgBlue[hIndex] += b;
|
||||||
imgAlpha[hIndex] += 1;
|
imgAlpha[hIndex] += 1;
|
||||||
}
|
}
|
||||||
// highlight-end
|
|
||||||
|
|
||||||
let [x, y] = [
|
let [x, y] = [
|
||||||
randomBiUnit(),
|
randomBiUnit(),
|
||||||
|
@ -22,7 +22,7 @@ some missing features.
|
|||||||
To review, the restrictions we've had so far:
|
To review, the restrictions we've had so far:
|
||||||
|
|
||||||
> ...we need to convert from fractal flame coordinates to pixel coordinates.
|
> ...we need to convert from fractal flame coordinates to pixel coordinates.
|
||||||
> To simplify things, we'll assume that we're _plotting a square image_ with range $[0,1]$ for both x and y
|
> To simplify things, we'll assume that we're plotting a square image with range $[0,1]$ for both x and y
|
||||||
>
|
>
|
||||||
> -- [The fractal flame algorithm](/2024/11/playing-with-fire)
|
> -- [The fractal flame algorithm](/2024/11/playing-with-fire)
|
||||||
|
|
||||||
@ -37,20 +37,82 @@ Second, the assumption that fractals use the range $[0, 1]$. My statement above
|
|||||||
for Sierpinski's Gasket, the solution set is indeed defined on $[0, 1]$, but all other images in the series
|
for Sierpinski's Gasket, the solution set is indeed defined on $[0, 1]$, but all other images in the series
|
||||||
use a display range of $[-2, 2]$.
|
use a display range of $[-2, 2]$.
|
||||||
|
|
||||||
Finally, the camera controls available in Apophysis/[`flam3`](https://github.com/scottdraves/flam3/wiki/XML-File-Format)
|
## Parameters
|
||||||
have a number of settings that were simply not implemented so far:
|
|
||||||
|
For comparison, here are the camera controls available in Apophysis and [`flam3`](https://github.com/scottdraves/flam3/wiki/XML-File-Format):
|
||||||
|
|
||||||
<center></center>
|
<center></center>
|
||||||
|
|
||||||
The parameters remaining to implement are scale, zoom, rotation, and position.
|
There are four parameters yet to implement: position, rotation, zoom, and scale.
|
||||||
|
|
||||||
## Scale
|
### Position
|
||||||
|
|
||||||
Fractal flames are defined on a continuous range. At some point, we must convert from fractal flame coordinates
|
Fractal flames normally use the origin as the center of an image. The position parameters (X and Y) move
|
||||||
into specific pixels, and scale is the parameter we use to do it. Specifically, scale is **the number of pixels
|
the center point, which effectively pans the image. A positive X position shifts the image left,
|
||||||
in one unit of the fractal flame coordinate system**.
|
and a negative X position shifts the image right. Similarly, a positive Y position shifts the image up,
|
||||||
|
and a negative Y position shifts the image down.
|
||||||
|
|
||||||
For example, if you open the [reference parameters](../params.flame) in a text editor, you'll see the following:
|
To apply the position, simply subtract the X and Y position from each point in the chaos game prior to plotting it:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
[x, y] = [
|
||||||
|
x - positionX,
|
||||||
|
y - positionY
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotation
|
||||||
|
|
||||||
|
After the position parameters are applied, we can rotate the image around the (potentially shifted) center point.
|
||||||
|
To do so, we'll go back to the [affine transformations](https://en.wikipedia.org/wiki/Affine_transformation)
|
||||||
|
we've been using. Specifically, the rotation angle $\theta$ gives us a transform matrix we can apply to our point:
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{bmatrix}
|
||||||
|
\text{cos}(\theta) & -\text{sin}(\theta) \\
|
||||||
|
\text{sin}(\theta) & \text{cos}(\theta)
|
||||||
|
\end{bmatrix}
|
||||||
|
|
||||||
|
\begin{bmatrix}
|
||||||
|
x \\
|
||||||
|
y
|
||||||
|
\end{bmatrix}
|
||||||
|
$$
|
||||||
|
|
||||||
|
As a minor tweak, we also negate the rotation angle to match the behavior of Apophysis/`flam3`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
[x, y] = [
|
||||||
|
x * Math.cos(-rotate) -
|
||||||
|
y * Math.sin(-rotate),
|
||||||
|
x * Math.sin(-rotate) +
|
||||||
|
y * Math.cos(-rotate),
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zoom
|
||||||
|
|
||||||
|
This parameter does what the name implies; zoom in and out of the image. Specifically, for a zoom parameter $z$,
|
||||||
|
every point in the chaos game is scaled by $\text{pow}(2, z)$ prior to plotting. For example, if the point is $(1, 1)$,
|
||||||
|
a zoom of 1 means we actually plot $(1, 1) \cdot \text{pow}(2, 1) = (2, 2)$.
|
||||||
|
|
||||||
|
```
|
||||||
|
[x, y] = [
|
||||||
|
x * Math.pow(2, zoom),
|
||||||
|
y * Math.pow(2, zoom)
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
In addition to scaling the image, renderers also [scale the image quality](https://github.com/scottdraves/flam3/blob/f8b6c782012e4d922ef2cc2f0c2686b612c32504/rect.c#L796-L797)
|
||||||
|
to compensate for the zoom parameter.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Scale
|
||||||
|
|
||||||
|
Finally, we need to convert from fractal flame coordinates to individual pixels. The scale parameter defines
|
||||||
|
how many pixels are in one unit of the fractal flame coordinate system. For example, if you open the
|
||||||
|
[reference parameters](../params.flame) in a text editor, you'll see the following:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<flame name="final xform" size="600 600" center="0 0" scale="150">
|
<flame name="final xform" size="600 600" center="0 0" scale="150">
|
||||||
@ -61,29 +123,57 @@ with 150 pixels per unit. Dividing 600 by 150 gives us an image that is 4 units
|
|||||||
And because the center is at $(0, 0)$, the final image is effectively looking at the range $[-2, 2]$ in the
|
And because the center is at $(0, 0)$, the final image is effectively looking at the range $[-2, 2]$ in the
|
||||||
fractal coordinate system (as mentioned above).
|
fractal coordinate system (as mentioned above).
|
||||||
|
|
||||||
|
To go from the fractal coordinate system to a pixel coordinate system, we multiply by the scale,
|
||||||
|
then subtract half the image width and height:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
[pixelX, pixelY] = [
|
||||||
|
x * scale - imageWidth / 2,
|
||||||
|
y * scale - imageHeight / 2
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
Scale can be used to implement a kind of "zoom" in images. If the reference parameters instead used `scale="300"`,
|
Scale can be used to implement a kind of "zoom" in images. If the reference parameters instead used `scale="300"`,
|
||||||
the same 600 pixels would instead be looking at the range $[-1, 1]$ in the fractal coordinate system.
|
the same 600 pixels would instead be looking at the range $[-1, 1]$ in the fractal coordinate system.
|
||||||
|
|
||||||
This also demonstrates the biggest problem with using scale: it's a parameter that only controls the output image.
|
However, this also demonstrates the biggest problem with using scale: it's a parameter that only controls the output image.
|
||||||
If the output image changed to `size="1200 1200"`, and scale stayed the same, the output image would have
|
If the output image changed to `size="1200 1200"` and we kept `scale="150"`, the output image would
|
||||||
a lot more white space. Instead, it's usually better to use a different parameter for controlling image size.
|
be looking at the range $[-4, 4]$ - nothing but white space. Because, using the zoom parameter
|
||||||
|
is the preferred way to zoom in and out of an image.
|
||||||
|
|
||||||
## Zoom
|
:::info
|
||||||
|
One final note about the camera controls: every step in this process (position, rotation, zoom, scale)
|
||||||
|
is an affine transformation. And because affine transformations can be chained together, it's possible to
|
||||||
|
express all of our camera controls as a single transformation matrix. This is important for software optimization;
|
||||||
|
rather than applying individual camera controls step-by-step, apply all of them at once.
|
||||||
|
|
||||||
|
Additionally, because the camera controls are an affine transformation, they could be implemented
|
||||||
|
as a transform after the final transform. In practice though, it's helpful to control them separately.
|
||||||
|
:::
|
||||||
|
|
||||||
## Rotation
|
## Camera
|
||||||
|
|
||||||
## Offset
|
|
||||||
|
|
||||||
|
|
||||||
|
With the individual steps defined, we can put together a more robust "camera" for viewing the fractal flame.
|
||||||
|
|
||||||
import CodeBlock from "@theme/CodeBlock";
|
import CodeBlock from "@theme/CodeBlock";
|
||||||
|
|
||||||
import cameraSource from "!!raw-loader!./camera"
|
import cameraSource from "!!raw-loader!./camera"
|
||||||
|
|
||||||
<CodeBlock language="typescript">{cameraSource}</CodeBlock>
|
<CodeBlock language="typescript">{cameraSource}</CodeBlock>
|
||||||
|
|
||||||
|
For demonstration, the output image has a 4:3 aspect ratio, removing the previous restriction of a square image.
|
||||||
|
In addition, the scale is automatically chosen to make sure the width of the image covers the range $[-2, 2]$.
|
||||||
|
As a result of the aspect ratio, the image height effectively covers the range $[-1.5, 1.5]$.
|
||||||
|
|
||||||
import {SquareCanvas} from "../src/Canvas";
|
import {SquareCanvas} from "../src/Canvas";
|
||||||
import FlameCamera from "./FlameCamera";
|
import FlameCamera from "./FlameCamera";
|
||||||
|
|
||||||
<SquareCanvas name={"flame_camera"} width={'95%'} aspectRatio={'4/3'}><FlameCamera /></SquareCanvas>
|
<SquareCanvas name={"flame_camera"} width={'95%'} aspectRatio={'4/3'}><FlameCamera /></SquareCanvas>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The fractal images so far relied on critical assumptions about the output format to make sure everything
|
||||||
|
looked correct. However, we can implement a 2D "camera" with a series of affine transformations - going from
|
||||||
|
the fractal flame coordinate system to pixel coordinates. Later implementations of fractal flame renderers like
|
||||||
|
[Fractorium](http://fractorium.com/) operate in 3D, and have to implement the camera slightly differently.
|
||||||
|
|
||||||
|
But for this blog series, it's nice to achieve feature parity with the existing code.
|
Loading…
Reference in New Issue
Block a user