Burning ship fractal

If we plot the escape times for points in this complex plane series, we end up in a world of burning ships.

$z_{n+1} = (|Re(z_n)| + i|Im(z_n)|)^2 + c, \quad{i = \sqrt{-1}}$

The largest ship is located on the real axis at -1.75.

World overview

If we consider an escape radius of 2, the world also has a radius of 2, since any points starting outside of the world diverge right away.

The ships are located near the real axis (y = 0) among negative real numbers.

Zooming in closer to the ships in the bottom left

The ship near -1.6 between the largest ship and the dark expanse.

Small ships near the edge of darkness

Zoomed in to the small ship near -1.57

Small ship and disorder near the expanse, with curving spires.

Left of the largest ship are some very small ships. The light ends at (-2, 0)

Zoomed in to a small ship near -1.94

Burning ship fractal equation

The equation using complex numbers is this:

$z_{n+1} = (|Re(z_n)| + i|Im(z_n)|)^2 + c$

It's similar to the Mandelbrot equation:

$z_{n+1} = z_n^2 + c$

Except instead of squaring each number, we add the absolute values of the real and imaginary components together during each iteration and square the sum instead.

z is a complex number composed of real and imaginary numbers:

$z = x + iy, \quad{i = \sqrt{-1}}$

Fractal images are generated by changing the value of c:

$c = (x_0, y_0)$

If we convert to using x and y, the burning ship fractal equation reduces to::

$x_{n+1} = x_n^2 - y_n^2 + x_0$
$y_{n+1} = 2|x_ny_n| + y_0$

A common way of exploring fractals is by putting different starting (x0, y0) numbers into the equation and making note of whether the series:

• diverges to infinity or stays within a limit
• if the series diverges, we make note of:
• the number of iterations until reaching the escape radius
• the distance from zero (0, 0) during the escape

Fractal coloring

Escape time: given a starting point (x0, y0), we can give each point a color by calculating the number of iterations it takes for the series to diverge, and the distance from zero when escaping.

const ESCAPE_THRESHOLD = 4;
const MAX_ITERATIONS = 255;

function iterateUntilEscape(x0, y0) {
let x = 0, y = 0;
let numIterations = 0;
while ((x*x + y*y < ESCAPE_THRESHOLD) &&
(numIterations < MAX_ITERATIONS)) {
const x_next = x*x - y*y + x0;
const y_next = 2*Math.abs(x*y) + y0;
x = x_next;
y = y_next;
numIterations++;
}
return [numIterations, x*x + y*y];
}

We'll set the RGBA colors of every pixel in the image based on the number of iterations. Here's an example of a fiery yellow/red color palette by scaling just the green and alpha values of each pixel.

function getColor(numIterations) {
return [
255,                // red
numIterations * 7,  // green
0,                  // blue
numIterations * 15  // alpha
];
}

Here's a general function for drawing fractals onto canvas elements. Changing the iterateUntilEscape function will change the drawn fractal.

function drawFractal(canvasEl, xRange, yRange, getColor) {
const canvasWidth = canvasEl.width;
const canvasHeight = canvasEl.height;
const context = canvasEl.getContext('2d');
const canvasImageData =
context.createImageData(canvasWidth, canvasHeight);
const image = canvasImageData.data;
for (let i = 0; i < canvasHeight; i++) {
for (let j = 0; j < canvasWidth; j++) {
const x0 = xRange[0] +
j*(xRange[1] - xRange[0]) / canvasWidth;
const y0 = yRange[0] +
i*(yRange[1] - yRange[0]) / canvasHeight;
const [numIterations, escapeDistance] =
iterateUntilEscape(x0, y0);
const pixelIndex = 4*i*canvasWidth + 4*j;
if (numIterations !== MAX_ITERATIONS) {
const mu = getMu(numIterations, escapeDistance);
const pixels = getColor({ numIterations, mu });
for (let p = 0; p < 4; p++) {
image[pixelIndex + p] = pixels[p];
}
}
}
}
context.putImageData(canvasImageData, 0, 0);
}

Here's the largest ship colored based on number of iterations alone.

By considering escape distances on a logarithmic scale, we can use this extra information to smooth the color transitions.

const ESCAPE_RADIUS = 2;

function getMu(numIterations, escapeDistance) {
return numIterations + 1 -
}

function getColor(mu) {
return [ 255, mu * 7, 0, mu * 15 ];
}

Here's the same image as above colored with renormalized iteration counts.

Nebulabrots are a way of rendering fractals where pixels are colored based on how frequently they're visited while calculating the iterations until escape across the viewport.

Sub-structures

Zoomed-in to various parts of the fractal world.

The mast of the largest ship

Distant view when approaching the expanse along the real axis near x = -1.5

The left edge of the world near x = -2.0

All fractals on the page are interactive and rendered when they're in view.

Fractal image controls

• shift + scroll mouse wheel when hovering to zoom in/out
• click and drag to pan the view

TODO

• Enable undo'ing the last camera action with cmd-z
• Enable customizing colors
• Smoother rendering as camera views pan
• Optimize overall rendering performance
• Enable resetting each canvas to its original state
• Prevent page from scrolling while zooming
• Allow editing x and y ranges: text inputs or click/drag slider
• Maybe show the (x0, y0) center of each canvas
• Draw boxes when selecting parts of the canvas to zoom in on
• Shift-drag to maintain the aspect ratio of the camera during zoom

Source code