3f164ec6eba0a20000-87433af5e671b8000-468eb9aed731a00000-31a68a146eb7ea0000

Ammonites walkthrough

Following the unexpected success of the Ammonites NFT on fxhash, several people have asked me to give more information about my approach for producing this type of generative pieces.

So here it is, I’m going to take this image and deconstruct it for you, step by step.

1. The easy part first – the background paper

The background canvas is generated with noise and random vertical / horizontal lines. With exaggerated contrast and in greyscale, it looks like this:

The code for generating these is fairly simple:

function noise_pattern(layer, fg,bg, N, ll)
{
	layer.fill(bg);
	layer.stroke(fg);
	layer.strokeWeight(1.0 / layer.width);
	layer.rect(-1, -1, 2, 2);

	ll = ll / layer.width * (1.0+rnd());
	for (let i = 0; i < N; i++)
	{
		let x1 = rrnd(-1 + ll, 1 - ll);
		let y1 = rrnd(-1, 1);
		let x2 = rrnd(-1, 1);
		let y2 = rrnd(-1 + ll, 1 - ll);
		
		layer.circle(x1, y1,ll);
	}
}

function canvas_pattern(layer, fg, bg, N, ll)
{
	layer.fill(bg);
	layer.stroke(fg);
	layer.strokeWeight(1.0 / layer.width);
	layer.rect(-1, -1, 2, 2);

	ll = ll / layer.width * (1.0+rnd());
	for (let i = 0; i < N; i++)
	{
		let x1 = rrnd(-1 + ll, 1 - ll);
		let y1 = rrnd(-1, 1);
		let x2 = rrnd(-1, 1);
		let y2 = rrnd(-1 + ll, 1 - ll);
		layer.line(x1 - ll, y2, x1 + ll, y2);
		layer.line(x2, y1 - ll, x2, y1 + ll);
	}
}

backgroundLayer.background('#b08a65');
canvas_pattern(backgroundLayer, color('#00000008'), color('#b08a65'), 1000, 100);
noise_pattern(backgroundLayer, color('#00000004'), color('#00000004'), 10000, 10);
noise_pattern(backgroundLayer, color('#00000004'), color('#00000004'), 20000, 4);

Note that I normalize my canvas to -1..1 units, instead of using pixel units.

If you look closely at the frame lines, you will notice they are not perfectly straight, but are a little shaky, to imitate the feel of hand drawn lines. This is the kind of tiny detail that came in late in the development, but I feel is extremely important to add that final polish. The shaky line is drawn by subdividing the whole line in 100 segments, and applying a tiny bit of randomization to each segment point.

2. Drawing spirals

The general formula for a spiral shape is well-known, and looks like (in polar coordinates)

r = e^(b θ) – different values of b give different spiral growths.

Next, we can scale the spiral to define the inner and outer shell locations. If they overlap, it’s a classic nautilus structure. If they don’t, we get an unrolled shell.

3. Shell subdivisions

Ammonites are made of consecutive chambers. Another parameter I introduced is the “number of chambers per rotation”, i.e, how many chambers are in a 2π turn around the spiral. Here is an example with RotationSteps = 7.

This number doesn’t have to be a integer, as a matter of fact non-integer values produce more complex patterns that are visually interesting. Here is one with RotationSteps = 8.5

and our image at the top of this page, stripped down to the bare essentials, looks like this (RotationSteps = 13.58)

4. Outer spiral decoration

Now that our shell is defined with an outer and inner edge, we can add decorations (spikes, knobs, ridges, …) to the outer edge.
For each chamber we have an index that goes from 0 to 1, and a ‘decoration function’ that produces the various decoration types.

For example here is a function that produces a spike in the middle of the chamber:

function addMiddleSpike(x, w, amount)
{
	if (x < 0) x = 0;
	else if (x > 1) x = 1;
	if (Math.abs(x - 0.5) < w)
	{
		var t = (1.0 - Math.abs(x - 0.5) / w);
		return t * t * t * amount;
	}
	return 0;
}

The various functions for producing the decorations are fairly simple, have quite a few parameters, and when combined together, provide a huge variety of designs with a lot of room for randomization – ideal for generative designs !

Here is our initial image, with its outer decoration applied:

5. Line weight control

