Algorithmic Bloom

This sketch generates an elegant fractal tree that grows recursively with branches tapering in length and color. The tree animates continuously with noise-driven swaying motion, set against a softly shifting sky gradient and textured ground, with seasonal color palettes you can cycle through by pressing keys.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the tree bushier — Increasing the maximum recursion depth adds more branching levels, creating a denser, fuller tree with finer details.
  2. Slow down the animation — Smaller noise offset increments create slower, more subtle swaying and color shifting for a calmer effect.
  3. Thinner branches — Reducing the stroke weight multiplier makes branches appear delicate and fine-lined.
  4. More dramatic leaf clusters — Drawing more leaf ellipses per cluster creates denser, more prominent foliage at branch endpoints.
  5. Wider sky shimmer range — Increasing the noise range (±20 to ±50) makes the sky's color shifts more pronounced and dramatic.
Prefer the full editor? Open it there →

📖 About This Sketch

Algorithmic Bloom creates a living, breathing fractal tree that grows from the bottom of the canvas upward, its branches splitting recursively and tapering as they climb. The tree never stops moving—branches sway organically thanks to Perlin noise, the sky and ground shift through subtle color variations, and the entire scene cycles through four seasonal color palettes (daytime blues, sunset oranges, winter whites, and spring pastels). The sketch demonstrates three essential p5.js techniques: recursive function calls to generate complex branching structures, Perlin noise (via the noise() function) to create natural-looking continuous motion, and dynamic color gradients built line-by-line using lerpColor().

The code is organized around a recursive branch() function that calls itself to create deeper and deeper levels of the tree, stopping when branches become too small or reach maximum depth. The draw loop runs sixty times per second, continuously updating noise offsets to animate the tree and redrawing the sky and ground with shifting colors. By studying this sketch, you will learn how recursion creates intricate natural forms, how Perlin noise drives organic motion, how to layer gradients for visual depth, and how keyboard input lets users explore variations of a generative artwork.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas in radians mode, initializes four color palettes, picks the first palette, and seeds six random noise offsets (for angles, reduction factors, and color shifts). The initial branch length is set to 25% of the canvas height.
  2. Every frame, draw() paints a dynamic sky gradient from top to bottom by interpolating between two sky colors and adding noise to each RGB component for subtle shimmer. It then draws a textured ground at the bottom with similar noise-based color variation.
  3. The translate() call repositions the drawing origin to the bottom center of the canvas, then branch() is called with the full initial length and depth 0, starting the recursive tree generation.
  4. Inside branch(), a horizontal line is drawn from the current position, the origin moves upward to the branch's tip, and then the function checks if the branch is short enough or deep enough to stop (leaf endpoint condition).
  5. If the branch continues, the function reads mouseX and mouseY to dynamically adjust the splitting angle and reduction factor, adds noise-based variations on top, and recursively calls itself twice—once for a right-leaning child branch and once for a left-leaning one, each rotated and shortened.
  6. Leaf clusters are drawn at branch endpoints using random-offset ellipses. The noise offsets increment every frame, causing smooth oscillation in angles, reduction factors, and colors. Pressing 'P' cycles to the next palette; pressing 'N' picks a random palette and reinitializes all noise offsets for a completely new tree.

🎓 Concepts You'll Learn

Recursive functionsPerlin noise animationDynamic color gradientsTransformations (translate, rotate, push/pop)Tree data structures and fractalsKeyboard interactionColor interpolation with lerpColor

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. Use it to initialize your canvas, set drawing modes, load data, and seed starting values that will change how the sketch looks and behaves.

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(RADIANS); // Ensure angles are in radians (default, but good to be explicit)
  strokeCap(ROUND); // Rounded ends for branches, for a softer look

  // Load the initial color palette
  loadPalette(currentPaletteIndex);

  // Initialize noise offsets with random values for varied starting animations
  angleNoiseOffset = random(1000);
  reductionFactorNoiseOffset = random(1000);
  skyNoiseOffset1 = random(1000);
  skyNoiseOffset2 = random(1000);
  groundNoiseOffset = random(1000); // NEW: Initialize ground noise offset

  // Initialize the initial branch length based on the canvas height
  branchLength = height * 0.25;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

initialization Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a full-window canvas that fills the entire browser window

initialization Initial Palette Load loadPalette(currentPaletteIndex);

Loads the first color palette into global color variables

initialization Noise Offset Randomization angleNoiseOffset = random(1000); reductionFactorNoiseOffset = random(1000); skyNoiseOffset1 = random(1000); skyNoiseOffset2 = random(1000); groundNoiseOffset = random(1000);

Seeds six independent random starting points for Perlin noise, ensuring different animation patterns each time the sketch runs

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window—the tree and sky will scale responsively to any screen size
angleMode(RADIANS);
Tells p5.js to interpret all angles in radians (0 to 2π) rather than degrees—important for rotate() and angle calculations in branch()
strokeCap(ROUND);
Makes the ends of drawn lines rounded instead of square, giving branches a softer, more natural appearance
loadPalette(currentPaletteIndex);
Calls a helper function to load the initial palette's colors into the global color variables like skyColor1, branchBaseColor, and leafColor
angleNoiseOffset = random(1000);
Seeds angleNoiseOffset with a random value between 0 and 1000—this ensures the noise pattern starts at a different point each time, creating variety
branchLength = height * 0.25;
Sets the trunk length to 25% of the canvas height, scaling the tree proportionally to any window size

