Midterm – Making Generative Pollack Paintings

Jackson Pollack was a painter during the abstract expressionist movement known for his paint drip paintings. He would splash paint onto a surface using the force of his entire body and painted from every angle. Due to him using his body to propel the paint, this combined with the use of colors create strongly emotional paintings where every drop of paint has deep emotion and meaning behind it.

As someone who enjoys abstract art, Pollack was one of the first abstract artists I was exposed to. His paintings hold special meaning to me due to this. When thinking of a generative art piece to create, I knew I wanted to do something more abstract, and I felt that using layers of randomly generated lines to emulate a Pollack painting would be a really interesting idea. My code randomly generates layers of lines and blobs that are place on top of another. The colors are semi random, some are lighter/darker shades of a randomly chosen color, while others are simply black or white. Shown below are some of the pictures that were generated, along with a pen-plotted image time-lapse video.

Video: https://drive.google.com/file/d/1zT2N_VmV7RY4kdVUCM4nddNGrpFSZkek/view?usp=sharing

There was much trouble using the pen plotter. First, since the lines are individual segments, it was really difficult to modify the layers of the SVG file. This is one of the reasons why I changed the line algorithm which will be explained later. Once this was fixed, I was now worried about how long the plotter would take to finish, and the fact that I had to constantly change the color of the pen to emulate the different colors. The colors did not necessarily work as I wanted, as the black would often drown out the other colors, making the colors look less layered. There was also a limitation regarding the thickness of the pens, which meant I could not really show the extremely varying widths of the lines on the plotter. I had made a separate program specifically to make my program work with the plotter, which can be seen here. Finally, I had issues relating to the plotter, which would malfunction at times and jerk downwards, messing up the home position. This meant it occasionally would draw outside the paper, and I would have to pause and reset the positioning often to make the plotter work again. While it is not perfect, it came out nice enough for what I wanted.

Sketch:

The code features an algorithm that will generate lines and blobs layer-by-layer. It first chooses a random color for the first layer. From there, it has a 65% to make the current color slightly lighter or darker. The other 35% chance has a 50% chance to choose a new color, or a 50% chance to choose white or black. The final two layers will always be white then black, respectively. The reason I chose to make the colors sometimes black or white is because I felt like using those colors more made the paintings have a more similar color scheme to Pollack’s. I also found it more aesthetically pleasing to look at, which is also why I did this. The reason for having a bias towards hues was because it also makes the piece more aesthetically pleasing to look at, since having only randomized colors created ugly and less coherent paintings.

The lines of the painting are generated in small segments. Each point will rotate slightly over sin for x and cos for y in a random direction, making the lines sort of snake around the canvas. When they are too far out of bounds, the lines will change course to ensure it remains visible. The newly created point is then connected to the previous point. There is a low chance for the algorithm to also randomly end the line and choose a new random starting place, so that it’s not all just one line. The line will also vary in its thickness from point to point, and new lines will have a more varying thickness. Once the lines of one layer have finished, it will then generate a random amount of paint blobs in the same color. These are made similarly, except that the points will move in a circular way so that it creates an irregularly shaped circle. Once this is finished, the code will start the next layer and repeat this until it finishes. At any time you can press a button labeled “Generate” to clear the canvas and start a new painting.

I am particularly proud of the Drawer object, the one that makes the lines. I based it off of this reference here, and then modified it to make it an object that I could call instead. I also slightly changed the way it draws, because the reference code uses line() instead of curveVertex(). The reason I changed this was because I found that using lines made the program really slow and it made it difficult to generate a lot of layers, which I wanted. When I changed to curveVertex(), the program worked much faster and was actually able to complete the painting relatively quickly. The code for the drawer object can be seen here:

class Drawer {
  //drawer class, this draws all the lines
  constructor() {
    this.seg = 0;
    this.swScale = 0; //line width multiplier
    this.sw = width * random(0.0002, 0.005); //choose random line width
    this.angVary = PI * 0.02; //how much the line varies its path
    this.lineLength = height * 0.001; //length of each segment
    this.x = 0;
    this.y = 0;
    this.setXY(); //choose random starting position
  }
  setXY() {
    //choose random starting point, angle, and line width
    this.x = round(random(width * 0.1, width - width * 0.1));
    this.y = round(random(height * 0.1, height - height * 0.1));
    this.ang = random(PI * 2);
    if (random(2) < 1) {
      //randomly vary the angle a bit more
      this.ang = PI * 0.25;
    } else {
      this.ang = PI * 0.75;
    }
    this.sw = width * random(0.0002, 0.005); //set width
  }
  makeLines(seg, swScale) {
    //line drawer. creates a small segment that will connect and form a line. once in a while, end the line and start randomly in a new place
    this.seg = seg;
    this.swScale = swScale;
    beginShape();
    for (let i = 0; i < this.seg; i++) {
      this.ang = this.ang + random(-this.angVary, this.angVary); //randomly choose a direction for the line to go
      this.x = this.lineLength * sin(this.ang) + this.x; //add angle to xy coords
      this.y = this.lineLength * cos(this.ang) + this.y;
      if (
        width * 0.1 * sin(this.ang) + this.x > width + width * 0.05 ||
        width * 0.1 * sin(this.ang) + this.x < 0 - width * 0.05 ||
        height * 0.1 * cos(this.ang) + this.y > height + height * 0.05 ||
        height * 0.1 * cos(this.ang) + this.y < 0 - height * 0.05
      ) {
        //if the next segment will go too far out of bounds, have it turn so it doesnt do that
        this.ang += 0.2;
      }
      this.sw += width * random(-0.00005, 0.00005); //make the line width get slightly smaller/larger for each segment
      this.sw = constrain(this.sw, width * 0.0001, width * 0.009); //make sure its not too big or small
      strokeWeight(this.sw * this.swScale); //apply line width and scaling
      curveVertex(this.x, this.y); //connect the new point to the previous one
      if (random(1000) < 1) {
        //once in a while, stop the line and start a new one
        this.setXY();
        endShape();
        beginShape();
      }
    }
    endShape();
    this.reset();
  }
  reset() {
    //reset the position
    this.setXY();
  }
}

Trying to understand this code and optimize it was definitely the most difficult part. It took much trial and error and tinkering with numbers to make the program work in a way that I liked it. This and making the colors look aesthetically pleasing were the two hardest parts for me. It took a lot of trial and error with the coloring to make sure it looked nice.

Some parts I would try to improve is make the code show itself drawing out the lines. I had this originally, but the program would slow down too much and would never be able to finish a painting because of the speed. Also, when using curveVertex(), the entire shape is made at once meaning it’s impossible to show this anyway. I would have to go back to using line(), which is really slow and inefficient. I also would want to add more user interaction, adding ways to modify parts of the paintings like how many layers, color schemes, or how many lines or blobs are drawn. I was having a lot of trouble adding these, so I decided against adding them, so all that’s left is the generate button. One last problem I have is that the lines seem to draw more towards the right, meaning sometimes the left side will have more empty space. I wasn’t sure at all how to fix this issue. These would be the things I would try to add for future iterations of the program.

Midterm Project – Spring Rain

Embedded Sketch and Link


https://editor.p5js.org/ss14740/sketches/HiD0F6V9R

Exported Images 

Concept and Artistic Vision

At first, I was inspired by the concept of identity and was attempting to create fingerprint patterns. However, I couldn’t get the shape to look the way I wanted to. Here is my sketch.

Although I am proud of the work I put into the fingerprints, but they did not have the organic shape I wanted, and I couldn’t figure out how to generative movement while still making the sketch feel natural and organic. In brainstorming other ideas, I revisited my other sketches for this class. Many of my sketches were of natural elements. I think it’s interesting to use computation to create more fluid, nature-related objects. So, I re-imagined my midterm to show rain falling on a lake, and the creation of the ripples to be the main artistic element of my sketch. Here are some of the images I was inspired by.

Luckily, I was able to draw from my previous draft in creating the new sketch, as both shared the same logic in creating the circular shapes and in modifying these shapes with Perlin noise.

Coding Translation and Logic 

I planned the sketch by first modifying my fingerprint sketch to create the ripples. I created a class for the ripples and a class for the raindrops, as these were my main two elements

class Raindrop {
  constructor(x, y) {
    this.pos = createVector(x, y); // Initial position
    this.size = 15; // Size of the raindrop
    this.speed = random(initialRaindropSpeedRange[0], initialRaindropSpeedRange[1]); // Random speed
    this.acceleration = 1.2; // Acceleration (gravity)
    this.targetY = random(height / 6, height); // Random point at which raindrop will stop
  }

  update() {
    this.speed += this.acceleration; // Increase speed due to acceleration (gravity)
    this.pos.y += this.speed; // Move the raindrop down by updating its y-coordinate

    // If mouse is pressed, make the raindrop fall faster
    if (mouseIsPressed) {
      this.speed = random(10, 15);
    } else {
      this.speed = constrain(this.speed, initialRaindropSpeedRange[0], initialRaindropSpeedRange[1]);
    }
  }

  display() {
    fill(201, 243, 255, 100); // Light blue, semi-transparent fill
    stroke(255); // White stroke
    strokeWeight(0.2); // Thin stroke weight

    // Draw a bezier curve to represent the raindrop's shape
    beginShape();
    vertex(this.pos.x, this.pos.y);
    bezierVertex(this.pos.x - this.size / 2, this.pos.y + this.size, this.pos.x + this.size / 2, this.pos.y + this.size, this.pos.x, this.pos.y);
    endShape(CLOSE);
  }

  hitsBottom() {
    return this.pos.y >= this.targetY; // Checks if the raindrop has reached the target Y position
  }
}

In the Raindrop class, I initialized all the elements to the raindrop. I made the speed random and added acceleration as each raindrop falls so it creates a more natural effect. When the mouse is pressed, the raindrops fall faster, creating a rainstorm-like effect. The shape of the raindrops are created with bezierVertex so that they look fluid. If the raindrop returns true that it hit the ‘ground’, then a ripple is created.

