Making of the generative octopuses
0xF7E1
March 14th, 2022

中文版 日本語版

Hi, I’m eziraros. I started making NFT on fxhash this year.

This article documents part of the development process of my latest drop, The generative octopuses. Please check the link before continuing to read.

(This project is done with p5.js)

Before we start, Why an octopus?

I watched a video on Vogue’s Youtube channel that introduced a French actor, Mathilde Warnier, and I found her account on Instagram, came across this photo. (It’s an octopus. Since I don’t have the copyright of this photo, I didn’t paste it here.)

It strikes me that I should draw an octopus with code.

After googling several images and storing them in the beautiful app Milanote, I started to make it.

Milanote is an excellent tool to collect ideas, organize information
Milanote is an excellent tool to collect ideas, organize information

Development process

Below are my rough steps

It was the front face at the beginning.
It was the front face at the beginning.
  1. Start by drawing on paper: I drew a basic octopus to check the structure and found the intersection between body and legs could be the center point.
  2. Implementation:
    1. Initialize the main canvas
    2. Implement Head & Leg
    3. Initialize them as head and legs (array of instances) in the main canvas
    4. Call the draw method of head and legs. They render on the main canvas separately.
    5. Implement Frame
    6. Implement Bubble
    7. Make presets

A note for those who are not familiar with this type of generative art

The piece is drawn frame by frame. In other words, the result is not pre-generated before drawing, but placing the latest result of the real-time calculation on the previous results.

Take the gif below as an example:

If I cleared the previous result on each frame, we get this.

Problems & solutions

There are 12 points I noted below.

1. Make legs have “z-index”

I use createGrphics to make every leg has its canvas(p5.Graphics) to render. And place it on the main canvas after rendering.

so the draw function in the main canvas looks like this:

for (let i = 0; i < legs.length / 2; i++) {
  legs[i].draw();
  legs[legs.length - 1 - i].draw();
  if (i == 0) {
    head.draw();
  }
}
bubbles.do('draw'); // 'do' is my custom function for array
the layers (looks like Damien Hirst’s shark)
the layers (looks like Damien Hirst’s shark)

2. Drawing Outlines

I made two classes to draw one leg. Leg and LegNode. A Leg contains the latest position, direction, size (leg width) and several LegNodes. It generates a LegNode on every N frames, with the position, direction, and size of the frame. Connect the common external tangents of LegNodes to get the outlines.

the red points are the center of LegNodes
the red points are the center of LegNodes

To make the outlines looks more freehand. Instead of line, I drew 20 circles between two endpoints with random size and opacity. I learned this trick from Orr Kislev. (Thanks, Orr Kislev!) He made an excellent animation to explain this.

3. Suckers position and rotation

The biggest rule is “draw inside”. For example, if isRightLegs (index < 4) && moving to the right (direction.x > 0), I draw them at the bottom. Moving down, draw left. But I don’t draw them while moving up or left.

I set an attribute called revealRate. It increases/decreases depending on the frameCount. It changes suckers’ size and position. Sucker’s rotation is following the leg’s direction. I draw ellipses with scale(1, n) and circle. It’s much easier to control. (compares with ellipse function)

4. The pose

The Leg has three attributes: position(vec2), direction(vec2), rotation(float). rotation changes slightly randomly on every ten frames, and then added to direction. Normalized direction added to position.

if (frameCount % 10 == 0) {
  this.rotation += fxRandom(-1, 1);
}
this.direction.rotate(this.rotation).normalize();
this.position.add(this.direction);

I push rotations into an array. I can check if the leg keeps the same direction while rotating. If so, I can reverse it.

const n = 50; // the length of cache

this.rotationCache.push(this.rotation);

if (this.rotationCache.length > n) {
  this.rotationCache.shift();
}

if (this.rotationCache.every(d => d > 0) || this.rotationCache.every(d => d < 0)) {
    this.rotation *= -1;
  }
}
check the rotations with different array length
check the rotations with different array length

5. The texture

It’s drawn with countless small rounds. circle(x, y, size) is replaced with my custom function circleNoise(x, y, size). It draws dozens of small rounds inside the circle randomly. I can get different results by changing the proportion of small rounds with different brightness.

for (let i = 0; i < dotLength; i++) {
  radius = fxRandom(0, this.size / 2);
  x = fxRandom(-1, 1) * radius;
  y = Math.sqrt(radius * radius - x * x, 2) * fxRandomSign();
  pg.circle(center.x + x, center.y + y, fxRandom(2, 3));
}
spotted version
spotted version

6. Changing the face direction

