mirror of
				https://github.com/bspeice/speice.io
				synced 2025-11-03 18:10:32 -05:00 
			
		
		
		
	Finish first draft.
This commit is contained in:
		@ -16,14 +16,14 @@ export default function FlameCamera({ children }: Props) {
 | 
			
		||||
 | 
			
		||||
  const [zoom, setZoom] = React.useState(0);
 | 
			
		||||
  const [rotate, setRotate] = React.useState(0);
 | 
			
		||||
  const [offsetX, setOffsetX] = React.useState(0);
 | 
			
		||||
  const [offsetY, setOffsetY] = React.useState(0);
 | 
			
		||||
  const [positionX, setPositionX] = React.useState(0);
 | 
			
		||||
  const [positionY, setPositionY] = React.useState(0);
 | 
			
		||||
 | 
			
		||||
  const resetCamera = () => {
 | 
			
		||||
    setZoom(0);
 | 
			
		||||
    setRotate(0);
 | 
			
		||||
    setOffsetX(0);
 | 
			
		||||
    setOffsetY(0);
 | 
			
		||||
    setPositionX(0);
 | 
			
		||||
    setPositionY(0);
 | 
			
		||||
  }
 | 
			
		||||
  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 },
 | 
			
		||||
      scale,
 | 
			
		||||
      zoom,
 | 
			
		||||
      rotate: rotate / 180 * Math.PI,
 | 
			
		||||
      offsetX,
 | 
			
		||||
      offsetY
 | 
			
		||||
      rotate: -rotate / 180 * Math.PI,
 | 
			
		||||
      positionX,
 | 
			
		||||
      positionY
 | 
			
		||||
    };
 | 
			
		||||
    setPainter(chaosGameCamera(gameParams));
 | 
			
		||||
  }, [scale, zoom, rotate, offsetX, offsetY]);
 | 
			
		||||
  }, [scale, zoom, rotate, positionX, positionY]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
      <>
 | 
			
		||||