class Ripple {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.size = 0;
    this.alpha = 230; // Initial transparency
    this.growthRate = 2; // How fast the ripple grows
    this.fadeRate = 4; // How fast the ripple fades
    this.layers = 6; // Number of ripple layers
    this.wideningFactor = 1.1; // Factor to widen the ripple's oval shape
  }

  update() {
    // Increase ripple size
    this.size += this.growthRate;
    // Decrease transparency so ripple fades out
    this.alpha -= this.fadeRate;
  }

  display() {
    noFill();
    stroke(201, 243, 255, this.alpha);
    strokeWeight(2);

    // Draw multiple layers of ripples
    for (let i = 0; i < this.layers; i++) {
      let layerSize = this.size - i * 10;
      let widening = pow(this.wideningFactor, i); // Widen each subsequent ripple
      if (layerSize > 0) {
        this.drawOvalRipple(layerSize * 2 * widening, layerSize * widening); // Draw each ripple layer
      }
    }
  }

  // Draw ripples with distortion using Perlin noise
  drawOvalRipple(ovalWidth, ovalHeight) {
    beginShape();
    for (let angle = 0; angle < TWO_PI; angle += 0.1) {
      let xoff = cos(angle) * 5; // X offset for Perlin noise
      let yoff = sin(angle) * 5; // Y offset for Perlin noise

      // Apply Perlin noise to distort the ripple shape
      let distortion = map(noise(xoff + this.size * noiseScale, yoff + this.size * noiseScale), 0, 1, 0.95, 1.05);
      
      // Calculate the X and Y coordinates for the ripple based on the oval's size and the distortion
      let x = this.pos.x + (ovalWidth / 2 * cos(angle)) * distortion;
      let y = this.pos.y + (ovalHeight / 2 * sin(angle)) * distortion;

      vertex(x, y); // Add vertex to the shape
    }
    endShape(CLOSE);
  }

  // Check if the ripple is fully faded
  isDone() {
    return this.alpha <= 0;
  }
}

In the Ripple class, I initialized all the elements to the ripple. I updated the ripple’s size and level of opacity. I drew multiple layers of ripples, with each growing larger. Each ripple has Perlin noise applied to it to distort the shape and make it look more organic. If the ripple is fully faded, it is removed from the simulation. In the draw function, I used ‘splice’ to remove each ripple and raindrop from their respective arrays.

Challenges

I wanted to make the background more interesting and fluid. Instead of just having a one colour background, I want to make a gradient background of two colours. Although this is usually a simple process that I have worked with in the past, I found it hard to implement into my code as my code worked with objects that changed and moved. This meant I needed to find a way to the gradient to be drawn as the background without affecting the raindrop or ripple objects (both objects have transparency to them and thus are visually effected) To do this, I drew the gradient on PGraphics (gradientBG) rather than directly onto the canvas. This way, the gradient is stored and I don’t need to redraw it every frame. In the draw function the gradient is applied using “image(gradientBG, 0, 0)”.

Pen Plotting

I did not have to change my sketch for the pen plotting. The process was enjoyable and informative, as it was my first time using a pen plotter. One improvement that I think could have been made is drawing different layers, and using a light colour for some of the individual ripples, as in the code the ripples become lighter in colour as they fade out. Here is my pen plot and the video of the process.

Future Improvements

To improve this project, I would add rainfall sounds and music in the back to create a calming ambiance. I would create other objects to add to the ambiance, such as lily pads or lake animals. I would also create a boundary for each ripple, and modify the ripples when they collide with each other, such as increasing the Perlin noise. I was attempting to do this, but kept running into errors/having glitches.

Midterm – Sonic Threads

Concept:

For this project, I wanted to use what we learned in data visualization and particle systems to visualize Palestinian folklore. I want to visualize sound waves through the lens of Palestinian embroidery. 

The song I picked has an Arabic version and an English Remix.  Thus, Palestinian folklore is known to be one of the vested tools to record events and phenomena. For the Palestinians, historically, it is women who sing these songs; they are a mirror of history and society and a contribution to crystallizing the higher goals of Palestinian society.  For this project, I wanted to use Tarweeda (a type of Palestinian folklore), which is slow singing governed by a specific rhythm that depends primarily on recitation, a melodious recitation that carries its own music. This type of music became vibrant in Palestine during the Ottoman occupation, where women would sing these songs after encoding the words and delivering secret messages to each other and to the resistance fighters. The encryption process happens by adding ‘lam” in the words at the end or final syllables. Through this lens, I want to embed the traditional visual language of Palestinian embroidery with  Palestinian Tarweed in an attempt to visualize the rhythms.

 

For the visualization aspect of the project, I looked into the Palestinian embroidery, which is usually a geometric pattern of shapes, flowers, and lines. The most famous embroidery colors are red threads on black fabric. 

Code and Process:

 

I have been inspired to work with sound waves for a while. I have been watching videos of how it works and how sound wave data can be visualized. Initially, it seemed very complex to me; however, when I watched more and more videos, I got the gist of it. I had to think of a couple of things first before I took this step because I was not certain I could do it. As a result, I decided to see if P5js can visualize data. I found p5.FFT, which is Fast Fourier Transform, is an analysis algorithm that can isolate audio frequencies within a waveform, which returns an array of the analyzed data between -1 and 1. Its waveform() represents the amplitude value at a specific time. The analyze() computes the amplitude along the frequency domain from low to high between 0 to 255, and the energy () measures the amplitude at specific frequencies or ranges. 

I began by preloading a small part of the song into P5js; I tested the P5js given example before doing my own iteration because the P5js sketch was confusing. I then created a variable that takes the p5.FFT data, and a wave data variable to take the waveform. Then, in an array of index numbers, I mapped the wave data for visual purposes and visualized it within a shape similar to what we did in class. When this part was working, I decided to add a Particle system class that is responsive to the sound. To do this, I analyzed the FFT data and read the amplitude of the data to see the numbers I needed to put into the energy to enable it to respond properly. This part took some time because I had to read a little more about the sound wave data and how to make it responsive. 

After everything was working fine, I began experimenting with the shape and how I wanted it to look. My goal was to make it look similar to the embroidery patterns, which are usually floral. I realized that I needed to take the Sine value of i for both the x and the y; I was initially doing Cos(i) for x and Sin(i) for y. Then, I added interaction by making frequency values for x and y, mapping them, and then adding them into the x and y formula for the shape.

Highlight:

let song;
let currentPlaySong = 1;

let song1;
let hasPlayed = false;
let hasPlayed1 = false;
let fft; //this is to generate the wave form Fast Fourier Transform

let r1;

//For Printing https://gokcetaskan.com/artofcode/high-quality-export
//  acording to this the size of my full screen sketch is good for high quality but the particles are not printing trying to fix this i will add them in a buffer actually realized its becuse the save condition was before the particle one

particles = [];

//button to start sketch
let button;
let started = false;

function preload() {
  song = loadSound("/sound.mp3");
  song1 = loadSound("/sound1.mp3");
}

function setup() {
  createCanvas(windowWidth, windowHeight);
  // createCanvas(windowWidth, windowHeight, SVG);
  angleMode(DEGREES);
  fft = new p5.FFT();

  //   button
  button = createButton("Start");
  button.position(width / 2, 530);
  button.mousePressed(startSketch);
}

function draw() {
  background(0);

  //added this before translate to position correctly
  if (!started) {
    text("1-move the curser to change the shape.", width / 2 - 130, 450);
    text(
      "2-Press A or a to listen to the Arabic version",
      width / 2 - 130,
      470
    );
    text("3-Press E or e to listen to the English Remix", width / 2 - 130, 490);
    text("4- Press R or r to Reset & S or s to Save", width / 2 - 130, 510);
    fill(255);
    textSize(15);
  }

  translate(width / 2, height / 2);
  //particles respond to frequencies
  fft.analyze();
  amp = fft.getEnergy(20, [200]);
  // shape

  let waveData = fft.waveform();

  if (started) {
    for (let t = -1; t <= 1; t += 0.09) {
      let freqX = map(mouseX, 0, width, 1, 10);
      let freqY = map(mouseY, 0, height, 1, 10);

      let offSet = map(mouseX, 0, width, 0, 150);
      beginShape();

      for (let i = 0; i < width; i++) {
        stroke(150, 0, 0);
        fill(0);
        strokeWeight(1);
        let index = floor(map(i, 0, 180, 0, waveData.length - 1));

        r1 = map(waveData[index], -1, 1, 100, 300);

        let x1 = r1 * sin(15 * i) * t * cos(i * freqX);
        let y1 = r1 * sin(15 * i) * sin(i * freqY);

        vertex(x1, y1);
      }
      endShape(CLOSE);

      //    if (key == 's'){
      //     save("mySVG.svg");
      //     noLoop();	}
      // }
    }

    let p = new particle();
    particles.push(p);

    for (let i = particles.length - 10; i >= 0; i--) {
      if (!particles[i].edges()) {
        particles[i].update(amp > 190);
        particles[i].show();
      } else {
        particles.splice(i, 0);
      }
    }
  }
}

//function to switch from arabic to english version of the song and reset
function keyPressed() {
  if (key === "a" || key === "A") {
    switchToSong1(); // play song 1 when 'a' is pressed
  } else if (key === "e" || key === "E") {
    switchToSong2(); // play song 2 when 'e' is pressed
  } else if (key === "r" || key === "r") {
    resetSongs(); // reset the songs when 'R' is pressed
  } else if (key === "s" || key === "S") {
    // Save the canvas as PNG without stopping the sound
    save("myPNG.png");
  }
}
//I need to add this because if i dont for some reason when I press a it does not play
function switchToSong1() {
  if (!song.isPlaying()) {
    song1.pause(); // pause song 2 if it's playing
    song.play(); // play song 1
    currentSong = 1;
  }
}

function switchToSong2() {
  if (!song1.isPlaying()) {
    song.pause(); // pause song 1 if it's playing
    song1.play(); // play song 2
    currentSong = 2;
  }
}

