33 lines of code

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)!

::tezos-storage-pointer[]{contract="KT1BJC12dG17CVvPKJ1VYaNnaT5mzfnUTwXv" path="ledger::19419" data_spec="FX-GEN-DATA-002"}