This step is so crucial that I feel it deserved a paragraph on its own. Traditional artists (working with ink) add a lot of variation to their lines thickness. So far our drawing looks very flat, so let’s make the outer spiral lines thicker at the bottom, and thinner at the top, and do the opposite for the inner spiral lines:

It’s a subtle difference, but all these little details compound in the end !

6. Drop shadows

This one is rather straightforward, I used the HTML canvas drop shadow features. P5.js doesn’t expose these, so we need to work with the device context directly. I vary the alpha of the shadow based on the distance from the center, and only draw the shadows for the last turn of the spiral.

const stepThreshold = RotationSteps * 2 | 0;
drawingLayer.drawingContext.shadowBlur = 24;
drawingLayer.drawingContext.shadowOffsetX = 3; 
drawingLayer.drawingContext.shadowOffsetY = 6; 
var shadowAlpha = (step > stepThreshold) ? 10 * (step - stepThreshold) : 0;
if (shadowAlpha > 200) shadowAlpha = 200;
drawingLayer.drawingContext.shadowColor = color(0, 0, 0, shadowAlpha);

7. Spiral shadow

Let’s add some sense of volume to our otherwise flat spiral, by drawing some more shadows around the edges. I basically walk around the spiral, drawing almost transparent circles with a brush that increases in size.

function drawShadows(nsteps)
{
	drawingLayer.push();
	drawingLayer.noStroke();
	drawingLayer.fill(color('#00000002'));

	var brushSize1 = function (x)
	{
		return 0.001 + 0.05 * x * x * x * x * x;
	}
	var brushSize2 = function (x)
	{
		return 0.001 + 0.2 * x * x * x * x * x;
	}
	draw_function_with_circle_brush(drawingLayer, function (x)
	{
		return outerSpiral(stepToAngle((nsteps - 0.1) * x)).smul(0.99);
	},
		brushSize1, 500, 100, 500);
	draw_function_with_circle_brush(drawingLayer, function (x)
	{
		return outerSpiral(stepToAngle((nsteps - 0.1) * x)).smul(0.92);
	},
		brushSize2, 500, 100, 499);

	draw_function_with_circle_brush(drawingLayer, function (x)
	{
		return innerSpiral(stepToAngle((nsteps - 0.1) * x)).smul(1.01);
	},
		brushSize2, 200, 50, 199);

	drawingLayer.pop();
}

and here is the end result:

8. Suture patterns

Ammonites have decorative suture patterns, which are textural lines separating the chambers. There are many different types of suture patterns, and I wish I had spent more time on these, as some of the patterns on the actual fossils are exquisitely intricate. I used simple parametric equations to draw 2d curves along the radial axis. Having several equations allowed me to add more entropy to the collection.

Here is our picture, with its suture patterns drawn.

9. Contour hatching

That was the hardest part to get right. I added hatching in 2 directions, first around the outer edge:

and then radially, using a variable alpha so the radial lines are transparent in the center:

Finally I also added a second radial pass, using white highlights, with alpha fading to transparent towards the edges:

10. Radial lines accents

I also added a feature to accent every N chamber separation. In our example N = 3, and it looks like this with the accents added:

11. Closing the shell

Here I close the shell by drawing a symmetrical version of the final radial line, and filling the gap with a dark color. This part was incredibly hard to get right in all cases, as the geometry around the closure can be pretty complicated with spikes and ridges… I ended up drawing the full shape on a different layer, and then using P5.js erase() mode to remove the part from the previous turn that should still be visible.

11. Magic blend trick for extra contrast

Finally, to add more contrast to the final image, I used P5.js blend() in SOFT_LIGHT mode, blending the image onto itself. This gave it this extra pop you can see in the picture at the top of the page.

12. Rare “ammolite” version

Some ammonites are made of an opal-like mineral that’s absolutely beautiful. I tried to recreate the effect in a very small (~4 %) number of editions. I picked colors from actual ammolite stone, and blended them over the shell contour with variable transparency and brush sizes.

I think it’s nice to add rare features like this, as people who mint one of these get really excited and generate a lot of buzz.

13. Closing notes

In conclusion, for this type of illustrations I believe that the devil is really in the details. All these little things that you can barely see individually, all add up to elevate the piece.

I had a lot of fun designing this, even though towards the end, I was seeing spirals in my sleep !

For any questions, hit me up on Twitter or FxHash discord.