function resetSongs() {
  // stop both songs
  song.stop();
  song1.stop();

  // Reset to the first song
  currentSong = 1;
}

function startSketch() {
  started = true;
  button.hide(); // hide the button after starting the sketch
}
class particle {
  constructor(){

  this.pos = p5.Vector.random2D().mult(200);
  this.vel = createVector(0,0);
  this.acc = this.pos.copy().mult(random(0.001,0.0001));
    // this.r = 10;
    // this.lifetime = 255;
this.PW = random(1,5);
}
update(cond){
  this.vel.add(this.acc);
  this.pos.add(this.vel);
  
  if(cond){
    this.pos.add(this.vel);
    this.pos.add(this.vel);
    this.pos.add(this.vel);
    
  }
  
}
  //remove particles that were drawn 
  edges(){
    if(this.pos.x<-width/2 || this.pos.x>width/2 || this.pos.y <-height/2 || this.pos.y>height/2
     ){
       return true;
  } else {
    return false;
  }}
show(){
  noStroke();
   
    fill(204, 0, 0);
  ellipse(this.pos.x, this.pos.y,this.PW);
  
}}

 

In this project, I had a couple of challenges. Initially, the shape I created was reflected in particles because this is how waveforms are printed out. For some reason, even when I drew lines or other shapes to map, it did not look right. I kept experimenting with the numbers within the intended shape until I figured it out. Another challenge was saving an SVG file and a PNG file. Whenever I try to save a file, the whole system lags. According to some sources linked below, it might have been because a) it was saved before the whole sketch was drawn, which made the particles disappear, and b) having the no-loop was causing issues. Even when I had a function for saving, the sound stopped playing; as a result, I added the saving part to the KeyPressed function, which magically solved the problem. 

I am mostly proud that I was able to add other versions of the song into the system and allow the users to switch with songs, reset the whole system, and start again. I did this using conditions and pressing keys instead of the cursor interaction. Further, I create a start condition (state)  button designed in CSS to start sketch.

Pen plotting:

 

As mentioned above, I had challenges saving SVG files. The p5js version I was using was the most compatible with sound, and despite my attempts to integrate the SVG into the file, it kept lagging. Apparently, sound is computationally heavy on the system, and adding SVG was not working, but I managed to get some images and then commented on the code. Another issue was that when pen plotting, the pen would pass through a specific point so many times that the center got a little damaged. I think it adds to the aesthetics of the design. I tried pen plotting twice. The first one was a disaster, but the second was good, considering that it was a new system for me. 

PenPlotP5js

https://drive.google.com/file/d/1yTITCwDRO5CCQ72peazwVLUxmkqrV-G8/view?usp=drive_link

Sketch:

Link: https://editor.p5js.org/shn202/full/9AGLnP3I8

Future work and improvements 

I am satisfied with how my project turned out. However, I think it is always good to keep experimenting and trying new things. For future improvements, I would add more user interactions where users can play with the colors, more complex shapes, and even the particle system. I would also like to create an array of songs that can be played individually or together to create a chaotic experience. 

A3 print:

Resources:

“Creating a Sound Wave Using P5.js.” Stack Overflow, stackoverflow.com/questions/55943459/creating-a-sound-wave-using-p5-js.

Colorful Coding. “Code an Audio Visualizer in P5js (From Scratch) | Coding Project #17.” YouTube, 28 Feb. 2021, www.youtube.com/watch?v=uk96O7N1Yo0.

Interactive Encyclopedia of the Palestine Question – Palquest, www.palquest.org/en/highlight/14497/palestinian-embroidery.

Kazuki Umeda. “Beautiful Sound Visualization Using Polar Coordinates.” YouTube, 3 June 2021, www.youtube.com/watch?v=CY5aGEXsGDo.

Moussa, Ahmad. “Working With SVGs in P5JS.” Gorilla Sun, 3 May 2023, www.gorillasun.de/blog/working-with-svgs-in-p5js.

p5.FFT. p5js.org/reference/p5.sound/p5.FFT.

“Palestinian Embroidery.” Interactive Encyclopedia of the Palestine Question – Palquest, www.palquest.org/en/highlight/14497/palestinian-embroidery.

العربية, مجلة الموسيقى. الترويدة الفلسطينية &Quot;المولالاة&Quot; تراث عاد للظهور. 31 Dec. 2023, www.arabmusicmagazine.org/item/1538-2023-12-31-10-16-38.

https://www.w3schools.com/css/ css comLab class

Taskan, Gokce. Art of Code – Gokce Taskan. gokcetaskan.com/artofcode/high-quality-export.

 

Midterm Project – Half of what I say is meaningless…….Julia

Concept:

The Julia set is a captivating mathematical concept that beautifully intertwines the realms of complex numbers and visual art. As I explore the intricate patterns generated by varying real and imaginary numbers, I find a profound resonance with the fluidity of creativity. Each adjustment in the parameters breathes life into the design, revealing a unique, ever-evolving masterpiece. The dance between chaos and order in the Julia set mirrors my artistic journey, where boundaries blur and possibilities expand. It serves as a reminder that the most enchanting creations often arise from the interplay of structured mathematics and the boundless freedom of artistic expression. (Not only this but there are many songs for Julia- hence the title).

In my code, I aimed to explore the intricate designs possible in p5.js using the Julia set. Both the dynamic range and the still design produced satisfying results. Getting the main code, which features interactive and dynamic effects, to achieve a smooth and colorful outcome took some time. On the other hand, the still version I created specifically for pen plotting was much easier to develop.

Results:

Main Sketch:

Pen-plot Sketch:

Coding Concepts

Referenced Image

Understanding Julia and Mandelbrot Sets

  • Particle System: The code initializes an empty array particles to hold the particle instances. Each Particle is represented by a position vector, velocity vector, and color, allowing them to move and change color dynamically.

 

class Particle {
  constructor(x, y, col) {
    this.position = createVector(x, y);
    this.velocity = createVector(0, 0);
    this.acceleration = createVector(0, 0);
    this.col = col; // Store particle's color and opacity
  }

  display() {
    stroke(this.col); 
    strokeWeight(2);
    point(this.position.x, this.position.y);

    // Add glowing effect by drawing semi-transparent ellipses
    noFill();
    stroke(this.col.levels[0], this.col.levels[1], this.col.levels[2], this.col.levels[3] / 2); // Fainter stroke for glow
    ellipse(this.position.x, this.position.y, 4, 4); // Small glowing ellipse
  }

  update() {
    let n = noise(
      this.position.x * noiseScale,
      this.position.y * noiseScale,
      frameCount * noiseScale
    );

    let a = TAU * n; // Noise angle for motion

    this.acceleration = createVector(cos(a) * 0.05, sin(a) * 0.05); // Smooth acceleration
    this.velocity.add(this.acceleration); // Update velocity based on acceleration
    this.velocity.limit(2); // Limit speed for smoothness
    this.position.add(this.velocity); // Update position based on velocity

    // Wrap particles around the screen when they go out of bounds
    if (!this.onScreen()) {
      this.position.x = random(width);
      this.position.y = random(height);
    }
  }

  onScreen() {
    return (
      this.position.x >= 0 &&
      this.position.x <= width &&
      this.position.y >= 0 &&
      this.position.y <= height
    );
  }
}

 

 

  • Julia Sets: The code defines multiple Julia sets, stored in the juliaSets array. Each Julia set is created with random complex constants (real and imaginary parts). The class JuliaSet manages the constants for generating the fractals and generates particles based on the Julia set equations.

 

class JuliaSet {
  constructor(cRe, cIm) {
    this.cRe = cRe;
    this.cIm = cIm;
  }

  // Update constants based on either rotation or mouse position
  updateConstants(cRe, cIm) {
    this.cRe = cRe;
    this.cIm = cIm;
  }

  createParticles(xMin, yMin, xMax, yMax) {
    push();
    // Rotate around the center of the quadrant
    translate((xMin + xMax) / 2, (yMin + yMax) / 2);
    rotate(frameCount * 0.001); 
    translate(-(xMin + xMax) / 2, -(yMin + yMax) / 2);

    for (let i = 0; i < numParticles; i++) {
      let x = random(xMin, xMax);
      let y = random(yMin, yMax);
      let zx = map(x, xMin, xMax, -1, 1);  
      let zy = map(y, yMin, yMax, -1, 1);  
      let iter = 0;

      while (zx * zx + zy * zy < 4 && iter < maxIterations) {
        let tmp = zx * zx - zy * zy + this.cRe;
        zy = 2 * zx * zy + this.cIm;
        zx = tmp;
        iter++;
      }

      // Assign colors based on the number of iterations
      let colorHue = map(iter, 0, maxIterations, 0, 360); // Map iteration to hue
      let opacity = map(iter, 0, maxIterations, 0, 255); // Map iteration to opacity
      let col = color(colorHue, 100, 255, opacity); // HSB color with variable opacity
      
      particles.push(new Particle(x, y, col));
    }
    pop();
  }
}

 

 

  • Oscillation: Oscillation is controlled by angleRe and angleIm, which are updated in the draw function when the mouse is not over the canvas. This creates a smooth oscillatory effect for the real and imaginary parts of the Julia sets. The amplitude of the oscillation is controlled by oscillationAmplitude, and oscillationSpeed determines how fast the angles change, causing the Julia set to dynamically oscillate.

 

// Oscillation variables
let angleRe = 0; // Angle for real part rotation
let angleIm = 0; // Angle for imaginary part rotation
let oscillationSpeed = 0.02; // Speed of the oscillation
let oscillationAmplitude = 1.5; // Amplitude of the oscillation

 

 

