Polished Modes: The three modes (Growth, Random, Stability) now work seamlessly, offering different visual representations of population dynamics.
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.
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.
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.
Mode Switching: Users can toggle between Growth, Random, and Stability modes using the keyboard.
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.
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
Growth Mode: Populations grow or shrink based on initial fertility and death rates.
Random Mode: Random changes in fertility and death rates cause erratic population behaviors.
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
}
}
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
Growth Mode: Populations grow or shrink based on predefined fertility and death rates.
Random Mode: Random fluctuations in fertility and death rates simulate unpredictable factors like pandemics or booms.
Stability Mode: Regions converge towards the replacement rate, stabilizing population growth.
Design Approach
Grid Layout: A grid layout represents various regions, and each circle symbolizes a region’s population.
Fertility and Death Rates: Each region has different fertility and death rates, randomly assigned initially. These rates determine whether a population grows or declines.
Replacement Rate: In Stability Mode, all populations will stabilize around a fertility rate of 2.1.
Modes: The user can toggle between modes using the keyboard.
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
}
}
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.
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.
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.
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
}
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.
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.
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
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
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:
SVG Implementation
Image Editing
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.
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.
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
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.
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.
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.