33 lines of code
written by cino
I'm still working on my next release (codename "colmenas", no ETA yet) and getting sometimes overwhelmed by it. In these moments, I found out that crafting minimalist generative pieces has a relaxing effect on me, kind of like a zen meditation. Releasing this work as a gift to my collectors felt the right thing to do, as a way of saying thanks for believing and supporting my work.
"33 lines of code" is a bit less minimal in terms of coding than my first attempt, "20 lines of code", but I think the outputs are more varied as well:
let h,a;
function setup() {
colorMode(HSB);
randomSeed(fxrand()*9999999999999);
createCanvas(min(windowWidth, windowHeight), min(windowWidth, windowHeight));
h = random(0,360);
a = fxrand()<0.5 ? random(0.01,1) : 1;
let mono = fxrand()<0.5 ? false : stro();
let d = random(1,50);
let s = fxrand()<0.5 ? random(15,300) : random(300,1000);
let n = random(0.01,1);
let focus = random(0,width);
for (let y=0; y<height; y+=height/(random(30,s))) {
let sat = random(30,100);
if (!mono) {stro()}
for (let x=0; x< width; x+=width/(random(1,s))) {
let d = dist(x,y,focus,y);
fill(color(h,sat,map(d,0,width/2,0,100),a));
rect(x,y,width,height);
sat = adj(sat,0,5,30,100,false);
}
h = fxrand()<n ? adj(h,-d,d,0,360,false) : h;
focus = adj(focus,-width/(d*0.5),width/(d*0.5),width*0.1,width*0.9,true);
}
fxpreview();
}
adj = (c,d1,d2,start,end,rebound) => {
let ac = c+random(d1,d2);
ac = ac<start ? (rebound ? ac+=d2 : ac+=end-start) : ac;
ac = ac>end ? (rebound ? ac-=d2 : ac-=end-start) : ac;
return ac;
}
stro = () => fxrand()<0.2 ? stroke(h,random(50,100),random(50,100),a) : (fxrand()<0.5 ? stroke(0,a) : noStroke());
The setup
let h,a;
Declaring variables h and a. I have to declare them before setup, because I need to use them in my custom functions (you will find them at the end of the script), and if I declared them in the main setup function they would be available just inside it.
function setup() {
Calling the p5js setup function and initializing the script.
colorMode(HSB);
Setting my color mode to HSB (together with HSL, HSB is in my opinion the only choice when it comes to generative art)
randomSeed(fxrand()*9999999999999);
Defining the random seed using fxhash's function based on transaction hashes
createCanvas(min(windowWidth, windowHeight), min(windowWidth, windowHeight));
Creating my canvas based on the window dimensions, so the piece can maintain its quality at any resolution. I chose a square whose sides equal the shortest one between width and height. This way I can avoid both stretching and overflow at different ratios.
The features
h = random(0,360);
Setting the value of my h variable, which will represent my starting hue value (color shade).
a = fxrand()<0.5 ? random(0.01,1) : 1;
Setting the value of my a variable, which will represent my alpha value (opacity). Here I use a ternary operator, which allows me to condense an if/else statement in just one line of code. It reads something like: if the value of fxrand() (which is fxhash random function, returning a value between 0 and 1 based on each transaction hash) is smaller than 0.5 (50% probability), then alpha will be a random number between 0.1 (almost full transparency) and 1 (full opacity), otherwise it will be 1. So I have 50% possibility of a random transparency, and 50% of full opacity.
let mono = fxrand()<0.5 ? false : stro();
Using another ternary operator (you will find plenty in this piece) I declare a new variable, called mono, that I will use to decide whether the stroke (border color of my shapes) should change at every row or not. There is a 50/50 chance, and if the stroke is supposed to stay the same all the time, I use my stro() function (you will find it at the end of the script) to decide what it should be.
let d = random(1,50);
Declaring a new variable, d (for delta). I will use it later to determine how much the hue can change from a row to the next.
let s = fxrand()<0.5 ? random(15,300) : random(300,1000);
Declaring a new variable, s (for step, maybe?). I will use it later in my loop to determine the minimum possible gap between my shapes: will they be thin rows, or thick ones? And how often will the color change on the horizontal axis?
let n = random(0.01,1);
Declaring a new variable, n (for noise). I will use it later to decide the chance that the hue will change from one row to the next.
let focus = random(0,width);
In this piece there is a sort of black smoke line interrupting the color flow, and the focus value indicates the center of the smoke. Here the algorithm decides where it will start on the x axis.
The drawing loop
for (let y=0; y<height; y+=height/(random(30,s))) {
Starting my loop by iterating through the vertical axis, using my s value as a max denominator.
let sat = random(30,100);
For each row, I start by choosing a random initial saturation value between 30 and 100.
if (!mono) {stro()}
Then, if the token is not "mono" (with just one stroke color), I call my stro() function to set the stroke for the current row.
for (let x=0; x< width; x+=width/(random(1,s))) {
Starting to iterate through the horizontal axis, again using my s value as a max denominator.
let d = dist(x,y,focus,y);
I declare a variable called d, (for distance), and set it to the value of the distance between the current point and the value of my focus variable (the center of the smoke). I know I already have another d variable (delta), but I don't use it inside this loop so there is no conflict.
fill(color(h,sat,map(d,0,width/2,0,100),a));
Here I set the color of my next shape: the hue, saturation and alpha are taken from their respective variables, while the brightness depends on the distance from the focus variable.
rect(x,y,width,height);
Finally drawing my shape. It is a rectangle starting at the current x and y and extending all the way to the right and bottom of the canvas.
sat = adj(sat,0,5,30,100,false);
After having drawn the shape, I adjust the value of my saturation variable using a function I created and that you will find below. Basically every horizontal step I am increasing it by a value between 0 and 5, and if it exceeds 100 it will go back to 30.
}
Closing the loop through the x axis, thus moving to the next row.
h = fxrand()<n ? adj(h,-d,d,0,360,false) : h;
Adjusting the value of my hue variable, again using a ternary operator. There is a chance that the hue stays the same, and the probability is determined by the noise variable I set earlier. If it doesn't stay the same, it again uses my adj() function: it can change from minus delta to plus delta (the d variable I defined earlier) and if it goes below 0 or above 360 it will reset.
focus = adj(focus,-width/(d*0.5),width/(d*0.5),width*0.1,width*0.9,true);
I also adjust the value of my focus variable to create that smoke effect. I once again use my adj() function: from a minimalist perspective I'm happy I managed to use a single function to interact with three different variables, each responding to its different rules.
}
Closing the loop through the y axis.
fxpreview();
Telling fxhash to take the screenshot for the preview.
}
End of my setup function and of my actual piece. Below here there are only the definitions of the two functions I used.
The functions
adj = (c,d1,d2,start,end,rebound) => {
The first one is my main function, called adj(). It takes 5 arguments:
- c defines the variable I want to adjust (originally it was meant to be color , hence the c, but later I expanded the scope of the function without bothering to change it)
- d1 is the minimum adjustment
- d2 is the maximum adjustment
- start is the minimum possible value
- end is the maximum possible value
- rebound determines if the value should rebound or reset upon reaching start or end. I set it to true (rebound) for focus , and false (reset) for hue and saturation .
let ac = c+random(d1,d2);
I declare the variable ac, which stands for adjusted c, and I set it to c plus a random value between d1 and d2.
ac = ac<start ? (rebound ? ac+=d2 : ac+=end-start) : ac;
If ac goes below start, it can either rebound or reset. Here I use a nested ternary operator, which reads something like this: is ac smaller than the start value? If so, is rebound set to true? If both answers are yes, then have ac bounce back by d2. If the first answer is a yes but the second is a no, then have ac increase by the max value (for example if the hue goes to -10, increase it by 360 so it gets to 350). Finally, if both answers are no, just keep it like it is.
ac = ac>end ? (rebound ? ac-=d2 : ac-=end-start) : ac;
Same as above, but this time in case ac exceeds the end value.
return ac;
Passing the value of ac as the result of the adjustment.
}
End of the adj() function.
stro = () => fxrand()<0.2 ? stroke(h,random(50,100),random(50,100),a) : (fxrand()<0.5 ? stroke(0,a) : noStroke());
My second function sets the stroke (border color) of my shapes. I used arrow functions for this piece specifically because if they have just one statement I could omit the return command, thus allowing me to save a line of code. What I say here with yet another nested ternary operator is that if fxrand() is less than 0.2 (20% probability), then the stroke will have the hue value set by my h variable and random saturation and brightness. Otherwise, it will have a 50% chance of being black, and a 50% chance of being non-existent.
That's it, I hope you enjoyed my piece and explanation, I'm really glad how this piece turned out and will for sure experiment more with minimalist coding in the future (also as a mean of self-care)!