// Check if mouse is over the canvas
 if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) {
   cRe = map(mouseX, 0, width, -1.5, 1.5);
   cIm = map(mouseY, 0, height, -1.5, 1.5);
 } 
 
 else {
   // Use oscillation when mouse is not over the canvas
   cRe = oscillationAmplitude * sin(angleRe);
   cIm = oscillationAmplitude * sin(angleIm);
   
   angleRe += oscillationSpeed;
   angleIm += oscillationSpeed;
 }

 

 

  • Particle Motion: Each Particle instance has:
    Position: Updated in the update method.
    Velocity: Calculated based on acceleration influenced by Perlin noise.
    Acceleration: Derived from noise to create smooth, natural movement.
    The update method utilizes the Perlin noise to define a direction (angle) of motion, which ensures that particles have a fluid, organic movement rather than erratic behavior.

 

let a = TAU * n; // Noise angle for motion

    this.acceleration = createVector(cos(a) * 0.05, sin(a) * 0.05); // Smooth acceleration
    this.velocity.add(this.acceleration); // Update velocity based on acceleration
    this.velocity.limit(2); // Limit speed for smoothness
    this.position.add(this.velocity); // Update position based on velocity

 

Embedded sketch

Sketch links:

Main: https://editor.p5js.org/mariamalkhoori/sketches/rAZ_ErDvE

Pen-Plot: https://editor.p5js.org/mariamalkhoori/sketches/y0ekmRdJv

Parts I’m proud of:

I think I’m overall proud that I was open to try something very new in a field (Maths) that’s quite intimidating.

I’m particularly proud of the integration of oscillation and Perlin noise in the particle system, which creates a captivating and fluid visual effect. The oscillation of the Julia sets introduces a dynamic quality, allowing the fractals to change smoothly over time, while the use of Perlin noise for particle movement ensures that their motion feels organic and natural rather than mechanical. This combination enhances the aesthetic appeal, making the visual experience engaging and immersive. Additionally, the interplay between the colors of the particles, driven by the fractal’s iterative escape dynamics, results in a stunning display that captivates the viewer’s attention. Overall, this synthesis of mathematical beauty and artistic design embodies the essence of generative art, transforming complex mathematical concepts into a mesmerizing visual spectacle.

Challenges:

I mainly faced challenges in figuring out the concept of the Julia set itself. Understanding the results of the different ranges required some effort to implement in the code.

Adding color and visual effects was just me testing and playing around, which resulted in some bugs that took time to fix.

I wanted to create only one Julia set that could spread across the screen, but I was unable to do so, so I settled for adding quadrants instead.

 Pen-Plotting translation and process:

For the pen-plotting translation, I had to create an entirely different code to produce a specific still image. I decided to explore more with this code, and I was really happy with how it ended up looking in the sketch itself. However, the plotted version looked completely different.

Initially, I had trouble implementing the SVG code and making it work, which was a hassle, and I ended up converting my images. In Inkscape, only a specific pattern was shown to me, and it looked very difficult to plot, so I had to edit it to display only the outline of what it provided. I tried to edit the image to make it resemble a Julia set range but failed to do so.

It’s not that I’m dissatisfied with the result; it’s quite the opposite. Upon seeing the final product, it reminded me of my art style, which made me very happy. While it is not a correct version of the Julia set, I would still say it is an extraction from it with a little touch from me.

Areas for improvement:
  1. SVG Implementation
  2. Image Editing
  3. Testing and Debugging
Future Work

Advanced Julia Set Exploration and possibly more math techniques: 

      • Investigate more complex variations of Julia sets and fractals. Experimenting with different mathematical formulas or parameters can yield unique visual results.
      • Consider implementing real-time adjustments to the parameters of the Julia set based on user interaction, enhancing the dynamic aspect of your visualizations.
References

https://thecodingtrain.com/challenges/22-julia-set

https://paulbourke.net/fractals/juliaset/

https://fractalsaco.weebly.com/julia-set.html

Apps: Inkscape

 

Midterm Project – Khalifa Alshamsi

Concept and Inspiration

The idea behind this project was to explore the concept of generative art—art created algorithmically through code. Using randomness and user input, the scene would evolve and change each time it was run. I was inspired by nature and its organic growth patterns, particularly how trees grow and how light changes throughout the day. I wanted to capture the serenity of these natural processes and translate them into a digital environment that users could interact with in real-time.

The project’s core focuses on creating a landscape that feels alive and constantly changing. The user’s ability to control elements like time and landscape features introduces a new level of engagement, making each session with the artwork feel unique.

Core Features

1. Time of Day Slider

The first element of user interaction is the time slider, which controls the day and night cycle in the landscape. As the slider is moved, the background color transitions smoothly from a warm sunrise to a vibrant midday, then to a calm sunset, and finally to a deep nighttime sky. The sun and moon move across the sky in sync with the slider, casting light and shadows that dynamically affect the scene. This gives users control over the atmosphere and mood of the landscape, allowing them to explore different times of day within the environment.

2. Dynamic Tree Growth

At the center of the landscape is a tree that grows organically with recursive branching patterns. The tree starts with a simple trunk, and as it grows, branches and leaves are added to create a natural-looking structure. The tree’s growth is controlled by randomness, ensuring that no two trees are ever the same. The recursive branch() function simulates the natural way that trees split and grow over time, and slight variations in angle and branch length introduce an element of unpredictability.

The tree’s presence in the landscape serves as the focal point, representing life and growth. Its organic form contrasts with the more structured features of the scene, like the background mountains.

3. Procedurally Generated Mountains

Behind the tree, procedurally generated mountains create a sense of depth and scale in the scene. Users can toggle the mountains on or off, and each time the mountains are generated, they are slightly different due to the random seed values used in their creation. The mountains are rendered in layers, creating a gradient effect that gives the impression of distance and elevation. This layered approach helps to simulate the natural terrain, adding complexity to the background without overpowering the main elements in the foreground.

The ability to toggle the mountains allows users to switch between a minimalist landscape and a more detailed environment, depending on their preference.

4. Background Trees

Smaller background trees are scattered across the landscape to fill out the scene and make it feel more immersive. These trees are procedurally generated as well, varying in size and shape. Their randomness ensures that the forest never feels static or repetitive. The background trees add a layer of depth to the scene, complementing the central tree and helping to balance the visual composition.

5. SVG Export Feature

A standout feature of the project is the ability to export the landscape as an SVG (Scalable Vector Graphics) file. SVGs are ideal for preserving the high-quality details of the landscape, especially for users who may want to print or use the artwork at different scales. The SVG export captures all of the generative elements, like the tree, background, and mountains, while excluding certain elements, such as shadows, that don’t translate well to vector format.

This functionality ensures that users can take their creations beyond the screen, preserving them as works of art in a flexible, scalable format.

Technical Breakdown

1. Background Color Transition

One of the key visual elements in the landscape is the smooth transition of background colors as the time slider is moved. The lerpColor() function handles this transition, which blends colors between sunrise, midday, sunset, and night. The gradual transition gives the scene a fluid sense of time passing.

function updateBackground(timeValue, renderer = this) {
  let sunriseColor = color(255, 102, 51);
  let sunsetColor = color(30, 144, 255);
  let nightColor = color(25, 25, 112);

  let transitionColor = lerpColor(sunriseColor, sunsetColor, timeValue / 100);
  
  if (timeValue > 50) {
    transitionColor = lerpColor(sunsetColor, nightColor, (timeValue - 50) / 50);
    c2 = lerpColor(color(255, 127, 80), nightColor, (timeValue - 50) / 50);
  } else {
    c2 = color(255, 127, 80);
  }

  setGradient(0, 0, W, H, transitionColor, c2, Y_AXIS, renderer);
}

This code adjusts the colors based on the time of day, providing a rich, dynamic background that feels hopefully alive and ever-changing.

2. Tree Growth and Branching

The tree at the center of the landscape is generated through a recursive branching algorithm. The branch() function ensures that each branch grows smaller as it extends from the trunk, mimicking how real trees grow.

function branch(depth, renderer = this) {
  if (depth < 10) {
    renderer.line(0, 0, 0, -H / 15);
    renderer.translate(0, -H / 15);

    renderer.rotate(random(-0.05, 0.05));

    if (random(1.0) < 0.7) {
      renderer.rotate(0.3);
      renderer.scale(0.8);
      renderer.push();
      branch(depth + 1, renderer);  // Recursively draw branches
      renderer.pop();
      
      renderer.rotate(-0.6);
      renderer.push();
      branch(depth + 1, renderer);
      renderer.pop();
    } else {
      branch(depth, renderer);
    }
  } else {
    drawLeaf(renderer);  // Draw leaves when the branch reaches its end
  }
}

The slight randomness in the angles and branch lengths ensures that no two trees look alike, providing a natural, organic feel to the landscape.

Challenges and Solutions

1. Handling Shadows

Creating realistic shadows for the tree and ensuring they aligned with the position of the sun or moon was a significant challenge. The shadow needed to rotate and stretch depending on the light source, and it had to be excluded from the SVG export to maintain a clean vector image. The solution involved careful handling of transformations and conditional rendering depending on whether the scene was being drawn on the canvas or the SVG.

2. Smooth Color Transitions

Achieving smooth color transitions between different times of day took some trial and error. I needed to carefully balance the lerpColor() function to ensure the transitions felt natural and did not introduce sudden jumps between colors. This was crucial to maintaining the peaceful atmosphere I aimed to create.

3. SVG Export Process

The ability to export the generated landscape as an SVG is a crucial part of the project. SVG, or Scalable Vector Graphics, allows the artwork to be saved in a format that retains its quality at any size. This is particularly useful for users who want to print the landscape or use it at different scales without losing resolution.

The process of exporting the landscape as an SVG involves creating a separate rendering context specifically for SVG, redrawing the entire scene onto this context, and then saving the resulting image.

function saveCanvasAsSVG() {
  let svgCanvas = createGraphics(W, H, SVG);  // Use createGraphics for SVG rendering

  redrawCanvas(svgCanvas);  // Redraw everything onto the SVG canvas

  save(svgCanvas, "myLandscape.svg");  // Save the rendered SVG file

  svgCanvas.remove();  // Remove the SVG renderer from memory
}