draw()

draw() runs 60 times per second by default. Everything drawn here is erased and redrawn each frame—that's how animation works in p5.js. The small noise offset increments are the secret: they feed new values to noise() each frame, creating smooth, natural-looking continuous motion.

🔬 These lines control how fast the noise offsets change each frame. What happens if you multiply all five of these increments by 0 (making them += 0)? What if you multiply them all by 2?

  angleNoiseOffset += 0.005;
  reductionFactorNoiseOffset += 0.005;
  skyNoiseOffset1 += 0.002;
  skyNoiseOffset2 += 0.003;
  groundNoiseOffset += 0.001;
function draw() {
  drawSkyGradient(); // Draw the gradient sky background with dynamic colors
  drawGroundGradient(); // Draw the ground at the bottom

  // Draw the artist's handle BEFORE the main translation for the tree
  drawArtistHandle();

  translate(width / 2, height - (height * 0.1)); // Move origin to bottom center, slightly above ground
  branch(branchLength, 0); // Start drawing the tree from the main trunk (depth 0)

  // Update noise offsets for continuous, subtle animation over time
  angleNoiseOffset += 0.005;
  reductionFactorNoiseOffset += 0.005;
  skyNoiseOffset1 += 0.002;
  skyNoiseOffset2 += 0.003;
  groundNoiseOffset += 0.001; // NEW: Animate ground noise offset
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

initialization Sky Gradient Draw drawSkyGradient();

Renders the animated background gradient sky

initialization Ground Gradient Draw drawGroundGradient();

Renders the textured ground plane

transformation Tree Origin Translation translate(width / 2, height - (height * 0.1));

Moves the origin to the bottom center of the canvas so the tree trunk grows upward from the ground

function-call Recursive Tree Generation branch(branchLength, 0);

Initiates the recursive branching algorithm starting from the full trunk length at depth 0

calculation Continuous Noise Animation angleNoiseOffset += 0.005; reductionFactorNoiseOffset += 0.005; skyNoiseOffset1 += 0.002; skyNoiseOffset2 += 0.003; groundNoiseOffset += 0.001;

Increments each noise offset every frame, creating smooth continuous variation in branch angles, reduction factors, and sky/ground colors

drawSkyGradient();
Calls the helper function to render a color-shifting sky by drawing many thin horizontal lines with interpolated colors
drawGroundGradient();
Calls the helper function to render the textured ground at the bottom of the canvas
drawArtistHandle();
Renders the artist's name in the bottom-right corner with color that adapts to the sky brightness
translate(width / 2, height - (height * 0.1));
Repositions the drawing origin (0, 0) to the bottom-center of the canvas, 10% above the very bottom where the tree base should start
branch(branchLength, 0);
Calls the recursive branch() function with the full initial trunk length and depth 0, which will draw the entire tree by recursively splitting and rotating
angleNoiseOffset += 0.005;
Increments the angle noise offset by a tiny amount each frame, causing the noise() function to return new values next frame and creating subtle swaying motion
reductionFactorNoiseOffset += 0.005;
Similarly increments the reduction factor noise offset, animating how much each branch shrinks relative to its parent
skyNoiseOffset1 += 0.002;
Increments the first sky color noise offset at a slightly slower rate, creating gentle color shifts in the top of the sky gradient
skyNoiseOffset2 += 0.003;
Increments the second sky color noise offset at a different rate, causing the bottom sky color to shift independently for depth
groundNoiseOffset += 0.001;
Increments the ground noise offset at the slowest rate, subtly animating the texture and variation in the ground plane

branch(len, depth)

branch() is the heart of this sketch—it's a recursive function that calls itself to create self-similar branching structures. Each call is slightly different because of noise and mouse position, creating organic variation. Recursion works here because we have a clear stopping condition (minBranchLength and maxDepth), preventing infinite loops.

🔬 Right now each branch splits into exactly two child branches (left and right). What happens if you duplicate the right-branch block so it appears three times in a row (before the left branch block)? You'd have three right branches and one left—an asymmetric tree!

    push();
    rotate(dynamicAngle);
    branch(len * dynamicReductionFactor, depth + 1);
    pop();

    // Recursive call for the left branch
    push();
    rotate(-dynamicAngle);
    branch(len * dynamicReductionFactor, depth + 1);
    pop();
function branch(len, depth) {
  // Determine branch color based on its depth in the tree
  let branchColor = lerpColor(branchBaseColor, branchTipColor, depth / maxDepth);
  stroke(branchColor); // Set the branch color

  // Branch thickness tapers with length
  strokeWeight(len * 0.05);
  line(0, 0, 0, -len); // Draw the branch line

  translate(0, -len); // Move the origin to the end of the current branch

  // If this branch is an endpoint (small enough OR reached max depth), draw leaves and stop recursion
  if ((len <= minBranchLength && depth > 3) || depth >= maxDepth) {
    drawLeaves(len); // Draw a cluster of leaves
    return; // Stop further recursion for this branch
  }

  // If the branch is still long enough and within max depth, continue branching recursively
  if (len > minBranchLength && depth < maxDepth) {
    // Determine dynamic angle for branching based on mouseX and noise
    let dynamicAngle = map(mouseX, 0, width, PI / 8, PI / 2);
    // Rely solely on noise for continuous, organic variation in angle
    dynamicAngle += map(noise(angleNoiseOffset + depth * 0.1), 0, 1, -PI / 16, PI / 16);
    dynamicAngle = constrain(dynamicAngle, PI / 8, PI / 2); // Keep angle within reasonable bounds

    // Determine dynamic reduction factor based on mouseY and noise
    let dynamicReductionFactor = map(mouseY, 0, height, 0.5, 0.8);
    // Rely solely on noise for continuous, organic variation in reduction factor
    dynamicReductionFactor += map(noise(reductionFactorNoiseOffset + depth * 0.1), 0, 1, -0.05, 0.05);
    dynamicReductionFactor = constrain(dynamicReductionFactor, 0.5, 0.8); // Keep reduction factor within bounds

    // Recursive call for the right branch
    push();
    rotate(dynamicAngle);
    branch(len * dynamicReductionFactor, depth + 1);
    pop();

    // Recursive call for the left branch
    push();
    rotate(-dynamicAngle);
    branch(len * dynamicReductionFactor, depth + 1);
    pop();
  }
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

calculation Color Gradient Based on Depth let branchColor = lerpColor(branchBaseColor, branchTipColor, depth / maxDepth);

Smoothly transitions branch color from dark (thick trunk) to light (thin twigs) based on recursion depth

drawing Branch Line Drawing line(0, 0, 0, -len);

Draws a vertical line upward from the current origin, creating the visible branch segment

conditional Recursion Termination Check if ((len <= minBranchLength && depth > 3) || depth >= maxDepth) {

Stops recursion when either the branch becomes too short (and is deep enough) or maximum depth is reached, preventing infinite loops

calculation Dynamic Branch Angle let dynamicAngle = map(mouseX, 0, width, PI / 8, PI / 2); dynamicAngle += map(noise(angleNoiseOffset + depth * 0.1), 0, 1, -PI / 16, PI / 16);

Combines mouse position and Perlin noise to create both interactive and smoothly animated branch splitting angles

calculation Dynamic Reduction Factor let dynamicReductionFactor = map(mouseY, 0, height, 0.5, 0.8); dynamicReductionFactor += map(noise(reductionFactorNoiseOffset + depth * 0.1), 0, 1, -0.05, 0.05);

Blends mouse Y position and noise to animate how much each child branch shrinks relative to its parent

recursion Right Branch Recursive Call push(); rotate(dynamicAngle); branch(len * dynamicReductionFactor, depth + 1); pop();

Saves the current transformation, rotates right, recursively draws a shorter right branch, then restores the transformation

recursion Left Branch Recursive Call push(); rotate(-dynamicAngle); branch(len * dynamicReductionFactor, depth + 1); pop();

Saves the current transformation, rotates left, recursively draws a shorter left branch, then restores the transformation

let branchColor = lerpColor(branchBaseColor, branchTipColor, depth / maxDepth);
lerpColor() blends between two colors smoothly—at depth 0 it returns branchBaseColor (dark), at maxDepth it returns branchTipColor (light), and at depths in between it returns a blend. Deeper branches become lighter.
stroke(branchColor);
Sets the color for all subsequent lines drawn until stroke() is called again
strokeWeight(len * 0.05);
Makes the branch thickness proportional to its length—thicker for long branches, thinner for short ones, creating a natural taper
line(0, 0, 0, -len);
Draws a straight line upward (negative y) with length len, from the current origin point to the tip of this branch
translate(0, -len);
Moves the origin to the end of the branch—all future drawing happens relative to this new position, so child branches grow from the branch tip
if ((len <= minBranchLength && depth > 3) || depth >= maxDepth) {
Checks if this is a leaf endpoint: either the branch is very short AND deep enough in the tree, OR the maximum depth is reached. If either is true, we stop recursing.
drawLeaves(len);
Draws a cluster of leaf ellipses at this branch tip
return;
Immediately exits this function call, preventing any further recursion down this branch
if (len > minBranchLength && depth < maxDepth) {
Only continues to the recursion code if the branch is still long enough and we haven't hit the depth limit
let dynamicAngle = map(mouseX, 0, width, PI / 8, PI / 2);
map() scales mouseX from 0-width onto PI/8 to PI/2 radians (22.5° to 90°)—moving the mouse right increases the angle, making branches spread wider
dynamicAngle += map(noise(angleNoiseOffset + depth * 0.1), 0, 1, -PI / 16, PI / 16);
noise() returns a smooth random value between 0 and 1. map() scales this onto ±PI/16 and adds it to the angle, creating organic oscillation. Deeper branches get slightly different noise values (depth * 0.1) so the whole tree sways as a unit.
dynamicAngle = constrain(dynamicAngle, PI / 8, PI / 2);
constrain() clamps the angle to stay between PI/8 and PI/2, preventing the branches from closing up too tightly or spreading unreasonably wide
let dynamicReductionFactor = map(mouseY, 0, height, 0.5, 0.8);
map() scales mouseY from 0-height onto 0.5-0.8—moving the mouse down makes each branch shrink more aggressively, moving it up makes branches stay longer
dynamicReductionFactor += map(noise(reductionFactorNoiseOffset + depth * 0.1), 0, 1, -0.05, 0.05);
Adds a noise-based variation (±0.05) to the reduction factor, creating subtle animation in how quickly branches taper
dynamicReductionFactor = constrain(dynamicReductionFactor, 0.5, 0.8);
Keeps the reduction factor bounded so branches can't fail to shrink (< 0.5) or fail to eventually stop (> 0.8)
push();
Saves the current transformation state (origin position, rotation, etc.) so we can apply the right-branch rotation without affecting the left branch
rotate(dynamicAngle);
Rotates the coordinate system by the dynamic angle in the positive (right/clockwise) direction
branch(len * dynamicReductionFactor, depth + 1);
Recursively calls branch() with a shorter length (multiplied by reduction factor) and a deeper depth level, drawing the right child branch in the rotated space
pop();
Restores the transformation to what it was before push(), undoing the right rotation so the coordinate system is ready for the left branch
rotate(-dynamicAngle);
Rotates in the negative (left/counter-clockwise) direction by the same angle, creating a symmetric left branch

drawLeaves(len)

drawLeaves() is called at every branch endpoint to create a visual accent. The random() function is key here—it ensures no two leaf clusters look exactly the same, adding organic variation. The push()/pop() pair is a safety pattern that isolates randomness to this single leaf.

🔬 This line draws the ellipse with random offsets (±leafSize/8) and random sizes (0.7-1.2 width, 0.4-0.8 height). What happens if you remove all the random() calls and use fixed values like 0 for position and 1.0 for size? The leaves will be identical and perfectly centered—very artificial-looking.

    ellipse(random(-leafSize / 8, leafSize / 8), random(-leafSize / 8, leafSize / 8), leafSize * random(0.7, 1.2), leafSize * random(0.4, 0.8));
function drawLeaves(len) {
  fill(leafColor); // Set leaf color with transparency
  noStroke(); // No outline for leaves

  // Draw multiple small ellipses to create a fuller leaf cluster
  let leafSize = len * 0.6;
  for (let i = 0; i < 3; i++) {
    push(); // Save current transformation for each leaf ellipse
    // NEW: Random x and y offsets are now applied relative to the current branch tip
    ellipse(random(-leafSize / 8, leafSize / 8), random(-leafSize / 8, leafSize / 8), leafSize * random(0.7, 1.2), leafSize * random(0.4, 0.8));
    pop(); // Restore transformation for the next leaf or branch
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Leaf Cluster Loop for (let i = 0; i < 3; i++) {

Draws three overlapping ellipses with random size and position to create a fuller, more natural-looking leaf cluster

calculation Random Leaf Offsets ellipse(random(-leafSize / 8, leafSize / 8), random(-leafSize / 8, leafSize / 8), leafSize * random(0.7, 1.2), leafSize * random(0.4, 0.8));

Places and sizes each leaf ellipse with random variations to avoid a uniform, artificial appearance

fill(leafColor);
Sets the fill color for all subsequent shapes. leafColor is a palette color that includes transparency (alpha), so leaves blend softly with the background.
noStroke();
Disables outlines for the leaf ellipses, creating a softer, more natural silhouette
let leafSize = len * 0.6;
Scales the leaf cluster size based on the branch length—shorter branches get smaller leaf clusters, longer endpoints get larger ones
for (let i = 0; i < 3; i++) {
Loops three times, drawing three overlapping leaf ellipses to create visual fullness and texture
push();
Saves the current transformation before applying random offsets to this leaf, so each leaf can be positioned independently
ellipse(random(-leafSize / 8, leafSize / 8), random(-leafSize / 8, leafSize / 8), leafSize * random(0.7, 1.2), leafSize * random(0.4, 0.8));
Draws an ellipse with random x and y offsets (±leafSize/8), and random width and height (70-120% and 40-80% of leafSize), creating natural variation in the leaf cluster appearance
pop();
Restores the transformation, preparing for the next loop iteration and ensuring each leaf's randomness doesn't affect the next one

drawSkyGradient()

This function draws a smooth gradient by layering thin horizontal lines, each with a slightly different color. The noise adds shimmer and organic motion to the sky without making it chaotic. This is a beautiful technique for creating dynamic environments.

🔬 These three lines add noise variation (±20 units) to each color channel. What happens if you change -20, 20 to -50, 50? The sky will shimmer much more intensely. What if you change it to 0, 0 (removing the noise)?

    let c1R = red(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001), 0, 1, -20, 20);
    let c1G = green(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001 + 100), 0, 1, -20, 20);
    let c1B = blue(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001 + 200), 0, 1, -20, 20);
function drawSkyGradient() {
  noStroke(); // No outline for the gradient lines
  for (let y = 0; y < height * 0.9; y++) { // Draw sky up to 90% of the canvas height
    // Interpolate color from skyColor1 (top) to skyColor2 (bottom)
    // Add noise to each color component for subtle, dynamic shifts
    let c1R = red(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001), 0, 1, -20, 20);
    let c1G = green(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001 + 100), 0, 1, -20, 20);
    let c1B = blue(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001 + 200), 0, 1, -20, 20);
    let dynamicSkyColor1 = color(c1R, c1G, c1B);

    let c2R = red(skyColor2) + map(noise(skyNoiseOffset2 + y * 0.001), 0, 1, -20, 20);
    let c2G = green(skyColor2) + map(noise(skyNoiseOffset2 + y * 0.001 + 100), 0, 1, -20, 20);
    let c2B = blue(skyColor2) + map(noise(skyNoiseOffset2 + y * 0.001 + 200), 0, 1, -20, 20);
    let dynamicSkyColor2 = color(c2R, c2G, c2B);

    let c = lerpColor(dynamicSkyColor1, dynamicSkyColor2, y / (height * 0.9));
    stroke(c); // Set the stroke color to the interpolated color
    line(0, y, width, y); // Draw a horizontal line to create the gradient
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Pixel-by-Pixel Gradient Loop for (let y = 0; y < height * 0.9; y++) {

Iterates through every y pixel from top to 90% down, drawing thin horizontal lines that form a smooth gradient when combined

calculation Dynamic Color with Noise let c1R = red(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001), 0, 1, -20, 20);

Extracts the red component of the base sky color and adds noise-based variation (±20 units) to create a shimmering effect

calculation Vertical Color Interpolation let c = lerpColor(dynamicSkyColor1, dynamicSkyColor2, y / (height * 0.9));

Blends the top sky color into the bottom sky color smoothly across the y pixels, creating the gradient effect

noStroke();
Disables outlines so the horizontal gradient lines blend seamlessly
for (let y = 0; y < height * 0.9; y++) {
Loops from y=0 (top) to y=height*0.9 (90% down), processing each horizontal row of the sky
let c1R = red(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001), 0, 1, -20, 20);
red() extracts just the red channel of skyColor1; noise() generates a smooth random value based on the offset and y position; map() scales that 0-1 noise value onto ±20; the sum creates a red channel that varies from the base by up to ±20 units
let c1G = green(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001 + 100), 0, 1, -20, 20);
Same process for the green channel, but the noise input is offset by +100 so it varies independently from red, creating natural color shifts
let c1B = blue(skyColor1) + map(noise(skyNoiseOffset1 + y * 0.001 + 200), 0, 1, -20, 20);
Same for blue, offset by +200, so all three color channels shimmer independently
let dynamicSkyColor1 = color(c1R, c1G, c1B);
Constructs a new color from the three noise-adjusted channels, creating a shimmering version of skyColor1
let dynamicSkyColor2 = color(c2R, c2G, c2B);
Does the same shimmer process for skyColor2 (the bottom sky), using a different noise offset (skyNoiseOffset2) so top and bottom shift independently
let c = lerpColor(dynamicSkyColor1, dynamicSkyColor2, y / (height * 0.9));
lerpColor() blends dynamicSkyColor1 and dynamicSkyColor2 based on the y position—at y=0 it returns color 1, at y=height*0.9 it returns color 2, and in between it smoothly blends
stroke(c);
Sets the line color to the interpolated shimmering gradient color
line(0, y, width, y);
Draws a thin horizontal line across the full width of the canvas at position y, using the gradient color

drawGroundGradient()

drawGroundGradient() is nearly identical to drawSkyGradient(), but operates on the bottom 10% of the canvas. Notice how smaller noise variation (±10 instead of ±20) creates more subtle texture in the ground—a subtle design choice that makes the ground feel solid and less chaotic than the sky.

function drawGroundGradient() {
  noStroke();
  for (let y = height * 0.9; y < height; y++) { // Draw ground from 90% height to bottom
    let c = lerpColor(groundColor1, groundColor2, (y - height * 0.9) / (height * 0.1));
    // NEW: Add noise to each color component for subtle ground texture
    let cR = red(c) + map(noise(groundNoiseOffset + y * 0.01), 0, 1, -10, 10);
    let cG = green(c) + map(noise(groundNoiseOffset + y * 0.01 + 100), 0, 1, -10, 10);
    let cB = blue(c) + map(noise(groundNoiseOffset + y * 0.01 + 200), 0, 1, -10, 10);
    stroke(color(cR, cG, cB));
    line(0, y, width, y);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Ground Gradient Loop for (let y = height * 0.9; y < height; y++) {

Iterates from 90% down to the bottom of the canvas, drawing thin lines that form the textured ground

calculation Ground Color Blend let c = lerpColor(groundColor1, groundColor2, (y - height * 0.9) / (height * 0.1));

Smoothly transitions the ground color from a lighter shade at the top to a darker shade at the bottom

calculation Ground Texture Noise let cR = red(c) + map(noise(groundNoiseOffset + y * 0.01), 0, 1, -10, 10);

Adds subtle noise-based variation to the ground color to create texture and natural-looking variation

noStroke();
Disables outlines for seamless blending of ground lines
for (let y = height * 0.9; y < height; y++) {
Loops from y=height*0.9 (where sky ends) to y=height (bottom of canvas), covering the ground area
let c = lerpColor(groundColor1, groundColor2, (y - height * 0.9) / (height * 0.1));
lerpColor() blends groundColor1 (lighter, at top) to groundColor2 (darker, at bottom). The ratio (y - height*0.9) / (height*0.1) starts at 0 and increases to 1 as y moves from top to bottom of the ground area
let cR = red(c) + map(noise(groundNoiseOffset + y * 0.01), 0, 1, -10, 10);
Extracts the red channel and adds noise variation (±10 units). The y * 0.01 makes different rows have different noise values for texture.
let cG = green(c) + map(noise(groundNoiseOffset + y * 0.01 + 100), 0, 1, -10, 10);
Same for green, with offset +100 to vary independently from red
let cB = blue(c) + map(noise(groundNoiseOffset + y * 0.01 + 200), 0, 1, -10, 10);
Same for blue, with offset +200
stroke(color(cR, cG, cB));
Constructs a color from the noise-adjusted RGB values and sets it as the stroke color
line(0, y, width, y);
Draws a thin horizontal line across the full width at position y, creating the textured ground effect

drawArtistHandle()

This utility function demonstrates thoughtful UI design: the adaptive text color ensures readability no matter what colors the palette uses. The brightness() function is a practical tool for making your sketch respond intelligently to its own color values.

function drawArtistHandle() {
  push(); // Save current drawing state (which is the canvas's default origin)
  textAlign(RIGHT, BOTTOM); // Align text to the bottom right
  textSize(12); // Small text size

  // Dynamically choose text color based on the current sky brightness
  let currentSkyBrightness = brightness(lerpColor(skyColor1, skyColor2, 0.5));
  let textColor = currentSkyBrightness > 127 ? color(50) : color(220); // Dark text on light sky, light text on dark sky

  fill(textColor); // Set text color
  noStroke(); // No stroke for the text
  // Position it with a small margin from the bottom and right edges
  text(artistHandle, width - 10, height - 10);
  pop(); // Restore previous drawing state (not strictly necessary here as no transformations were applied)
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Adaptive Text Color let textColor = currentSkyBrightness > 127 ? color(50) : color(220);

Chooses between dark and light text depending on the sky's brightness, ensuring good contrast and readability

push();
Saves the current drawing state before making any changes specific to the text drawing
textAlign(RIGHT, BOTTOM);
Aligns text so its right edge and bottom edge are at the coordinates passed to text()—this makes positioning easier in the corner
textSize(12);
Sets the font size to 12 pixels, suitable for small credit text
let currentSkyBrightness = brightness(lerpColor(skyColor1, skyColor2, 0.5));
lerpColor() blends the two sky colors midway; brightness() returns a value 0-255 indicating how bright that color is. This tells us if the sky is currently light or dark.
let textColor = currentSkyBrightness > 127 ? color(50) : color(220);
Ternary operator: if sky brightness > 127 (light), use dark gray (50); otherwise use light gray (220). This ensures text is always readable against the sky.
fill(textColor);
Sets the text color to the adaptively chosen color
noStroke();
Disables outlines on the text for a clean appearance
text(artistHandle, width - 10, height - 10);
Draws the artist's name string 10 pixels from the right and bottom edges of the canvas
pop();
Restores the drawing state, though this function doesn't apply transformations that would affect later drawing

loadPalette(index)

loadPalette() is a helper that decouples color data from drawing logic. The palettes array stores RGB values as plain arrays, and this function converts them to p5.Color objects for use throughout the sketch. This pattern makes it easy to swap entire color schemes with a single function call.

function loadPalette(index) {
  const palette = palettes[index];
  skyColor1 = color(palette.sky1);
  skyColor2 = color(palette.sky2);
  groundColor1 = color(palette.ground1);
  groundColor2 = color(palette.ground2);
  branchBaseColor = color(palette.branchBase);
  branchTipColor = color(palette.branchTip);
  leafColor = color(palette.leaf);
}
Line-by-line explanation (8 lines)
const palette = palettes[index];
Retrieves one palette object from the palettes array using the provided index (0-3)
skyColor1 = color(palette.sky1);
Extracts the RGB array from the palette object and converts it to a p5.Color object, then stores it in the global skyColor1 variable
skyColor2 = color(palette.sky2);
Loads the bottom sky color from the palette
groundColor1 = color(palette.ground1);
Loads the lighter ground color
groundColor2 = color(palette.ground2);
Loads the darker ground color
branchBaseColor = color(palette.branchBase);
Loads the dark color used for thick trunk branches
branchTipColor = color(palette.branchTip);
Loads the light color used for thin twig tips
leafColor = color(palette.leaf);
Loads the leaf/flower color, which includes alpha transparency for soft blending

keyPressed()

keyPressed() is a built-in p5.js function that runs whenever the user presses a key. By reinitializing noise offsets after changing palettes, the sketch ensures each palette sees a fresh tree animation. This is a simple but effective way to give users control over exploration.

function keyPressed() {
  if (key === 'p' || key === 'P') { // Cycle through palettes when 'P' is pressed
    currentPaletteIndex = (currentPaletteIndex + 1) % palettes.length;
    loadPalette(currentPaletteIndex);
    // Re-initialize noise offsets for a new tree appearance with the new palette
    angleNoiseOffset = random(1000);
    reductionFactorNoiseOffset = random(1000);
    skyNoiseOffset1 = random(1000);
    skyNoiseOffset2 = random(1000);
    groundNoiseOffset = random(1000); // NEW: Re-initialize ground noise offset
  } else if (key === 'n' || key === 'N') { // Generate a new tree (random palette, new noise) when 'N' is pressed
    // Pick a random palette index
    currentPaletteIndex = floor(random(palettes.length));
    loadPalette(currentPaletteIndex); // Load the new random palette
    // Re-initialize all noise offsets for a completely new tree appearance
    angleNoiseOffset = random(1000);
    reductionFactorNoiseOffset = random(1000);
    skyNoiseOffset1 = random(1000);
    skyNoiseOffset2 = random(1000);
    groundNoiseOffset = random(1000); // NEW: Re-initialize ground noise offset
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Palette Cycling (P key) if (key === 'p' || key === 'P') {

Detects the P key and moves to the next palette in the list, wrapping around to the first after the last

conditional Random Tree Generation (N key) } else if (key === 'n' || key === 'N') {

Detects the N key and picks a completely random palette and new noise offsets for a fresh tree

if (key === 'p' || key === 'P') {
Checks if the user pressed P or p (both uppercase and lowercase work)
currentPaletteIndex = (currentPaletteIndex + 1) % palettes.length;
Increments the palette index by 1, and uses modulo (%) to wrap back to 0 after reaching the last palette. With 4 palettes: 0→1→2→3→0...
loadPalette(currentPaletteIndex);
Loads the new palette's colors into the global color variables
angleNoiseOffset = random(1000);
Reinitializes the angle noise offset with a new random value, causing the tree to animate differently under the new palette
} else if (key === 'n' || key === 'N') {
Checks if the user pressed N or n
currentPaletteIndex = floor(random(palettes.length));
Picks a random integer from 0 to 3 (since palettes.length is 4), selecting a random palette instead of cycling to the next one

windowResized()

windowResized() is a built-in p5.js function that runs automatically whenever the user resizes the browser window. Without it, the canvas and tree would be stuck at their initial size. This function makes Algorithmic Bloom responsive to any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // Resize the canvas to fit the new window size
  branchLength = height * 0.25; // Recalculate the initial branch length for responsiveness
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new browser window dimensions, ensuring the sketch fills the screen even after a resize
branchLength = height * 0.25;
Recalculates the trunk length based on the new canvas height, keeping the tree proportionally scaled to the window

📦 Key Variables

branchLength number

Stores the initial length of the main trunk, which is recalculated based on canvas height to scale the tree proportionally

let branchLength; // Assigned in setup() as height * 0.25
baseReductionFactor number

Controls the base ratio by which each child branch is shorter than its parent (67% by default)—determines overall tree shape and tapering

let baseReductionFactor = 0.67;
minBranchLength number

The minimum length a branch can reach before the recursion stops and leaves are drawn instead—controls detail level

let minBranchLength = 4;
maxDepth number

The maximum recursion depth—prevents infinite recursion and controls tree complexity and performance

let maxDepth = 10;
angleNoiseOffset number

A continuously updating offset for Perlin noise that controls branch splitting angles, creating organic swaying motion

let angleNoiseOffset; // Initialized in setup() and incremented in draw()
reductionFactorNoiseOffset number

A continuously updating offset for Perlin noise that controls the branch length reduction factor, adding subtle variation

let reductionFactorNoiseOffset; // Initialized in setup() and incremented in draw()
skyNoiseOffset1 number

Offset for Perlin noise applied to the top sky color gradient, creating shimmer and organic color shifts

let skyNoiseOffset1; // Initialized in setup() and incremented in draw()
skyNoiseOffset2 number

Offset for Perlin noise applied to the bottom sky color gradient, animating it independently from the top

let skyNoiseOffset2; // Initialized in setup() and incremented in draw()
groundNoiseOffset number

Offset for Perlin noise applied to the ground color gradient, creating subtle texture and motion in the ground plane

let groundNoiseOffset; // Initialized in setup() and incremented in draw()
skyColor1 p5.Color

The top color of the sky gradient, loaded from the current palette and animated with noise

let skyColor1; // Assigned by loadPalette()
skyColor2 p5.Color

The bottom color of the sky gradient, loaded from the current palette

let skyColor2; // Assigned by loadPalette()
groundColor1 p5.Color

The lighter top color of the ground gradient, loaded from the current palette

let groundColor1; // Assigned by loadPalette()
groundColor2 p5.Color

The darker bottom color of the ground gradient, loaded from the current palette

let groundColor2; // Assigned by loadPalette()
branchBaseColor p5.Color

The dark color used for thick trunk branches, interpolated toward lighter colors as branches taper

let branchBaseColor; // Assigned by loadPalette()
branchTipColor p5.Color

The light color used for the thinnest twig tips, creating a natural color transition from base to tip

let branchTipColor; // Assigned by loadPalette()
leafColor p5.Color

The color of leaf/flower ellipses at branch endpoints, includes alpha transparency for soft blending

let leafColor; // Assigned by loadPalette()
palettes array of objects

Stores four color palettes representing daytime, sunset, winter, and spring, each with sky, ground, and branch colors

const palettes = [{sky1: [173, 216, 230], ...}, {...}, {...}, {...}];
currentPaletteIndex number

Tracks which palette is currently active (0-3), changed by pressing P to cycle or N to randomize

let currentPaletteIndex = 0;
artistHandle string

The artist credit string displayed in the bottom-right corner of the canvas

const artistHandle = 'Algorithmic Artisan';

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

PERFORMANCE drawSkyGradient() and drawGroundGradient()

Drawing one thin line per pixel (360-1080+ lines per frame) is expensive; this hammers CPU and GPU, especially on large monitors

💡 Draw lines every 2-4 pixels instead of every pixel: change `y++` to `y += 2`. The eye cannot distinguish individual lines anyway, and performance will improve dramatically. Example: `for (let y = 0; y < height * 0.9; y += 2)`

BUG branch() rotation and recursion

When the mouse is at the far edges (x=0 or x=width), the angle becomes extreme (PI/2 = 90°), causing branches to point horizontally instead of upward. Very unintuitive.

💡 Clamp the mouse position before mapping: `let constrainedMouseX = constrain(mouseX, width * 0.2, width * 0.8);` then map that instead. This keeps angles reasonable across the whole screen.

STYLE Global variable declarations

Many variables are declared but uninitialized (e.g., `let branchLength;`), making it unclear what their initial state is

💡 Initialize all globals explicitly: `let branchLength = 0;` This makes code intent clearer and prevents potential undefined-value bugs.

FEATURE keyPressed()

Users cannot change palettes or generate new trees without knowing to press P or N; there's no UI hint

💡 Display on-screen instructions like 'Press P for palette, N for new tree' at the top of the canvas, or use the artist handle area to show current controls

BUG drawLeaves() and ellipse random offsets

The random offset range `±leafSize/8` is recalculated fresh for every call within the loop, meaning leaves are scattered unpredictably. They should form cohesive clusters.

💡 Calculate leafSize once before the loop and use it consistently. Consider reducing the random offset range or making it scale with the original branchLength instead of the computed leafSize, to keep clusters visually tight.

🔄 Code Flow

Code flow showing setup, draw, branch, drawleaves, drawskygradient, drawgroundgradient, drawartisthandle, loadpalette, keypressed, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Setup] setup --> palette-load[Initial Palette Load] setup --> noise-initialization[Noise Offset Randomization] setup --> sky-draw[Sky Gradient Draw] setup --> ground-draw[Ground Gradient Draw] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click palette-load href "#sub-palette-load" click noise-initialization href "#sub-noise-initialization" click sky-draw href "#sub-sky-draw" click ground-draw href "#sub-ground-draw" draw --> noise-update[Continuous Noise Animation] draw --> tree-origin-setup[Tree Origin Translation] draw --> tree-branch-draw[Recursive Tree Generation] draw --> drawleaves[drawLeaves] draw --> drawskygradient[drawSkyGradient] draw --> drawgroundgradient[drawGroundGradient] click draw href "#fn-draw" click noise-update href "#sub-noise-update" click tree-origin-setup href "#sub-tree-origin-setup" click tree-branch-draw href "#fn-branch" click drawleaves href "#fn-drawleaves" click drawskygradient href "#fn-drawskygradient" click drawgroundgradient href "#fn-drawgroundgradient" tree-branch-draw --> angle-calculation[Dynamic Branch Angle] tree-branch-draw --> reduction-calculation[Dynamic Reduction Factor] tree-branch-draw --> endpoint-check[Recursion Termination Check] tree-branch-draw --> branch-draw-line[Branch Line Drawing] tree-branch-draw --> right-branch-recursion[Right Branch Recursive Call] tree-branch-draw --> left-branch-recursion[Left Branch Recursive Call] click angle-calculation href "#sub-angle-calculation" click reduction-calculation href "#sub-reduction-calculation" click endpoint-check href "#sub-endpoint-check" click branch-draw-line href "#sub-branch-draw-line" click right-branch-recursion href "#sub-right-branch-recursion" click left-branch-recursion href "#sub-left-branch-recursion" drawleaves --> leaf-loop[Leaf Cluster Loop] leaf-loop --> random-positioning[Random Leaf Offsets] click leaf-loop href "#sub-leaf-loop" click random-positioning href "#sub-random-positioning" drawskygradient --> gradient-loop[Pixel-by-Pixel Gradient Loop] gradient-loop --> color-noise-calc[Dynamic Color with Noise] gradient-loop --> color-blend[Vertical Color Interpolation] click gradient-loop href "#sub-gradient-loop" click color-noise-calc href "#sub-color-noise-calc" click color-blend href "#sub-color-blend" drawgroundgradient --> ground-gradient-loop[Ground Gradient Loop] ground-gradient-loop --> ground-color-interpolation[Ground Color Blend] ground-gradient-loop --> ground-noise-variation[Ground Texture Noise] click ground-gradient-loop href "#sub-ground-gradient-loop" click ground-color-interpolation href "#sub-ground-color-interpolation" click ground-noise-variation href "#sub-ground-noise-variation" draw --> keypressed[keyPressed] draw --> windowresized[windowResized] click keypressed href "#fn-keypressed" click windowresized href "#fn-windowresized" keypressed --> palette-cycle[Palette Cycling] keypressed --> random-generation[Random Tree Generation] click palette-cycle href "#sub-palette-cycle" click random-generation href "#sub-random-generation"

❓ Frequently Asked Questions

What visual elements can I expect to see in the Algorithmic Bloom sketch?

The sketch features an elegant fractal tree growing against a shifting sky and textured ground, with branches that sway and colors that cycle through seasonal palettes.

How can I interact with the Algorithmic Bloom sketch?

Users can explore new variations of the generative landscape by moving their mouse or restarting the sketch.

What creative coding concepts are showcased in the Algorithmic Bloom sketch?

The sketch demonstrates recursion in generating fractal trees, noise-driven motion for organic movement, and dynamic color palettes to create a visually rich experience.

Preview

Algorithmic Bloom - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Algorithmic Bloom - Code flow showing setup, draw, branch, drawleaves, drawskygradient, drawgroundgradient, drawartisthandle, loadpalette, keypressed, windowresized
Code Flow Diagram