translate(width/2, height/2);
scale(-1, 1);
translate(-width/2, -height/2);

7. Head

The hardest part of the entire work. I use the same way as the legs to draw. It’s apparent in a ‘smoke’ version.

#85 https://www.fxhash.xyz/gentk/505131
#85 https://www.fxhash.xyz/gentk/505131

8. Frame

Frame 1
Frame 1

The Frame class has x, y, width, height, frameWeight. And there is a param called depth in the config. (0 = block all, 8 = reveal all). I convert the leg index to leg depth to check if the leg is blocked or not.

In frame 1 mode, stop render if it was blocked && outside the frame.

In frame 2 mode, I erase the pixels with erase().

Frame 2
Frame 2

9. Make leg able to move to the front

I created two canvases on one leg using createGraphics. One for the space behind the frame and one for the front.

look! the middle leg!
look! the middle leg!

10. The bubbles

The bubbles are not circles or ellipses. Instead, I draw them with random decagons. I think that looks more organic.

canvas.beginShape();

for (let i = 0; i < 10; i++) {
  radius = this.size * fxRandom(1, 1.4);
  angle = i / 10 * 360;
  x = canvas.sin(angle) * radius;
  y = canvas.cos(angle) * radius * this.compressY;
  canvas.vertex(x, y);
}

canvas.endShape(canvas.CLOSE);

11. Color palettes

I used to pick up colors with a random function. (e.g. fxRandom(360) to pick up a hue of the main color, and then plus/minus 120 or 200 to get another hue as secondary.) But this time, I hand-picked the hues.

I created several categories.

const categorySettings = [
  { name: CATEGORY_BLACK },
  { name: CATEGORY_BLACK2 },
  { name: CATEGORY_WHITE },
  ...
];

And then pick some hues.

const hueNameMap = [
  {
    name: COLOR_YELLOW,
    hue: 45,
    briBias: 0,
  },
  {
    name: COLOR_BLUE,
    hue: 210,
    briBias: 0,
  },
  //....
];

Generate different versions with hue setting

hueNameMap.forEach(m => {
  let hue = m.hue.length > 1 ? fxRandom(m.hue[0], m.hue[1]) : m.hue;

  // pushPreset is a custom function, first param is the name of target category
  pushPreset(CATEGORY_BLACK, {
    name: capitalize(m.name),
    hue,
    briBias: -50,
    noiseBias: -1,
    sat: 0,
    bgColor: [hue, 10, 80],
    bubbleColor: [0, 0, 100, .5],
    frame2Color: ['#fff'],
  });
});

hueNameMap.forEach(m => {
  pushPreset(CATEGORY_SMOKE, {
    name: capitalize(m.name),
    hue: m.hue.length > 1 ? fxRandom(m.hue[0], m.hue[1]) : m.hue,
    briBias: 0 + m.briBias,
    noiseBias: -2,
    opacity: .06,
    sat: 25,
    bgColor: [0, 0, 20],
    fgColor: [0, 0, 20],
    bubbleColor: ['#fff'],
    frameColor: ['#fff'],
    satFade: -.5 + (m.satFadeBias || 0),
  });
});

Finally, I got 106 presets.

12. Performance tuning

The worst-case was below 10fps. I tweaked the performance by updating only the changed pixels.

Every leg has a pixelArea.

this.pixelArea = {
  x: this.pos.x,
  y: this.pos.y,
  width: 20,
  height: 20,
};

While Leg is moving, keep checking if the current point is inside pixelArea. If not, update the pixelArea. Finally, get the pixels inside pixelArea and place them on the main canvas.

before:

masterCanvas.image(this.canvas, 0, 0);

after:

this.pixel = this.canvas.get(pa.x, pa.y, pa.width, pa.height);
masterCanvas.image(this.pixels, pa.x, pa.y);
red is the drawing area for the space behind the frame
red is the drawing area for the space behind the frame

Apply this to the Head, too. It is 30fps now.

Postscript

As a generative art creator, I am not the type that will think clearly before making something, but I prefer to let the generated results lead me. Like a journey. (so the code get messy sometimes.) The above solutions may not be the best solutions to achieve the octopus, but anyway, it’s a way to achieve it. I hope this will be helpful to you.

By the way, the floor price in the secondary market is lower now. It’s time to pick one!

Arweave TX
tH7LTg0zh8gVlLMh6OOUEFshWdaS_8NwkgrZYl95dxo
Ethereum Address
0xF7E15015D31e1Be374c21E6F1dE91147C8B5db88
Content Digest
tEL7_taJEbIiA4rcfZnOvHDRqnKzV2haRnnVV2YFU94