In this process:

  • Separate SVG Context: The createGraphics() This function creates a new rendering context specifically for SVG output. This ensures that the drawing commands translate into vector graphics rather than raster images.
  • Redrawing the Scene: The redrawCanvas() function is called with the SVG context, ensuring that all elements (trees, mountains, background, etc.) are redrawn onto the SVG canvas.
  • SVG Export: The save() function is then used to save the rendered SVG file to the user’s device. Shadows, which don’t translate well to SVG, are excluded during this process to maintain the export’s aesthetics quality.

This dual rendering (canvas and SVG) approach ensures that users can enjoy real-time interactivity while still being able to export high-quality vector images.

Understanding Seed Values and Randomness

In this project, seed values are crucial in ensuring that the randomness used in generating elements (like trees and mountains) remains consistent across renders. By setting a randomSeed(), we control the sequence of random values used, which ensures that the same landscape is generated each time the same seed is used.

For example, if a seed value is set before generating a tree, the randomness in branch angles and lengths will be the same every time the tree is drawn, giving users a predictable yet varied experience. Changing the seed value introduces a new pattern of randomness, creating a different tree shape while maintaining the same underlying algorithm.

randomSeed(treeShapeSeed);  // Controls the randomness for tree generation

By controlling the seed values for both trees and mountains, the landscape retains a level of predictability while still offering variation in each render.

The Midterm Code

index.html File:

<!DOCTYPE html>
<html lang="en">

<head>
   <!-- Loads the core p5.js library first -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
   
   <!-- Then loads p5 sound -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script>
   
   <!-- Load p5 SVG after p5.js is loaded -->
   <script src="https://unpkg.com/p5.js-svg@1.5.1"></script>
   
   <link rel="stylesheet" type="text/css" href="style.css">
   <meta charset="utf-8" />
</head>
  <body>
    <main>
    </main>
    <script src="sketch.js"></script>
  </body>
</html>

 

Sketch.JS File:

let W = 650;  
let H = 450;  

let timeSlider;  // Slider to control day and night transition
let shiftButton;  // Button to shift trees and mountain layout
let saveButton;  // Button to save the current drawing as an SVG
let mountainsButton;  // Button to toggle mountains on/off
let addTreesButton;  // Button to toggle trees on/off
let showMountains = false;  // Boolean to track if mountains are visible
let showTrees = false;  // Boolean to track if trees are visible
let treeShapeSeed = 0;  // Seed value to control tree randomness
let mountainShapeSeed = 0;  // Seed value to control mountain randomness
let sunX;  // X position of the sun (controlled by slider)
let sunY;  // Y position of the sun (controlled by slider)
let moonX;  // X position of the moon (controlled by slider)
let moonY;  // Y position of the moon (controlled by slider)
let mountainLayers = 2;  // Number of mountain layers to draw
let treeCount = 8;  // Number of background trees to draw

const Y_AXIS = 1;  // Constant for vertical gradient drawing
let groundLevel = H - 50;  // The ground level (50 pixels above the canvas bottom)

function setup() {
  createCanvas(W, H); 
  background(135, 206, 235);  

  // Creates the time slider that controls day-night transitions
  timeSlider = createSlider(0, 100, 50);
  timeSlider.position(200, 460);
  timeSlider.size(250);
  timeSlider.input(updateCanvasWithSlider);  // Calls function to update canvas when slider moves

  // Creates button to toggle mountains on/off
  mountainsButton = createButton('Mountains');
  mountainsButton.position(100, 460);
  mountainsButton.mousePressed(() => {
    toggleMountains();  // Toggle the visibility of mountains
    redrawCanvas();  // Redraws the canvas after the toggle
  });

  // Creates a button to toggle trees on/off
  addTreesButton = createButton('Add Trees');
  addTreesButton.position(10, 460);
  addTreesButton.mousePressed(() => {
    toggleTrees();  // Toggles the visibility of trees
    redrawCanvas();  // Redraws the canvas after the toggle
  });

  // Creates a button to shift trees and mountain layout (randomize positions and shapes)
  shiftButton = createButton('Shift');
  shiftButton.position(490, 460);
  shiftButton.mousePressed(() => {
    changeTreeCountAndShape();  // Randomize the trees and mountain layout
    redrawCanvas();  // Redraws the canvas after the layout shift
  });

  // Creates a button to save the canvas as an SVG file
  saveButton = createButton('Save as SVG');
  saveButton.position(550, 460);
  saveButton.mousePressed(saveCanvasAsSVG);  // Save the canvas as an SVG when clicked

  noLoop();  // Prevents continuous looping of the draw function, manual updates only
  redrawCanvas();  // Perform the initial canvas draw
}

// This is called whenever the slider is moved; to update the canvas to reflect the new slider value
function updateCanvasWithSlider() {
  redrawCanvas();  // Redraws the canvas to update the time of day
}

// Saves the current canvas as an SVG
function saveCanvasAsSVG() {
  let svgCanvas = createGraphics(W, H, SVG);  // Creates a new graphics context for SVG rendering

  redrawCanvas(svgCanvas);  // Redraws everything onto the SVG canvas

  save(svgCanvas, "myLandscape.svg");  // Saves the SVG with a custom filename

  svgCanvas.remove();  // Removes the SVG canvas once it's saved
}

// Main function to redraw the entire canvas (or an SVG) when something changes
function redrawCanvas(renderer = this) {
  // Clears the canvas or the SVG renderer
  if (renderer === this) {
    background(135, 206, 235);  // Light blue background for daytime sky
  } else {
    renderer.background(135, 206, 235);  // Same background for SVG output
  }

  // Get the slider value to adjust the time of day (0 to 100)
  let timeValue = timeSlider.value();  

  // Updates the background gradient and sun/moon positions based on the time of day
  updateBackground(timeValue, renderer);  
  updateSunAndMoon(timeValue, renderer);

  // Checks if the mountains should be drawn, and draws them if necessary
  if (showMountains) {
    randomSeed(mountainShapeSeed);  // Ensures randomness is consistent with each draw
    drawLayeredMountains(renderer);  // Draws the mountain layers
  }

  // Draws the simple green ground at the bottom of the canvas
  drawSimpleGreenGround(renderer);  

  // Checks if trees should be drawn, and draws them if necessary
  if (showTrees) {
    drawBackgroundTrees(renderer);  // Draws background trees
  }

  // Draws the main tree in the middle of the canvas
  if (renderer === this) {
    // Draws the main tree on the canvas itself
    push();
    translate(W / 2, H - 50);  // Positions the tree in the center at the ground level
    randomSeed(treeShapeSeed);  // Controls randomness for consistent tree shapes
    drawTree(0, renderer);  // Draws the tree
    pop();

    // Calculates and draw the shadow of the tree based on the sun or moon position
    let shadowDirection = sunX ? sunX : moonX;  // Uses the sunX if it's daytime, otherwise use moonX
    let shadowAngle = map(shadowDirection, 0, width, -PI / 4, PI / 4);  // Adjusts shadow direction

    push();
    translate(W / 2, H - 50);  // Translates to the same position as the tree
    rotate(shadowAngle);       // Rotates the shadow based on the angle
    scale(0.5, -1.5);          // Flips and scales down the shadow so it doens't pop outside the green grass and shows up in the sky
    drawTreeShadow(0, renderer);  // Draws the tree shadow
    pop();
  } else {
    // Draws the main tree in an SVG without shadow because it doesn't look nice
    renderer.push();
    renderer.translate(W / 2, H - 50);  // Positions for SVG
    randomSeed(treeShapeSeed);
    drawTree(0, renderer);  // Draws the tree in SVG mode
    renderer.pop();
  }
}

// Toggles mountains visibility on or off
function toggleMountains() {
  showMountains = !showMountains;  // Flips the boolean to show/hide mountains
  redrawCanvas();  // Redraws the canvas to reflect the change
}

// Toggles trees visibility on or off
function toggleTrees() {
  showTrees = !showTrees;  // Flips the boolean to show/hide trees
  redrawCanvas();  // Redraws the canvas to reflect the change
}

// Draws the tree shadow based on depth, used to create a shadow effect for the main tree
function drawTreeShadow(depth, renderer = this) {
  renderer.stroke(0, 0, 0, 80);  // Semi-transparent shadow
  renderer.strokeWeight(5 - depth);  // Adjust shadow thickness based on depth
  branch(depth, renderer);  // Recursively draw branches as shadows
}

// Randomizes tree and mountain layouts by changing seeds and tree count
function changeTreeCountAndShape() {
  treeShapeSeed = millis();  // Updates the tree shape randomness seed
  mountainShapeSeed = millis();  // Updates the mountain shape randomness seed
  treeCount = random([8, 16]);  // Randomly selects between 8 or 16 trees
  mountainLayers = random([1, 2]);  // Randomly select 1 or 2 mountain layers
  redrawCanvas();  // Redraw the canvas with the new layout
}

// Draws multiple layers of mountains based on the chosen number of layers
function drawLayeredMountains(renderer = this) {
  if (mountainLayers === 2) {
    let layers = [
      { oy: 200, epsilon: 0.02, startColor: '#888888', endColor: '#666666', reductionScaler: 100 },
      { oy: 270, epsilon: 0.015, startColor: '#777777', endColor: '#555555', reductionScaler: 80 }
    ];
    // Draw two layers of mountains, with different heights and colors
    layers.forEach(layer => {
      drawMountainLayer(layer.oy, layer.epsilon, layer.startColor, layer.endColor, layer.reductionScaler, renderer);
    });
  } else if (mountainLayers === 1) {
    let layers = [
      { oy: 250, epsilon: 0.02, startColor: '#777777', endColor: '#555555', reductionScaler: 80 }
    ];
    // Draw a single layer of mountains
    layers.forEach(layer => {
      drawMountainLayer(layer.oy, layer.epsilon, layer.startColor, layer.endColor, layer.reductionScaler, renderer);
    });
  }
}

