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