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

Leave a Reply

Your email address will not be published. Required fields are marked *