// Draws a single layer of mountains
function drawMountainLayer(oy, epsilon, startColor, endColor, reductionScaler, renderer = this) {
  let col1 = color(startColor);  // Starts the color for the gradient
  let col2 = color(endColor);  // Ends the color for the gradient

  renderer.noStroke();
  for (let x = 0; x < width; x++) {
    // Generates random mountain height based on noise and epsilon value
    let y = oy + noise(x * epsilon) * reductionScaler;
    let col = lerpColor(col1, col2, map(y, oy, oy + reductionScaler, 0, 1));  // Creates color gradient
    renderer.fill(col);  // Fills mountain with the gradient color
    renderer.rect(x, y, 1, height - y);  // Draws vertical rectangles to represent mountain peaks
  }
}

// Draws trees in the background based on the current tree count
function drawBackgroundTrees(renderer = this) {
  let positions = [];  // Array to store the X positions of the trees

  for (let i = 0; i < treeCount; i++) {
    let x = (i + 1) * W / (treeCount + 1);  // Spaces the trees evenly across the canvas
    positions.push(x);
  }

  // Draws each tree at the calculated X positions
  positions.forEach(posX => {
    renderer.push();
    renderer.translate(posX, groundLevel);  // Positions trees on the ground
    renderer.scale(0.3);  // Scales down the background trees
    randomSeed(treeShapeSeed + posX);  // Uses a random seed to vary tree shapes
    drawTree(0, renderer);  // Draws each tree
    renderer.pop();
  });
}

// Updates the background gradient based on the time of day (slider value)
function updateBackground(timeValue, renderer = this) {
  let sunriseColor = color(255, 102, 51);  
  let sunsetColor = color(30, 144, 255);  
  let nightColor = color(25, 25, 112);  
  
  // Lerp between sunrise and sunset based on time slider
  let transitionColor = lerpColor(sunriseColor, sunsetColor, timeValue / 100);
  
  // If the time is past halfway (i.e., after sunset), lerp to nighttime
  if (timeValue > 50) {
    transitionColor = lerpColor(sunsetColor, nightColor, (timeValue - 50) / 50);
    c2 = lerpColor(color(255, 127, 80), nightColor, (timeValue - 50) / 50);
  } else {
    c2 = color(255, 127, 80);  // Defaults to an orange hue for sunrise/sunset
  }

  setGradient(0, 0, W, H, transitionColor, c2, Y_AXIS, renderer);  // Apply gradient to the background
}

// Updates the position of the sun and moon based on the time of day (slider value)
function updateSunAndMoon(timeValue, renderer = this) {
  // Update sun position during daytime
  if (timeValue <= 50) {
    sunX = map(timeValue, 0, 50, -50, width + 50);  // Sun moves across the sky
    sunY = height * 0.8 - sin(map(sunX, -50, width + 50, 0, PI)) * height * 0.5;
    
    renderer.noStroke();
    renderer.fill(255, 200, 0);  // Yellow sun
    renderer.ellipse(sunX, sunY, 70, 70);  // Draws the sun as a large circle
  }
  
  // Updates moon position during nighttime
  if (timeValue > 50) {
    moonX = map(timeValue, 50, 100, -50, width + 50);  // Moon moves across the sky
    moonY = height * 0.8 - sin(map(moonX, -50, width + 50, 0, PI)) * height * 0.5;
    
    renderer.noStroke();
    renderer.fill(200);  // Light gray moon
    renderer.ellipse(moonX, moonY, 60, 60);  // Draw the moon as a smaller circle
  }
}

// Creates a vertical gradient for the background sky
function setGradient(x, y, w, h, c1, c2, axis, renderer = this) {
  renderer.noFill();  // Ensures no fill is applied

  if (axis === Y_AXIS) {
    // Loops through each horizontal line and apply gradient colors
    for (let i = y; i <= y + h; i++) {
      let inter = map(i, y, y + h, 0, 1);  // Interpolation factor
      let c = lerpColor(c1, c2, inter);  // Lerp between the two colors
      renderer.stroke(c);  // Sets the stroke to the interpolated color
      renderer.line(x, i, x + w, i);  // Draws the gradient line by line
    }
  }
}

// Draws the green ground at the bottom of the canvas
function drawSimpleGreenGround(renderer = this) {
  renderer.fill(34, 139, 34);  // Set fill to green
  renderer.rect(0, H - 50, W, 50);  // Draws a green rectangle as the ground
}

// Draws a main tree in the center of the canvas
function drawTree(depth, renderer = this) {
  renderer.stroke(139, 69, 19);  // Sets the stroke to brown for the tree trunk
  renderer.strokeWeight(3 - depth);  // Adjusts the stroke weight for consistency
  branch(depth, renderer);  // Draws the branches recursively
}

// Draws tree branches recursively
function branch(depth, renderer = this) {
  if (depth < 10) {  // Limits the depth of recursion
    renderer.line(0, 0, 0, -H / 15);  // Draws a vertical line for the branch
    renderer.translate(0, -H / 15);  // Moves up along the branch

    renderer.rotate(random(-0.05, 0.05));  // Slightly randomize branch angle

    if (random(1.0) < 0.7) {
      // Draws two branches at slightly different angles
      renderer.rotate(0.3);  // Rotates clockwise
      renderer.scale(0.8);  // Scales down the branch
      renderer.push();  // Saves the current state
      branch(depth + 1, renderer);  // Recursively draws the next branch
      renderer.pop();  // Restores the previous state
      
      renderer.rotate(-0.6);  // Rotates counterclockwise for the second branch
      renderer.push();
      branch(depth + 1, renderer);  // Recursively draws the next branch
      renderer.pop();
    } else {
      branch(depth, renderer);  // Continues the drawing of the same branch
    }
  } else {
    drawLeaf(renderer);  // Once depth limit is reached, it draws leaves
  }
}

// Draws leaves on the branches
function drawLeaf(renderer = this) {
  renderer.fill(34, 139, 34);  // Set fill to green for leaves
  renderer.noStroke();
  for (let i = 0; i < random(3, 6); i++) {
    renderer.ellipse(random(-10, 10), random(-10, 10), 12, 24);  // Draws leaves as ellipses
  }
}

Final Midterm Result

Generated Art

Plotted Version:

Writing Robot A3 

Printed Versions on A3 paper:


I am genuinely proud of the generative landscape project’s end result. From the initial concept of creating a dynamic, evolving scene to the final implementation, the project beautifully balances aesthetic simplicity with technical complexity. Seeing the tree grow, the sky transitions seamlessly from day to night, and the mountains form in the background, each element comes together to create a peaceful yet engaging environment.

Areas of Improvement

  • Smoother Transitions and Animations when having multiple things on the canvas.
  • Enhancing the Complexity of the Environment with different sets of grass and water sources.
  • The rendering process for SVG differs from canvas rendering, and handling that discrepancy can sometimes result in mismatched visuals or loss of detail, such as scaling issues or slightly off positions.

Sources

  • https://p5js.org/reference/p5/randomSeed/
  • Coding Challenge #14 – The Coding Train on YouTube
  • https://github.com/zenozeng/p5.js-svg
  • Fractal Trees and Recursive Branching
  • https://www.w3.org/Graphics/SVG/
  • https://github.com/processing/p5.js/wiki

Midterm Project – Interactive Fan 𖣘

Concept: Electric fan Royalty Free Vector Image - VectorStock

My concept is an interactive electric fan. The user can control its movement using the right or left arrow buttons so it would move clockwise or anti-clockwise accordingly.

Sketch:

Full-screen here

Sketch images:

Code logic:

In the sketch.js file:

  • The text giving instructions to the user fades after 7 milliseconds.
  • New blades are created and added to the array. The ones in the front are shorter and the ones in the back are longer.
  • Fan accelerates right on right arrow click and accelerates left on left arrow click.
  • Friction force slows it down gradually when no key is pressed.
  • The velocity is limited to 0.3 so that it does not move too fast.

In the blade.js file:

The blade class contains all the functions needed for a single blade.

constructor:

  • Includes length and phase offset for the wave.

drawBlade:

  • Rotates the blade continuously using sin combined with amplitude.
  • Draws the bezier curve’s control points using vectors.

Concepts used from class:

  • Vectors
  • Forces (Friction)
  • Oscillation

Challenges and reference:

  • It was quite challenging to figure out how to draw vectors as the bezier curve’s control points. For that, I watched this video which helped with that.
// use vectors for bezier control points
let lenVector = createVector(this.length / 4, 1); // base vector for length
for (let j = 0; j < 4; j++) {
  let curveOffset = map(j, 0, 4, -this.length / 2, this.length / 2);
  let controlPoint1 = lenVector.copy().mult(-1);   // start of the bezier
  let controlPoint2 = createVector(-this.length / 4, curveOffset); // control point 1
  let controlPoint3 = createVector(this.length / 4, -curveOffset); // control point 2
  let controlPoint4 = lenVector.copy();            // end of the bezier

  // draw the bezier curve using vector points
  bezier(
    controlPoint1.x, controlPoint1.y, 
    controlPoint2.x, controlPoint2.y, 
    controlPoint3.x, controlPoint3.y, 
    controlPoint4.x, controlPoint4.y
  );
}

Pen plotting:

    • For the pen plotting, I had to modify many things from my initial sketch (shown below). This one had many different opacity levels and colors that would not be ideal to plot.

  • Instead, I modified the sketch to be much simpler, with curves instead of rectangles and without the fading effect. This turned out to be a better version to plot on the pen plotter. I also removed the filled color just for the pen plotting purposes.

Pen Plotted Photo

Future improvements:

As an improvement, it would be nice to connect it to an arduino and create a DIY fan that would move based on the key input that the user chooses.

 

Midterm Progress 2 – Julia Set

Concept:

Continuing my work from the previous sketch, I noticed two things that needed to be done: refining my original code and creating a pen-plotting-friendly version. This time, I mainly focused on the latter. I kept the Julia set design but made it less dynamic and interactive to suit pen plotting. The design in the coding sketch produced a different concept than the plotted version, but I still liked the final outcome.

Code Highlight:
let c = { re: 0, im: 0 };
let zoom = 1;

