Swaying Fractal Tree - xelsed.ai

This sketch grows an animated fractal tree from a bare seed into a full, leafy canopy over about five seconds, using recursive branching where each branch spawns two smaller child branches. Once grown, the whole tree sways gently as if blown by a breebreeze, all rendered against a soft blue-to-white sky gradient.

🧪 Try This!

Experiment with the code by making these changes:

  1. Grow the tree instantly — Increasing the growth increment makes the tree reach full size almost immediately instead of over five seconds.
  2. Make it a windy day — Boosting both the wind's speed and amplitude makes the tree whip back and forth dramatically instead of gently swaying.
  3. Give it autumn leaves — Changing the leaf fill color turns the whole canopy from summer green to warm autumn orange.
  4. Grow a wider, bushier tree — Widening the branch split angle spreads each fork further apart, producing a much bushier silhouette.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch draws a fractal tree that recursively splits into smaller and smaller branches, tapering from a thick brown trunk to thin twigs tipped with green leaves. What makes it visually compelling is the combination of a growth animation - the tree unfurls from nothing over about five seconds - with a continuous wind sway driven by a sine wave, so even a fully-grown tree never sits perfectly still. The core p5.js techniques at play are recursion, the push()/pop() transformation stack, translate()/rotate(), and color interpolation with lerpColor().

The code is organized around one recursive function, drawBranch(), which draws a single branch segment, then calls itself twice (once rotated left, once rotated right) to draw the next generation of smaller branches - this repeats up to maxDepth times. setup() prepares the canvas and starting colors, draw() advances the growth and wind animation each frame and kicks off the recursion from the base of the trunk, and two small helper functions handle drawing a leaf cluster and painting the sky gradient. Studying this sketch teaches you how a handful of variables (angle, length ratio, depth) can produce complex, organic-looking structure through recursion alone.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, sets the branch split angle to 25 degrees, calculates a starting trunk length based on the window size, and defines the trunk and twig colors used for blending.
  2. Every frame, draw() first paints a vertical sky gradient behind everything, then smoothly eases the trunk length toward a target value in case the window was resized.
  3. draw() also nudges treeGrowth upward by a tiny amount each frame (reaching 1.0 after roughly five seconds) and computes a wind angle using sin(millis() * 0.001 * 0.8), which oscillates gently back and forth forever.
  4. draw() then translates to the bottom-center of the canvas and calls drawBranch() to recursively draw the trunk and every branch generation beneath the current growth level.
  5. Inside drawBranch(), each call checks how much of that branch's segment should be visible based on treeGrowth, applies wind sway (stronger toward the tips, weaker at the trunk), draws a tapered, color-blended line segment, and - once that segment is fully grown - spawns two smaller child branches by calling drawBranch() again at a rotated angle.
  6. Once a branch reaches the maximum recursion depth and is fully grown, drawLeaf() draws a small cluster of green ellipses at its tip instead of spawning further children.

🎓 Concepts You'll Learn

Recursionpush()/pop() transformation stacktranslate() and rotate()Color interpolation with lerpColor()Sine-wave-driven animationLinear interpolation (lerp) for smooth transitions

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the right place to size the canvas and calculate any values (like baseLength) that depend on the window's dimensions, plus set up drawing defaults like strokeCap().

