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