function setup() {
  createCanvas(600, 600);
  noFill();
}

function draw() {
  background(0); 
  let w = 4 / zoom;
  let h = (4 * height) / width / zoom;

  stroke(255); 
  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      let zx = map(x, 0, width, -2, 2) * zoom;
      let zy = map(y, 0, height, -2, 2) * zoom;
      let i = 0;
      while (i < 100) {
        let tmp = zx * zx - zy * zy + c.re;
        zy = 2.0 * zx * zy + c.im;
        zx = tmp;
        if (zx * zx + zy * zy > 4) break;
        i++;
      }
      if (i === 100) {
        point(x, y);
      }
    }
  }

  // Draw additional Julia sets
  drawAdditionalSets();
}

function mouseMoved() {
  c.re = map(mouseX, 0, width, -1.5, 1.5);
  c.im = map(mouseY, 0, height, -1.5, 1.5);
}

function mouseWheel(event) {
  zoom += event.delta * 0.001;
  zoom = constrain(zoom, 0.1, 10);
}

// Draw additional Julia sets
function drawAdditionalSets() {
  let sets = [
    { re: -0.7, im: 0.27015 },
    { re: 0.355, im: 0.355 },
    { re: -0.4, im: 0.6 },
    { re: 0.355, im: -0.355 },
    { re: -0.7, im: -0.27015 }
  ];

  for (let set of sets) {
    let zx, zy, i;
    for (let x = 0; x < width; x++) {
      for (let y = 0; y < height; y++) {
        zx = map(x, 0, width, -2, 2) * zoom;
        zy = map(y, 0, height, -2, 2) * zoom;
        i = 0;
        while (i < 100) {
          let tmp = zx * zx - zy * zy + set.re;
          zy = 2.0 * zx * zy + set.im;
          zx = tmp;
          if (zx * zx + zy * zy > 4) break;
          i++;
        }
        if (i === 100) {
          stroke(225); 
          point(x, y);
        }
      }
    }
  }
}

 

 

Key Components
  • Julia Set Calculation: The core of the code lies in the logic that iterates over each pixel on the canvas, mapping pixel coordinates to a complex plane and then applying the iterative formula for the Julia set.
  • Rendering Multiple Julia Sets: The drawAdditionalSets() function renders additional Julia sets with predefined complex constants. By iterating over multiple constants and reapplying the Julia set formula, this function draws additional sets on the same canvas, expanding the visual complexity of the sketch.
Sketch:

Pen Point Sketch:
Final Steps:
  • Focus on refining the main code.
  • Provide A3 prints.

Midterm Progress #2 “Dynamic Hearts” – Stefania Petre

Concept

The Dynamic Hearts project aims to create an engaging visual experience through the generation of heart-shaped forms that interact with each other. This project draws inspiration from the ebb and flow of life, where hearts symbolize love, connection, and emotional depth. The hearts dynamically change in size, color, and motion based on Perlin noise, simulating a dance of souls interacting with one another, occasionally colliding and sometimes drifting apart, creating a mesmerizing display of movement and color.

Design

1. Shapes: The sketch generates multiple heart shapes whose sizes and positions are influenced by Perlin noise, resulting in smooth and organic motions. Each heart is drawn in the center of the canvas, enhancing the visual symmetry and creating a harmonious composition.

2. Color Palette: Colors are assigned dynamically based on the index of each heart, producing a gradient effect across the shapes. This choice evokes a sense of depth, movement, and emotional resonance, as colors shift and blend seamlessly.

3. Interactivity: The motion of the hearts is dictated by a combination of Perlin noise and an orbiting pattern. This feature adds a layer of interactivity, allowing viewers to experience the visual output as a living, breathing entity that shifts with the rhythm of the noise, mimicking the spontaneity of life itself.

States and Variations

The sketch can exhibit different states based on its parameters:
– The number of heart shapes can be adjusted to create a denser or sparser visual.
– Modifying the radius and orbit settings can lead to variations in the hearts’ motions and interactions, resulting in diverse visual patterns.
– The introduction of additional interactive elements, such as changing behavior through keyboard inputs or dynamically adjusting colors based on viewer interactions, can further enrich the visual experience.

Identified Risks

One of the more complex aspects of this project is managing the performance and responsiveness of the visual output, especially with a larger number of hearts. Ensuring the sketch runs smoothly without lag is essential for providing a seamless user experience.

Risk Reduction Strategies

To minimize potential risks:
– The number of heart shapes is set to a manageable level (15) to maintain performance while allowing for a visually rich experience.
– The use of `noFill()` and `strokeWeight()` enhances rendering efficiency while preserving visual quality.
– Performance will be tested across different devices to ensure responsiveness, with adjustments made based on testing results.

Process and Evolution

Starting from my initial concept of creating dynamic spirals, I aimed to design a captivating visual that responded to user input. The initial draft featured spiraling lines that changed in size and color based on mouse position. However, through a process of exploration and experimentation, I was inspired to shift my focus to heart shapes, which felt more resonant with the themes of connection and emotional depth.

The transition from spirals to hearts involved redefining the visual language of the project. I began by adapting the existing code, replacing the spirals with heart shapes that could interact in an engaging manner. By leveraging Perlin noise, I was able to create fluid and organic movements that echoed the unpredictability of human emotions and relationships. The resulting composition features hearts that move like souls, sometimes colliding and other times drifting apart, providing a poignant metaphor for our connections in life.

Final Product

In conclusion, the Dynamic Hearts project represents a culmination of my explorations in interactive art, showcasing how shapes can convey emotional narratives and foster a sense of connection through visual interaction. The final product has evolved significantly from the initial draft, transforming into a rich and engaging experience that reflects the complexity of life and love.

 

 

//Dynamic Hearts - Midterm by SP

let numShapes = 15; // Number of shapes
let shapeRadius = 100; // Distance from center
let maxRadius = 1000; // Maximum size for shapes
let angleStep = 0.02; // Speed of rotation

let noiseOffsetX1 = 0; // X-offset for Perlin noise (Group 1)
let noiseOffsetY1 = 1000; // Y-offset for Perlin noise (Group 1)

let noiseOffsetX2 = 5000; // X-offset for Perlin noise (Group 2)
let noiseOffsetY2 = 6000; // Y-offset for Perlin noise (Group 2)

let orbitRadius = 200; // Distance between the two groups

function setup() {
    createCanvas(windowWidth, windowHeight);
    noFill();
    strokeWeight(2);
}

function draw() {
    background(0, 30);

    // Calculate the central orbit angle based on Perlin noise
    let orbitAngle1 = noise(noiseOffsetX1) * TWO_PI; // Group 1 orbit angle
    let orbitAngle2 = noise(noiseOffsetX2) * TWO_PI; // Group 2 orbit angle

    // Group 1 position based on orbit
    let centerX1 = orbitRadius * cos(orbitAngle1);
    let centerY1 = orbitRadius * sin(orbitAngle1);
    
    // Group 2 position based on orbit, opposite direction
    let centerX2 = orbitRadius * cos(orbitAngle2 + PI);
    let centerY2 = orbitRadius * sin(orbitAngle2 + PI);

    // Draw first group of hearts
    push();
    translate(width / 2 + centerX1, height / 2 + centerY1);
    drawShapeGroup(numShapes, noiseOffsetX1, noiseOffsetY1, shapeRadius);
    pop();

    // Draw second group of hearts
    push();
    translate(width / 2 + centerX2, height / 2 + centerY2);
    drawShapeGroup(numShapes, noiseOffsetX2, noiseOffsetY2, shapeRadius);
    pop();

    // Update Perlin noise offsets for more fluid motion
    noiseOffsetX1 += 0.01;
    noiseOffsetY1 += 0.01;
    noiseOffsetX2 += 0.01;
    noiseOffsetY2 += 0.01;
}

// Function to draw a group of hearts
function drawShapeGroup(num, noiseX, noiseY, radius) {
    for (let i = 0; i < num; i++) {
        // Dynamic position based on Perlin noise
        let noiseFactorX = noise(noiseX + i * 0.1) * 2 - 1;
        let noiseFactorY = noise(noiseY + i * 0.1) * 2 - 1;
        let xOffset = radius * noiseFactorX;
        let yOffset = radius * noiseFactorY;
        
        drawHeart(xOffset, yOffset, i);
    }
}

// Function to draw a heart shape
function drawHeart(x, y, index) {
    stroke(map(index, 0, numShapes, 100, 255), 100, 255, 150); // Dynamic color
    beginShape();
    for (let t = 0; t < TWO_PI; t += 0.1) {
        // Heart shape parametric equations with scaling factor for size
        let scaleFactor = 4; // Adjust this factor for size (increased for larger hearts)
        let xPos = x + scaleFactor * (16 * pow(sin(t), 3));
        let yPos = y - scaleFactor * (13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t));
        vertex(xPos, yPos);
    }
    endShape(CLOSE);
}

// Adjust canvas size on window resize
function windowResized() {
    resizeCanvas(windowWidth, windowHeight);
}

 

Generating Voronoi Cells with a Noise Overlay

Project Concept and Design

The goal of this project was to create an interactive Voronoi diagram that responds dynamically to user input and produces aesthetically compelling outputs by adding generative noise. The idea stemmed from the desire to visualize Voronoi patterns, which are generated by partitioning a canvas into regions based on the distance to a set of given points. Each point acts as a “cell center,” and the area around it forms its own unique region.

The primary objectives of the project were to:

  1. Develop an interactive tool that allows users to generate and explore Voronoi diagrams.
  2. Implement different visual variations using color noise and dynamic borders.
  3. Introduce a mechanism for rendering high-quality SVG exports, so users can save and share their creations.

https://photos.app.goo.gl/9BCq82kGNhyzQprz9

Click the screen to put down a cell. WARNING: Cell generation takes time.

 

