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)
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.
Below are my rough steps
Head
& Leg
head
and legs
(array of instances) in the main canvasdraw
method of head
and legs
. They render on the main canvas separately.Frame
Bubble
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.
There are 12 points I noted below.
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
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.
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.
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)
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;
}
}
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));
}
translate(width/2, height/2);
scale(-1, 1);
translate(-width/2, -height/2);
The hardest part of the entire work. I use the same way as the legs to draw. It’s apparent in a ‘smoke’ version.
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()
.
I created two canvases on one leg using createGraphics
. One for the space behind the frame and one for the front.
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);
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.
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);
Apply this to the Head
, too. It is 30fps now.
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!