@ -64,14 +64,14 @@ export default function FlameCamera({ children }: Props) {
 | 
			
		||||
                   onInput={e => setRotate(Number(e.currentTarget.value))}/>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={styles.inputElement}>
 | 
			
		||||
            <p>Offset X: {offsetX}</p>
 | 
			
		||||
            <input type={"range"} min={-2} max={2} step={0.01} value={offsetX}
 | 
			
		||||
                   onInput={e => setOffsetX(Number(e.currentTarget.value))}/>
 | 
			
		||||
            <p>Offset X: {positionX}</p>
 | 
			
		||||
            <input type={"range"} min={-2} max={2} step={0.01} value={positionX}
 | 
			
		||||
                   onInput={e => setPositionX(Number(e.currentTarget.value))}/>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={styles.inputElement}>
 | 
			
		||||
            <p>Offset Y: {offsetY}</p>
 | 
			
		||||
            <input type={"range"} min={-2} max={2} step={0.01} value={offsetY}
 | 
			
		||||
                   onInput={e => setOffsetY(Number(e.currentTarget.value))}/>
 | 
			
		||||
            <p>Offset Y: {positionY}</p>
 | 
			
		||||
            <input type={"range"} min={-2} max={2} step={0.01} value={positionY}
 | 
			
		||||
                   onInput={e => setPositionY(Number(e.currentTarget.value))}/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {children}
 | 
			
		||||
 | 
			
		||||
@ -3,19 +3,17 @@ export function camera(
 | 
			
		||||
    y: number,
 | 
			
		||||
    width: number,
 | 
			
		||||
    height: number,
 | 
			
		||||
    scale: number,
 | 
			
		||||
    zoom: number,
 | 
			
		||||
    positionX: number,
 | 
			
		||||
    positionY: number,
 | 
			
		||||
    rotate: number,
 | 
			
		||||
    offsetX: number,
 | 
			
		||||
    offsetY: number,
 | 
			
		||||
    zoom: number,
 | 
			
		||||
    scale: number,
 | 
			
		||||
): [number, number] {
 | 
			
		||||
    const zoomFactor = Math.pow(2, zoom);
 | 
			
		||||
 | 
			
		||||
    // Zoom, offset, and rotation are
 | 
			
		||||
    // Position, rotation, and zoom are
 | 
			
		||||
    // applied in IFS coordinates
 | 
			
		||||
    [x, y] = [
 | 
			
		||||
        (x - offsetX) * zoomFactor,
 | 
			
		||||
        (y - offsetY) * zoomFactor,
 | 
			
		||||
        (x - positionX),
 | 
			
		||||
        (y - positionY),
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    [x, y] = [
 | 
			
		||||
@ -23,7 +21,12 @@ export function camera(
 | 
			
		||||
        y * Math.sin(rotate),
 | 
			
		||||
        x * Math.sin(rotate) +
 | 
			
		||||
        y * Math.cos(rotate),
 | 
			
		||||
    ]
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    [x, y] = [
 | 
			
		||||
      x * Math.pow(2, zoom),
 | 
			
		||||
      y * Math.pow(2, zoom)
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Scale transforms IFS coordinates
 | 
			
		||||
    // to pixel coordinates. Shift by half
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
// hidden-start
 | 
			
		||||
import { Props as ChaosGameColorProps } from "../3-log-density/chaosGameColor";
 | 
			
		||||
import { randomBiUnit } from "../src/randomBiUnit";
 | 
			
		||||
import { randomChoice } from "../src/randomChoice";
 | 
			
		||||
@ -10,14 +9,13 @@ import {paintColor} from "../3-log-density/paintColor";
 | 
			
		||||
 | 
			
		||||
const quality = 10;
 | 
			
		||||
const step = 100_000;
 | 
			
		||||
// hidden-end
 | 
			
		||||
 | 
			
		||||
export type Props = ChaosGameColorProps & {
 | 
			
		||||
  scale: number;
 | 
			
		||||
  zoom: number,
 | 
			
		||||
  positionX: number;
 | 
			
		||||
  positionY: number;
 | 
			
		||||
  rotate: number;
 | 
			
		||||
  offsetX: number;
 | 
			
		||||
  offsetY: number;
 | 
			
		||||
  zoom: number,
 | 
			
		||||
  scale: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function* chaosGameCamera(
 | 
			
		||||
@ -29,16 +27,15 @@ export function* chaosGameCamera(
 | 
			
		||||
    palette,
 | 
			
		||||
    colors,
 | 
			
		||||
    finalColor,
 | 
			
		||||
    scale,
 | 
			
		||||
    zoom,
 | 
			
		||||
    positionX,
 | 
			
		||||
    positionY,
 | 
			
		||||
    rotate,
 | 
			
		||||
    offsetX,
 | 
			
		||||
    offsetY,
 | 
			
		||||
    zoom,
 | 
			
		||||
    scale,
 | 
			
		||||
  }: Props
 | 
			
		||||
) {
 | 
			
		||||
  const pixels = width * height;
 | 
			
		||||
 | 
			
		||||
  // highlight-start
 | 
			
		||||
  const imgRed = Array<number>(pixels)
 | 
			
		||||
    .fill(0);
 | 
			
		||||
  const imgGreen = Array<number>(pixels)
 | 
			
		||||
@ -54,7 +51,7 @@ export function* chaosGameCamera(
 | 
			
		||||
    c: number
 | 
			
		||||
  ) => {
 | 
			
		||||
    const [pixelX, pixelY] =
 | 
			
		||||
      camera(x, y, width, height, scale, zoom, rotate, offsetX, offsetY);
 | 
			
		||||
      camera(x, y, width, height, positionX, positionY, rotate, zoom, scale);
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      pixelX < 0 ||
 | 
			
		||||
@ -75,7 +72,6 @@ export function* chaosGameCamera(
 | 
			
		||||
    imgBlue[hIndex] += b;
 | 
			
		||||
    imgAlpha[hIndex] += 1;
 | 
			
		||||
  }
 | 
			
		||||
  // highlight-end
 | 
			
		||||
 | 
			
		||||
  let [x, y] = [
 | 
			
		||||
    randomBiUnit(),
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ some missing features.
 | 
			
		||||
To review, the restrictions we've had so far:
 | 
			
		||||
 | 
			
		||||
> ...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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
use a display range of $[-2, 2]$.
 | 
			
		||||
 | 
			
		||||
Finally, the camera controls available in Apophysis/[`flam3`](https://github.com/scottdraves/flam3/wiki/XML-File-Format)
 | 
			
		||||
have a number of settings that were simply not implemented so far:
 | 
			
		||||
## Parameters
 | 
			
		||||
 | 
			
		||||
For comparison, here are the camera controls available in Apophysis and [`flam3`](https://github.com/scottdraves/flam3/wiki/XML-File-Format):
 | 
			
		||||
 | 
			
		||||
<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
 | 
			
		||||
into specific pixels, and scale is the parameter we use to do it. Specifically, scale is **the number of pixels
 | 
			
		||||
in one unit of the fractal flame coordinate system**.
 | 
			
		||||
Fractal flames normally use the origin as the center of an image. The position parameters (X and Y) move
 | 
			
		||||
the center point, which effectively pans the image. A positive X position shifts the image left,
 | 
			
		||||
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
 | 
			
		||||
<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
 | 
			
		||||
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"`,
 | 
			
		||||
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.
 | 
			
		||||
If the output image changed to `size="1200 1200"`, and scale stayed the same, the output image would have
 | 
			
		||||
a lot more white space. Instead, it's usually better to use a different parameter for controlling image size.
 | 
			
		||||
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 we kept `scale="150"`, the output image would
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
## Offset
 | 
			
		||||
 | 
			
		||||
## Camera
 | 
			
		||||
 | 
			
		||||
With the individual steps defined, we can put together a more robust "camera" for viewing the fractal flame.
 | 
			
		||||
 | 
			
		||||
import CodeBlock from "@theme/CodeBlock";
 | 
			
		||||
 | 
			
		||||
import cameraSource from "!!raw-loader!./camera"
 | 
			
		||||
 | 
			
		||||
<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 FlameCamera from "./FlameCamera";
 | 
			
		||||
 | 
			
		||||
<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.
 | 
			
		||||
		Reference in New Issue
	
	Block a user