Key Components and Design Decisions

  1. Concept of Voronoi Diagrams: Voronoi diagrams divide a plane into cells based on the distance to a set of points. Each point is the “seed” of a cell, and the cell comprises all the points closer to that seed than to any other. By using the Euclidean distance between each point on the canvas and each seed, I determined which cell a given pixel belonged to. This was implemented in the code using nested loops iterating over every pixel on the canvas.
  2. User Interactivity: To make the program interactive, I incorporated mouse-based interaction. Users can click anywhere on the canvas to add new cell centers, which dynamically redraws the entire Voronoi diagram. This way, users can create and explore different patterns based on the arrangement of cell centers.
  3. Generative Art Techniques: Instead of filling each Voronoi cell with a solid color, I wanted to experiment with visual noise to give a more organic feel. I used Perlin noise (noise()) to create outlines reminiscent of a height map. This produces a grainy texture that adds depth and uniqueness to each cell while retaining the overall Voronoi structure.
  4. Border Detection: One of the most critical visual elements in Voronoi diagrams is the border between cells. I implemented a pixel-based edge detection by comparing each pixel’s color with its right and lower neighbors. If the colors differed, it indicated a boundary, and I set that pixel’s color to black to create crisp, clear borders.

Project Development and Challenges

  1. Implementing Voronoi Calculations Efficiently: The first major hurdle was the efficiency of the Voronoi generation. Each pixel needs to be classified based on its distance to all cell centers, which scales poorly as more cells are added. I attempted optimizations, such as limiting the range of cells to check based on a maximum radius, but these did not yield significant performance improvements.
  2. Handling Color Transitions with Noise: Initially, I experimented with applying noise to the entire canvas indiscriminately, but this made it difficult to distinguish cell boundaries. I resolved this by using a lower noise scale and blending the cell color with a base color (white) to reduce visual clutter while retaining a natural texture.
  3. Anti-Aliasing and Edge Smoothing: A key issue was achieving smooth edges, especially along cell borders. I attempted to implement anti-aliasing by blending pixel colors based on their distance to the border, but this proved difficult within p5.js’s pixel-based drawing environment. Ultimately, I increased the pixelDensity() to minimize aliasing artifacts.

The Most Complex and Frightening Part

The most daunting part of this project was optimizing the Voronoi generation algorithm. Due to the pixelwise nature of the computation, the performance dropped rapidly as the number of cells increased. To minimize this risk, I conducted experiments with various distance calculation optimizations, such as bounding box checks and region partitioning, but none provided a breakthrough solution. Ultimately, I accepted that the current implementation, while not perfectly optimized, is still effective for typical canvas sizes and cell counts.

What I Would Do Differently

If I had more time, I would:

  1. Implement a more efficient Voronoi generation using a Fortune’s Algorithm, which scales better than pixel-based methods.
  2. Experiment with different visualizations, such as using curves or gradient fills for cell interiors.
  3. Create a more advanced edge detection algorithm that produces smoother, anti-aliased borders without compromising on performance.

Final Thoughts

This project pushed my understanding of generative art and computational geometry. By exploring different variations and solving various challenges, I was able to produce a compelling, interactive system that not only generates Voronoi diagrams but also serves as a tool for artistic exploration. Though there are areas for improvement, I am satisfied with the current state of the program and look forward to iterating on it further in the future.

Midterm Project – Fibonacci Dream: A Journey Through Spirals

For my midterm project, I wanted to explore the mesmerizing nature of the Fibonacci spiral through a generative art system. I named the project “Fibonacci Dream” because it combines the beauty of mathematical spirals with dynamic, flowing petals that continuously change over time.

Concept and Artistic Vision:
The Fibonacci sequence and the golden angle have always fascinated me. I love how these mathematical principles appear in nature—from sunflowers to seashells. My goal was to mimic this natural elegance by creating a digital flower that grows and oscillates, combining organic movement with vibrant color palettes.

While researching Fibonacci spirals, I learned how deeply they connect to growth patterns in nature. This connection inspired me to build a visual system that transitions between different moods through rotating petals, oscillating sizes, and shifting color schemes. The final sketch uses a Fibonacci spiral to control the petal arrangement, creating a dynamic, mesmerizing flower with endless possibilities.

I used colors inspired by the natural world, with gradients of pink, yellow, blue, and green to evoke different emotional states. This color-changing feature makes the flower feel alive, as if it’s reacting to its environment.

Embedded Sketch:

https://editor.p5js.org/ha2297/full/Eo2Yntsgr

Coding Logic and Translation:

The logic behind “Fibonacci Dream” revolves around creating a dynamic, generative artwork using a Fibonacci spiral, with petals that oscillate, rotate, and change color over time. Here’s how each part of the code works:

  1. Fibonacci Spiral Arrangement
    The petals follow a Fibonacci spiral pattern, where each petal’s position is determined by an angle (137.5°) and a radius that grows as the square root of the petal index. This ensures that the petals are evenly spaced and spread outward in a natural spiral.
let angle = i * spiralAngle + time * 10;
let radius = sqrt(i) * 8;
let x = radius * cos(angle);
let y = radius * sin(angle);

2.Oscillation and Rotation
To give the petals life, they oscillate in size using the sine function, creating a smooth breathing effect. The petals also rotate in a spiral, and the speed of rotation is controlled by a user-adjustable slider.

let oscillation = map(sin(time + i), -1, 1, 0.8, 1.2);
let dynamicPetalLength = petalLength * oscillation;
let dynamicPetalWidth = petalWidth * oscillation;
rotate(angle + time);

3.Color Palettes
The code includes six color palettes that the user can switch between. Each palette uses gradients like pink to yellow or blue to green, adding variety to the artwork

switch (colorPalette) {
  case 0:
    colorVal = map(i, 0, numPetals, 255, 100); // Pink to yellow gradient
    fill(255, colorVal, 150, 200);
    break;
  // Other palettes...
}

4.User Interaction
Two sliders let the user control the rotation speed and zoom level, while a button switches between color palettes. These interactive elements make the artwork more engaging and customizable.

rotationSpeedSlider = createSlider(1, 50, 10);
zoomSlider = createSlider(0.5, 2, 1, 0.01);
colorChangeButton = createButton('Switch Color Palette');
colorChangeButton.mousePressed(switchColorPalette);

5. SVG Export
The artwork can be saved as an SVG image by pressing the ‘s’ key, allowing users to export and preserve the visual as a vector graphic.

function keyPressed(){
  if(key == "s" || key =="S"){
    save("image.svg");
  }
}

Code Snippet:

let oscillation = map(sin(time + i), -1, 1, 0.8, 1.2);
let dynamicPetalLength = petalLength * oscillation;
let dynamicPetalWidth = petalWidth * oscillation;

This code snippet is key to creating a “breathing” effect for the petals by making them grow and shrink over time. It uses the sin() function to generate oscillating values between -1 and 1, which are then mapped to a range between 0.8 and 1.2. This controls the petal size fluctuation, where petals shrink to 80% of their original size and grow to 120%, giving the effect of rhythmic breathing.

The combination of time and petal index (i) ensures that each petal oscillates with a slight variation, making the motion feel more organic. The mapped oscillation value is applied to both petal length and width, making the petals dynamically change in size.

This subtle movement brings the generative art to life. Without this oscillation, the flower would feel static and mechanical. The breathing effect, combined with the spiral pattern and color changes, adds a layer of depth, making the artwork more engaging and lifelike.

Challenges:

One of the biggest challenges was ensuring that the oscillation and rotation worked smoothly together, especially when combined with user input. It took several iterations to get the timing and size oscillation just right without breaking the flow of the petals.

Another challenge was handling the zoom function. Initially, zooming affected the positioning of the sliders and button, but I managed to fix this by anchoring them to the screen while zooming the canvas content independently.

Another issue that took some time to resolve was an error I encountered when trying to save the SVG file. Initially, I was using the following HTML file. However, I kept getting an error when trying to export the SVG. After some debugging, I realized that the issue was with the version of the p5.js SVG library I was using. I switched to an older version of the HTML file.The error occurred because the version of p5.js-svg I was initially using wasn’t compatible with the main p5.js version in my project. By switching to an older, more stable version, I was able to fix the issue and successfully save the SVG files.

Pen Plotting Experience:

For the pen plotting experience, I initially wanted to make the image of the flower more interesting by dividing it into two color layers. I chose to have the inner layer in yellow and the outer layer in pink to create a subtle contrast. When I printed it with the pen plotter, I used light blue for the inner part and dark blue for the outer part.

Unfortunately, the video I recorded didn’t save. So, I decided to reprint the image with the same colors and setup. However, while printing the second layer (the outer part in dark blue), the ink started running out—not completely, but enough to create a faded effect. I thought about pausing the process to replace the pen, but I ended up liking the way it looked with the gradual fading. It gave the piece a more unique, unexpected quality, so I let the print finish as it was. This added an element of spontaneity to the final artwork that I hadn’t initially planned for, but really enjoyed.

First Version:
Second Version:

A3 Printed Images:

Areas for Improvement and Future Work
In future versions of this project, I have several ideas to improve and expand on:

  1. More User Interaction:

I want to add mouse or touch gestures, so users can control the flower’s rotation and zoom by dragging or pinching the screen. This would make the interaction more natural and fun, especially on touch devices like tablets.

2. 3D Elements:

I’m thinking of adding 3D effects to make the flower feel more immersive. For example, the petals could rise and fall, creating a sense of depth. This would make the flower seem more realistic, almost like it’s floating on the screen.
I also want to explore how natural forces like gravity or wind could affect the petals in 3D, adding a new layer of interaction and visual complexity.

3. Improving Pen Plotting:

I plan to refine the line quality for smoother transitions between layers of the flower when using the pen plotter.
I want to try different styles like stippling (dot patterns) or crosshatching (overlapping lines) to create shading and color effects similar to the digital version.
Experimenting with different pens and inks, like metallic or gradient colors, could help capture the vibrant look of the digital artwork in physical form.
These improvements will make the project more interactive and visually rich, both in the digital version and the pen-plotted version.

References: 

https://editor.p5js.org/xiao2202/sketches/vkHJQX4gy

https://github.com/Jttpo2/flowers