diff --git a/blog/2024-11-15-playing-with-fire/4-camera/index.mdx b/blog/2024-11-15-playing-with-fire/4-camera/index.mdx
index 48f3e96..a3ec8a7 100644
--- a/blog/2024-11-15-playing-with-fire/4-camera/index.mdx
+++ b/blog/2024-11-15-playing-with-fire/4-camera/index.mdx
@@ -1,15 +1,14 @@
---
slug: 2025/03/playing-with-fire-camera
title: "Playing with fire: The camera"
-date: 2025-03-07 12:00:00
+date: 2025-03-10 12:00:00
authors: [bspeice]
tags: []
---
Something that bugged me while writing the first three articles on fractal flames were the constraints on
output images. At the time, I had worked out how to render fractal flames by studying
-the source code of [Apophysis](https://sourceforge.net/projects/apophysis/)
-and [flam3](https://github.com/scottdraves/flam3). That was just enough to define a basic camera for displaying
+ [Apophysis](https://sourceforge.net/projects/apophysis/) and [flam3](https://github.com/scottdraves/flam3); just enough to display images
in a browser.
Having spent more time with fractal flames and computer graphics, it's time to implement
@@ -26,33 +25,30 @@ To review, the restrictions we've had so far:
>
> -- [The fractal flame algorithm](/2024/11/playing-with-fire)
-There are a couple problems here:
+First, we've assumed that fractals get displayed in a square image. Ignoring aspect ratios simplifies
+the render process, but we don't usually want square images. It's possible to render a large
+square image and crop it to fit, but we'd rather render directly to the desired size.
-First, the assumption that fractals get displayed in a square image. Ignoring aspect ratios simplifies
-the render process, but we usually don't want square images. As a workaround, you could render
-a large square image and crop it to fit an aspect ratio, but it's better to render the desired
-image size to start with.
-
-Second, the assumption that fractals use the range $[0, 1]$. My statement above is an over-simplification;
-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]$.
+Second, we've assumed that fractals have a pre-determined display range. For Sierpinski's Gasket,
+that was $[0, 1]$ (the reference parameters used a range of $[-2, 2]$). However, if we could
+control the display range, it would let us zoom in and out of the image.
## Parameters
-For comparison, here are the camera controls available in Apophysis and [`flam3`](https://github.com/scottdraves/flam3/wiki/XML-File-Format):
+For comparison, the camera controls available in Apophysis offer a lot of flexibility:

-There are four parameters yet to implement: position, rotation, zoom, and scale.
+The remaining parameters to implement are: position (X and Y), rotation, zoom, and scale.
### Position
-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.
+Fractal flames normally use $(0, 0)$ as the image center. The position parameters (X and Y) move
+the center point, which effectively pans the image. A positive X position shifts left, and a
+negative X position shifts right. Similarly, a positive Y position shifts up, and a negative
+Y position shifts the image down.
-To apply the position, simply subtract the X and Y position from each point in the chaos game prior to plotting it:
+To apply the position parameters, simply subtract them from each point in the chaos game prior to plotting it:
```typescript
[x, y] = [
@@ -63,9 +59,9 @@ To apply the position, simply subtract the X and Y position from each point in t
### 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:
+After the position parameters, we can rotate the image around the (new) center point. To do so, we'll go back to the
+[affine transformations](https://en.wikipedia.org/wiki/Affine_transformation) we've been using so far.
+Specifically, the rotation angle $\theta$ gives us a transform matrix we can apply prior to plotting:
$$
\begin{bmatrix}
@@ -79,7 +75,6 @@ y
\end{bmatrix}
$$
-As a minor tweak, we also negate the rotation angle to match the behavior of Apophysis/`flam3`.
```typescript
[x, y] = [
@@ -90,11 +85,17 @@ As a minor tweak, we also negate the rotation angle to match the behavior of Apo
];
```
+:::note
+To match the behavior of Apophysis/`flam3`, we need to negate the rotation angle.
+:::
+
### 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)$.
+This parameter does what the name implies; zoom in and out of the image. To do this, we multiply
+the X and Y coordinates of each point by a zoom factor. For a zoom parameter $z$, the zoom factor
+will be $\text{pow}(2, z)$.
+
+For example, if the current point is $(1, 1)$, a zoom parameter of 1 means we actually plot $(1, 1) \cdot \text{pow}(2, 1) = (2, 2)$.
```
[x, y] = [
@@ -105,25 +106,32 @@ a zoom of 1 means we actually plot $(1, 1) \cdot \text{pow}(2, 1) = (2, 2)$.
:::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.
+to compensate for the reduced display range.
:::
### 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:
+how many pixels are in one unit of the fractal flame coordinate system, which gives us a mapping from one system
+to the other.
+
+If you open the [reference parameters](../params.flame) in a text editor, you'll see the following:
```xml
```
-This says that the final image should be 600 pixels wide and 600 pixels tall, centered at the point $(0, 0)$,
-with 150 pixels per unit. Dividing 600 by 150 gives us an image that is 4 units wide and 4 units tall.
-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).
+Here's what each element means:
-To go from the fractal coordinate system to a pixel coordinate system, we multiply by the scale,
+- `size="600 600"`: The image should be 600 pixels wide and 600 pixels tall
+- `center="0 0"`: The image is centered at the point $(0, 0)$
+- `scale="150"`: The image has 150 pixels per unit
+
+Let's break it down. Dividing the image width (600) by the image scale (150) gives us a value of 4.
+This means the image should be 4 units wide (same for the height). Because the center is at $(0, 0)$,
+the final image is effectively using the range $[-2, 2]$ in fractal coordinates.
+
+Now, to go from fractal coordinates to pixel coordinates we multiply by the scale,
then subtract half the image width and height:
```typescript
@@ -133,22 +141,23 @@ then subtract half the image width and height:
]
```
-Scale can be used to implement a kind of "zoom" in images. If the reference parameters instead used `scale="300"`,
+Scale and zoom have similar effects on images. If the reference parameters used `scale="300"`,
the same 600 pixels would instead be looking at the range $[-1, 1]$ in the fractal coordinate system.
+Using `zoom="1"` would accomplish the same result.
-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.
+However, this also demonstrates the biggest problem with using scale: it only controls the output image.
+For example, if the output image changed to `size="1200 1200"` and we kept `scale="150"`, it would
+have a display range of $[-4, 4]$. There would be a lot of extra white space. Because the zoom parameter
+has the same effect regardless of output image size, it is the preferred way to zoom in and out.
:::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.
+express all the camera controls as a single transformation matrix. This is important for software optimization;
+rather than applying parameters step-by-step, we can 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.
+They could also be implemented as part of the final transform, but in practice, it's helpful
+to control them separately.
:::
## Camera
@@ -160,9 +169,9 @@ import cameraSource from "!!raw-loader!./camera"
{cameraSource}
-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]$.
+To demonstrate, this display has a 4:3 aspect ratio, removing the restriction of a square image.
+In addition, the scale is automatically chosen so the image width covers the range $[-2, 2]$.
+Because of the 4:3 aspect ratio, the image height now covers the range $[-1.5, 1.5]$.
import {SquareCanvas} from "../src/Canvas";
import FlameCamera from "./FlameCamera";
@@ -171,9 +180,11 @@ import FlameCamera from "./FlameCamera";
## 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.
+The previous fractal images relied on assumptions about the output format to make sure they looked correct.
+Now, we have much more control. We can implement a 2D "camera" as a series of affine transformations, going from
+fractal flame coordinates to pixel coordinates.
-But for this blog series, it's nice to achieve feature parity with the existing code.
\ No newline at end of file
+More recent fractal flame renderers like [Fractorium](http://fractorium.com/) can also operate in 3D,
+and have to implement a more complex camera system to handle the extra dimension.
+
+But for this blog series, it's nice to achieve feature parity with the reference implementation.
\ No newline at end of file