function setup() {
  createCanvas(windowWidth, windowHeight);

  branchAngle = radians(25); // base split angle for branches

  // initial trunk length based on canvas
  baseLength = min(width, height) * 0.25;

  // colors for branches
  trunkColor = color(80, 42, 15);  // dark brown
  twigColor  = color(150, 95, 45); // lighter brown

  strokeCap(ROUND);
}
Line-by-line explanation (6 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window.
branchAngle = radians(25); // base split angle for branches
Converts 25 degrees to radians (the unit p5.js's rotate() expects) and stores it for use in every branch split.
baseLength = min(width, height) * 0.25;
Sets the trunk's starting length to a quarter of whichever canvas dimension (width or height) is smaller, so the tree fits nicely on any screen shape.
trunkColor = color(80, 42, 15); // dark brown
Defines the RGB color used at the base of the tree (depth 0).
twigColor = color(150, 95, 45); // lighter brown
Defines the RGB color used at the outermost twigs (deepest recursion level) - the tree blends between these two colors by depth.
strokeCap(ROUND);
Rounds the ends of every line drawn afterward, so branch segments connect smoothly instead of showing sharp square joints.

draw()

draw() runs continuously, roughly 60 times per second. Here it plays three roles at once: repainting the background, advancing two independent animations (growth and wind) using time-based values, and triggering the recursive tree drawing.

🔬 This line controls how fast the tree grows. What happens if you change 0.0035 to 0.0005 (much slower) or 0.05 (almost instant)?

  // Grow the tree over time (approx 5 seconds to fully grow)
  treeGrowth = min(1, treeGrowth + 0.0035);
function draw() {
  drawSkyGradient();

  // Smoothly update baseLength if window size changes
  const targetBaseLength = min(width, height) * 0.25;
  baseLength = lerp(baseLength, targetBaseLength, 0.1);

  // Grow the tree over time (approx 5 seconds to fully grow)
  treeGrowth = min(1, treeGrowth + 0.0035);

  // Gentle wind using a sine wave
  const t = millis() * 0.001; // time in seconds
  const wind = sin(t * 0.8) * radians(6); // ±6° sway

  // Draw the tree from bottom center, going upward
  push();
  translate(width / 2, height);
  drawBranch(baseLength, 0, wind);
  pop();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Growth Progress Update treeGrowth = min(1, treeGrowth + 0.0035);

Increments the global growth value each frame but caps it at 1.0 so the tree stops growing once fully mature.

calculation Wind Sway Calculation const wind = sin(t * 0.8) * radians(6); // ±6° sway

Uses a sine wave over time to create a smooth, repeating back-and-forth sway angle.

drawSkyGradient();
Paints the background sky gradient fresh every frame before anything else is drawn.
const targetBaseLength = min(width, height) * 0.25;
Recomputes what the trunk length SHOULD be based on the current canvas size, in case the browser window was resized.
baseLength = lerp(baseLength, targetBaseLength, 0.1);
Moves baseLength 10% of the way toward its target each frame, producing a smooth resize instead of an instant jump.
treeGrowth = min(1, treeGrowth + 0.0035);
Advances the tree's overall growth progress by a small amount each frame; min(1, ...) prevents it from exceeding fully grown.
const t = millis() * 0.001; // time in seconds
Converts milliseconds since the sketch started into seconds, giving a smoothly increasing time value to drive the sine wave.
const wind = sin(t * 0.8) * radians(6); // ±6° sway
sin() oscillates between -1 and 1 forever; multiplying by radians(6) scales that into a gentle ±6-degree sway angle that changes continuously.
translate(width / 2, height);
Moves the drawing origin to the horizontal center and bottom of the canvas, so the tree's trunk starts growing upward from there.
drawBranch(baseLength, 0, wind);
Kicks off the recursive tree drawing, starting at depth 0 (the trunk) with the current wind angle.

drawBranch()

This is the heart of the sketch: a recursive function that calls itself with a shorter length and one deeper depth, twice per call. push() and pop() save and restore the coordinate system around each call so that rotations and translations applied to one branch don't leak into its sibling.

🔬 Each branch spawns exactly 2 children by rotating +branchAngle and -branchAngle. What happens visually if you add a THIRD push()/rotate(0)/drawBranch()/pop() block here to make each branch split into 3?

    // Right branch
    push();
    rotate(branchAngle);
    drawBranch(childLen, depth + 1, wind);
    pop();

    // Left branch
    push();
    rotate(-branchAngle);
    drawBranch(childLen, depth + 1, wind);
    pop();
function drawBranch(len, depth, wind) {
  if (depth > maxDepth) return;

  // Map global growth to this branch's local progress
  const g = treeGrowth * (maxDepth + 1); // 0..maxDepth+1
  const local = g - depth;

  // Not grown to this level yet
  if (local <= 0) return;

  // 0..1, how much of this segment is drawn
  const progress = local < 1 ? local : 1;

  // Thickness tapers with depth
  const thickness = map(depth, 0, maxDepth, 12, 1.5);

  // Color blends from trunkColor to twigColor with depth
  const c = lerpColor(trunkColor, twigColor, depth / maxDepth);

  // Sway stronger at the tips, weaker in the trunk
  const swayStrength = map(depth, 0, maxDepth, 0.2, 1.0);
  const localSway = wind * swayStrength;

  push();

  // Apply wind sway
  rotate(localSway);

  stroke(c);
  strokeWeight(thickness);
  noFill();

  // Draw this branch segment upwards (negative Y)
  const segLen = len * progress;
  line(0, 0, 0, -segLen);

  // Move to the end of this segment
  translate(0, -segLen);

  // If this is a fully grown tip, draw a leaf
  if (depth === maxDepth && progress >= 1.0) {
    drawLeaf();
  }

  // If this segment is fully grown, spawn child branches
  if (depth < maxDepth && progress >= 1.0) {
    const childLen = len * 0.72;

    // Right branch
    push();
    rotate(branchAngle);
    drawBranch(childLen, depth + 1, wind);
    pop();

    // Left branch
    push();
    rotate(-branchAngle);
    drawBranch(childLen, depth + 1, wind);
    pop();
  }

  pop();
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Recursion Depth Guard if (depth > maxDepth) return;

Stops the recursion once the deepest branch level has been reached, preventing infinite recursion.

conditional Not-Grown-Yet Guard if (local <= 0) return;

Skips drawing this branch entirely if the overall tree hasn't grown far enough to reach this depth yet.

conditional Leaf Draw Check if (depth === maxDepth && progress >= 1.0) {

Draws a leaf only at the very tip of a fully-grown final-depth branch.

conditional Child Branch Spawn if (depth < maxDepth && progress >= 1.0) {

Once a segment is completely drawn, this block recursively creates the next two smaller branches at rotated angles.

if (depth > maxDepth) return;
Base case of the recursion - if we've gone deeper than allowed, stop immediately.
const g = treeGrowth * (maxDepth + 1); // 0..maxDepth+1
Scales the overall 0-1 growth value up so it can be compared against branch depth numbers (0 through maxDepth).
const local = g - depth;
Figures out how far the global growth has progressed relative to THIS branch's depth - positive means it should start appearing.
if (local <= 0) return;
If growth hasn't reached this depth yet, don't draw anything for this branch (or its children).
const progress = local < 1 ? local : 1;
Clamps local between 0 and 1 - this fraction determines how much of THIS branch's length is currently visible, creating the growing effect.
const thickness = map(depth, 0, maxDepth, 12, 1.5);
Uses map() to linearly convert depth into a stroke thickness, so branches taper from thick (12px) at the trunk to thin (1.5px) at the tips.
const c = lerpColor(trunkColor, twigColor, depth / maxDepth);
Blends between the two defined colors based on how deep this branch is, giving a gradual color transition from dark bark to lighter twigs.
const swayStrength = map(depth, 0, maxDepth, 0.2, 1.0);
Deeper (further out) branches sway more than the trunk, mimicking how real tree tips whip around more than the thick base.
rotate(localSway);
Applies this branch's wind-driven rotation on top of whatever rotation its parent branches already applied, since transforms stack.
const segLen = len * progress;
Multiplies the branch's full length by its growth progress, so an unfinished branch appears shorter than its final length.
line(0, 0, 0, -segLen);
Draws the branch as a vertical line going upward (negative Y) from the current origin, which was just moved here by translate().
translate(0, -segLen);
Moves the drawing origin to the tip of the just-drawn segment, so anything drawn next (leaves or child branches) starts from there.
const childLen = len * 0.72;
Each child branch is 72% the length of its parent, which is what makes the tree naturally taper as it recurses.
rotate(branchAngle);
Rotates the coordinate system clockwise by the branch angle before drawing the right child branch.
drawBranch(childLen, depth + 1, wind);
Recursively calls itself to draw the next generation branch, one depth level deeper.

drawLeaf()

drawLeaf() is called only when a branch reaches maxDepth AND is fully grown - it's a simple decorative function that runs at the tip of every terminal branch, showing how small repeated details (leaves) can make a procedural structure feel organic.

function drawLeaf() {
  noStroke();
  fill(34, 139, 34, 220); // forest green with a bit of alpha
  const leafW = 10;
  const leafH = 14;
  // Draw two small overlapping ellipses for a leaf cluster
  ellipse(0, 0, leafW, leafH);
  ellipse(3, -2, leafW * 0.8, leafH * 0.8);
}
Line-by-line explanation (4 lines)
noStroke();
Turns off outlines so the leaf ellipses appear as solid filled shapes.
fill(34, 139, 34, 220); // forest green with a bit of alpha
Sets the leaf color to forest green with slight transparency (alpha 220 out of 255), so overlapping leaves blend softly.
ellipse(0, 0, leafW, leafH);
Draws the first leaf ellipse centered at the branch tip (which is the current origin, since translate() already moved here).
ellipse(3, -2, leafW * 0.8, leafH * 0.8);
Draws a second, slightly smaller ellipse offset up and to the right, creating a clustered, layered leaf look instead of one plain oval.

drawSkyGradient()

This function creates a smooth vertical gradient by drawing many thin horizontal lines, each a slightly different interpolated color computed with lerpColor(). It's a common technique for backgrounds in p5.js since there's no built-in gradient fill.

🔬 This loop draws one line per pixel row for a smooth gradient. What happens if you change the loop increment to y += 4, drawing a line every 4 rows instead of every 1?

  for (let y = 0; y < height; y++) {
    const t = y / max(height - 1, 1);
    const c = lerpColor(topColor, bottomColor, t);
    stroke(c);
    line(0, y, width, y);
  }
function drawSkyGradient() {
  const topColor = color(180, 220, 255);   // light sky blue
  const bottomColor = color(255, 255, 255); // white

  noFill();
  for (let y = 0; y < height; y++) {
    const t = y / max(height - 1, 1);
    const c = lerpColor(topColor, bottomColor, t);
    stroke(c);
    line(0, y, width, y);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

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

Iterates over every horizontal row of pixels in the canvas to draw one full-width line per row, each a slightly different blended color.

const topColor = color(180, 220, 255); // light sky blue
Defines the color used at the very top row of the canvas.
const bottomColor = color(255, 255, 255); // white
Defines the color used at the very bottom row of the canvas.
for (let y = 0; y < height; y++) {
Loops through every single pixel row from top (y=0) to bottom (y=height-1).
const t = y / max(height - 1, 1);
Converts the current row number into a 0-1 fraction representing how far down the canvas we are; max(height-1, 1) avoids dividing by zero on a 1-pixel-tall canvas.
const c = lerpColor(topColor, bottomColor, t);
Blends between the sky blue and white based on that fraction, so colors near the top are bluer and colors near the bottom are whiter.
line(0, y, width, y);
Draws a single horizontal line across the full width of the canvas at this row, using the blended color set by stroke() just above.

windowResized()

windowResized() is a special p5.js callback that automatically runs whenever the browser window is resized. Pairing it with the smooth baseLength lerp in draw() keeps the tree adjusting gracefully instead of jumping abruptly to a new size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Calls p5's built-in canvas resizing whenever the browser window changes size, keeping the sketch full-window.

📦 Key Variables

maxDepth number

Controls how many levels of recursive branching occur - the biggest factor in how detailed and bushy the tree looks.

let maxDepth = 9;
baseLength number

Stores the current trunk length, recalculated from canvas size and smoothly eased with lerp() when the window resizes.

let baseLength;
branchAngle number

The angle (in radians) each branch rotates away from its parent when splitting into two children.

let branchAngle;
treeGrowth number

Tracks overall growth progress from 0 (nothing grown) to 1 (fully grown), incremented a tiny bit every frame in draw().

let treeGrowth = 0;
trunkColor object

A p5.Color used as the starting color (at depth 0) for the branch color gradient.

let trunkColor = color(80, 42, 15);
twigColor object

A p5.Color used as the ending color (at the deepest branch level) for the branch color gradient.

let twigColor = color(150, 95, 45);

🔧 Potential Improvements (3)

Here are some ways this code could be enhanced:

PERFORMANCE drawSkyGradient()

The gradient is redrawn by looping over every single pixel row (potentially 1000+ iterations calling line() and lerpColor()) on every single frame, even though the sky never changes after the window stops resizing.

💡 Render the gradient once into an offscreen buffer with createGraphics() (or only recompute it inside windowResized()) and simply image() that buffer each frame instead of recalculating it 60 times per second.

STYLE drawBranch()

Several 'magic numbers' (0.0035 growth speed, 0.72 length ratio, radians(6) wind amplitude, 12/1.5 thickness range) are hardcoded inline, making them hard to find and tune together.

💡 Lift these into named global constants near the top of the file (e.g. let growthSpeed = 0.0035; let lengthRatio = 0.72;) so they're easy to locate and experiment with without hunting through the recursive function.

FEATURE draw()

The wind is entirely automatic and the viewer has no way to influence the tree's behavior, which limits interactivity.

💡 Map mouseX or mouseY to the wind's speed or amplitude (e.g. const windAmp = map(mouseX, 0, width, 2, 20)) so moving the mouse changes how strongly the tree sways, turning a passive animation into an interactive one.

🔄 Code Flow

Code flow showing setup, draw, drawbranch, drawleaf, drawskygradient, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> growthupdate[Growth Progress Update] draw --> windcalculation[Wind Sway Calculation] draw --> drawbranch[drawbranch] click setup href "#fn-setup" click draw href "#fn-draw" click growthupdate href "#sub-growth-update" click windcalculation href "#sub-wind-calculation" drawbranch --> depthguard[Recursion Depth Guard] drawbranch --> growthguard[Not-Grown-Yet Guard] drawbranch --> childspawn[Child Branch Spawn] click drawbranch href "#fn-drawbranch" click depthguard href "#sub-depth-guard" click growthguard href "#sub-growth-guard" click childspawn href "#sub-child-spawn" childspawn --> drawbranch drawbranch --> leafcheck[Leaf Draw Check] click leafcheck href "#sub-leaf-check" leafcheck --> drawleaf[drawleaf] click drawleaf href "#fn-drawleaf" draw --> drawskygradient[drawskygradient] drawskygradient --> gradientloop[Row-by-Row Gradient Loop] click drawskygradient href "#fn-drawskygradient" click gradientloop href "#sub-gradient-loop" gradientloop --> drawskygradient

❓ Frequently Asked Questions

What visual experience does the Swaying Fractal Tree sketch provide?

The sketch creates a stunning fractal tree that grows from a seed, featuring recursive branching, brown bark, and lush green leaves swaying gently against a serene sky gradient.

Is there any user interaction available in the Swaying Fractal Tree sketch?

The sketch automatically animates the tree's growth and sway in the wind, but it does not require direct user interaction.

What creative coding techniques are showcased in the Swaying Fractal Tree sketch?

This sketch demonstrates recursive drawing for natural structures, dynamic color blending, and smooth animations using mathematical functions like sine waves.

Preview

Swaying Fractal Tree - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Swaying Fractal Tree - xelsed.ai - Code flow showing setup, draw, drawbranch, drawleaf, drawskygradient, windowresized
Code Flow Diagram