Midterm Progress 3

Final Project Refinements

  1. Polished Modes: The three modes (Growth, Random, Stability) now work seamlessly, offering different visual representations of population dynamics.
  2. Final Experimentation: After experimenting with various settings, I settled on a configuration that produces visually appealing population patterns, especially in Stability Mode where populations stabilize.
  3. Plotted Output: The final SVG image generated in Stability Mode resonated with me, as it visually symbolizes population balance. I exported the high-res version and prepared it for printing on A3 sheets.

view code

Midterm Progress 2

Project Development

  1. Fertility & Death Rate Integration: By this phase, I fully incorporated fertility and death rates. Each region now grows or shrinks based on these factors, and in Stability Mode, populations stabilize around a 2.1 replacement rate.
  2. Mode Switching: Users can toggle between Growth, Random, and Stability modes using the keyboard.
  3. Visual Feedback: The size of each circle dynamically adjusts based on the population changes, offering a visual representation of how populations are evolving in each region.
  4. Significant Code Progress: Implemented the full logic for handling different population behaviors across modes.

Code Refinements

In this phase, I added full functionality for fertility and death rates, and populations now stabilize in Stability Mode.

let regions = [];
let mode = 0; // Mode selector

function setup() {
  createCanvas(800, 800);
  let gridSize = 4;
  let spacing = width / gridSize;
  
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      let x = spacing * i + spacing / 2;
      let y = spacing * j + spacing / 2;
      let population = random(50, 200);
      let fertilityRate = random(1.5, 5);
      let deathRate = random(0.5, 3);
      regions.push(new Region(x, y, population, fertilityRate, deathRate));
    }
  }
}

function draw() {
  background(255);
  switch (mode) {
    case 0:
      growthMode();
      break;
    case 1:
      randomMode();
      break;
    case 2:
      stabilityMode();
      break;
  }
}

function keyPressed() {
  if (key === '1') mode = 0;
  if (key === '2') mode = 1;
  if (key === '3') mode = 2;
}

function growthMode() {
  for (let region of regions) {
    region.grow();
    region.display();
  }
}

function randomMode() {
  for (let region of regions) {
    region.randomizeRates();
    region.grow();
    region.display();
  }
}

function stabilityMode() {
  for (let region of regions) {
    region.stabilize();
    region.grow();
    region.display();
  }
}

Three Variations for Export

  1. Growth Mode: Populations grow or shrink based on initial fertility and death rates.
  2. Random Mode: Random changes in fertility and death rates cause erratic population behaviors.
  3. Stability Mode: Populations gradually stabilize around the replacement rate.

I exported these three variations in SVG format using the save() function in p5.js.

function keyPressed() {
  if (key === 's') {
    save(); // Saves the current state as SVG
  }
}

 

Midterm Progress 1

Project Concept

The project mimics population dynamics in different parts of the world by incorporating fertility rates and death rates, allowing each region to grow, shrink, or stabilize based on these parameters. Over time, population levels converge towards a 2.1 replacement rate (the level needed to maintain a stable population). This concept is inspired by real-world demographic transitions, where regions move from rapid population growth to stabilization.

  • Regions as Grid: A grid of circles represents different global regions. Each circle’s size correlates to the population size, while fertility and death rates drive the growth or shrinkage.
  • Fertility Rate: Controls how quickly a region’s population expands.
  • Death Rate: Controls how quickly populations decline.
  • Replacement Rate: Populations converge towards a stable size (replacement rate of 2.1 children per woman), creating patterns of population stability.

Modes of the System

  1. Growth Mode: Populations grow or shrink based on predefined fertility and death rates.
  2. Random Mode: Random fluctuations in fertility and death rates simulate unpredictable factors like pandemics or booms.
  3. Stability Mode: Regions converge towards the replacement rate, stabilizing population growth.

Design Approach

  1. Grid Layout: A grid layout represents various regions, and each circle symbolizes a region’s population.
  2. Fertility and Death Rates: Each region has different fertility and death rates, randomly assigned initially. These rates determine whether a population grows or declines.
  3. Replacement Rate: In Stability Mode, all populations will stabilize around a fertility rate of 2.1.
  4. Modes: The user can toggle between modes using the keyboard.
  5. Visualization: Circle size visualizes population changes, expanding during growth and shrinking during decline.

Most Complex Part

Handling the stabilization towards the 2.1 replacement rate is the most uncertain part. Populations should gradually approach equilibrium without abrupt changes. I’ll mitigate this risk by implementing gradual transitions in the stabilization mode, where the fertility and death rates are slowly adjusted.

Initial Code and Progress (Growth Logic)

At this phase, I implemented the basic grid and growth logic, focusing on population growth and shrinkage. I designed classes and functions that would later incorporate fertility and death rates.

class Region {
  constructor(x, y, population, fertilityRate, deathRate) {
    this.x = x;
    this.y = y;
    this.population = population;
    this.fertilityRate = fertilityRate;
    this.deathRate = deathRate;
  }

  grow() {
    let growthFactor = this.fertilityRate - this.deathRate;
    this.population += growthFactor;
    this.population = constrain(this.population, 10, 300);
  }

  display() {
    fill(100, 150, 200);
    ellipse(this.x, this.y, this.population);
  }

  randomizeRates() {
    this.fertilityRate = random(1.5, 5); // Random fertility rate
    this.deathRate = random(0.5, 3); // Random death rate
  }

  stabilize() {
    this.fertilityRate = lerp(this.fertilityRate, 2.1, 0.05); // Gradual shift towards replacement rate
    this.deathRate = lerp(this.deathRate, 1.9, 0.05); // Shift death rate to balance fertility
  }
}

 

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.