AI Fractal Tree - Growing Branches in the Wind

This sketch procedurally grows a fractal tree from a single trunk into a full branching structure over ten seconds, using recursion to place each generation of branches. Wind is faked with a sine wave so every branch gently sways, and once the tree reaches full depth, green leaf dots appear at every branch tip.

🧪 Try This!

Experiment with the code by making these changes:

  1. Grow the tree faster — Lowering growT means less time is needed for the growth progress t to reach 1, so the tree reaches full depth and sprouts leaves much sooner.
  2. Make a bushier tree — Raising maxD lets the recursion go deeper, adding more generations of smaller branches for a fuller, more detailed canopy.
  3. Give it pink blossoms instead of leaves — The fill color used for the tip ellipses is what makes them read as green leaves - swapping the color turns them into flowers instantly.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch draws a tree that grows in real time: a single trunk at the start slowly unfolds into a full fractal canopy over about ten seconds, with every branch gently swaying as if blown by wind. It's a great sketch to study because it demonstrates recursion (a function that calls itself), the push()/pop() matrix stack for building branching transformations, and using sin() with frameCount to fake believable motion - three techniques that show up constantly in generative art.

The code is tiny - just four functions - but packs in a lot: setup() prepares the canvas and colors, draw() paints a sky-to-grass gradient and kicks off the tree by calling the recursive branch() function, and branch() draws one segment then calls itself twice to grow two child branches at an angle. By tracing through branch() line by line you'll understand how translate() and rotate() combine to place each new branch relative to its parent, and how a simple millis()-based timer can drive a growth animation without any extra state variables.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas that fills the browser window and defines two colors - sky blue and grass green - that will be used to paint the background.
  2. Every frame, draw() paints a vertical gradient from sky to grass by looping over every pixel row and drawing a horizontal line in a blended color, then adds a soft dark ellipse as a ground shadow.
  3. draw() also computes a growth progress value from millis() (time since the page loaded) divided by growT, mapping that progress to a target branch depth - so the tree literally unlocks one more generation of branches as time passes, until it reaches maxD.
  4. The recursive branch() function draws one line segment for the current branch, then - if it hasn't reached the target depth yet - calls itself twice, once rotated left and once rotated right, each time with a shorter length, which is how the whole tree structure emerges from a single function.
  5. A wind offset calculated as sin(frameCount * 0.02 + depth * 0.1) is added to each branch's rotation angle every frame, so branches sway independently based on their depth, creating a natural, staggered swaying motion instead of everything moving in lockstep.
  6. Once the growth timer reaches 1 (the boolean full becomes true), every branch at the deepest level draws a small green ellipse at its tip instead of continuing to branch, representing a leaf.

🎓 Concepts You'll Learn

Recursionpush()/pop() matrix stacktranslate() and rotate() transformationssin() driven animationmillis()-based timingColor interpolation with lerpColor()

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. Here it's used to size the canvas to the window and precompute the two colors used for the gradient, so draw() doesn't need to recreate them every frame.

function setup(){
  createCanvas(windowWidth,windowHeight);
  sky=color(180,220,255);grass=color(90,180,90);
}
Line-by-line explanation (3 lines)
createCanvas(windowWidth,windowHeight)
Creates a canvas that fills the entire browser window, using p5's built-in windowWidth and windowHeight values.
sky=color(180,220,255)
Stores a light blue color in the global variable sky, to be reused every frame for the top of the background gradient.
grass=color(90,180,90)
Stores a green color in the global variable grass, used for the bottom of the background gradient (and visually implying ground).

draw()

draw() runs continuously and is responsible for both the static-looking background (redrawn every frame) and kicking off the dynamic, recursive tree. Notice how it computes just a single number (d, the target depth) each frame and hands off all the actual branching work to branch().

🔬 This line turns elapsed time into a target branch depth. What happens visually if you replace floor(t*(maxD-1)) with round(t*(maxD-1)) - does the tree seem to jump to new depths sooner or later?

let t=min(millis()/growT,1),d=1+floor(t*(maxD-1));full=t>=1;
function draw(){
  for(let y=0;y<height;y++){stroke(lerpColor(sky,grass,y/height));line(0,y,width,y);}
  noStroke();fill(0,0,0,40);ellipse(width/2+60,height-25,140,35);
  let t=min(millis()/growT,1),d=1+floor(t*(maxD-1));full=t>=1;
  push();translate(width/2,height);
  stroke(120,70,20);branch(height*0.25,1,d);
  pop();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Sky-to-grass gradient for(let y=0;y<height;y++){stroke(lerpColor(sky,grass,y/height));line(0,y,width,y);}

Draws one horizontal line per pixel row, blending from sky color at the top to grass color at the bottom using lerpColor().

calculation Growth progress and depth let t=min(millis()/growT,1),d=1+floor(t*(maxD-1));full=t>=1;

Turns elapsed time into a 0-1 growth progress value t, then maps that to an integer target depth d, and flags full once growth is complete.

for(let y=0;y<height;y++){stroke(lerpColor(sky,grass,y/height));line(0,y,width,y);}
Loops through every vertical pixel row and draws a full-width line, blending sky and grass colors based on how far down the screen y is - this paints the whole background gradient before anything else is drawn.
noStroke();fill(0,0,0,40);ellipse(width/2+60,height-25,140,35);
Turns off outlines, sets a very transparent black fill, then draws a wide flat ellipse near the bottom to act as a soft ground shadow beneath the tree.
let t=min(millis()/growT,1),d=1+floor(t*(maxD-1));full=t>=1;
millis() returns milliseconds since the sketch started; dividing by growT and clamping with min(...,1) gives a 0-to-1 growth progress t. That progress is scaled up to an integer depth d between 1 and maxD, and full becomes true once t hits 1 (fully grown).
push();translate(width/2,height);
Saves the current drawing state, then shifts the origin to the horizontal center and bottom of the canvas, so the tree's trunk can be drawn starting from (0,0) instead of needing to calculate screen coordinates every time.
stroke(120,70,20);branch(height*0.25,1,d);
Sets a brown stroke color for the wood, then kicks off the recursive tree by calling branch() with an initial trunk length (a quarter of the canvas height), starting depth 1, and the current target depth d.
pop();
Restores the drawing state saved by push(), so any transformations made while drawing the tree don't leak into the next frame's drawing.

branch()

branch() is a classic example of recursion in graphics: instead of writing separate code for every branch, one function draws a segment, moves and rotates the coordinate system, then calls itself to draw the next smaller segment. push()/pop() are essential here because each branch needs its own local rotation and position without affecting its siblings.

🔬 These two lines create the fork by drawing two child branches. What happens if you change len*0.7 to len*0.9 (branches barely shrink) or len*0.5 (branches shrink fast)?

  rotate(a+w);branch(len*0.7,depth+1,maxDepth);
  rotate(-2*a);branch(len*0.7,depth+1,maxDepth);

🔬 This draws a small circular leaf once the tree is fully grown. What happens if you make the ellipse much bigger, or swap fill(40,180,60) for a bright pink so leaves stand out more?

  if(depth==maxDepth&&full){
    fill(40,180,60);noStroke();ellipse(0,-len,10,10);stroke(120,70,20);
  }
function branch(len,depth,maxDepth){
  strokeWeight(maxDepth-depth+2);
  line(0,0,0,-len);
  if(depth==maxDepth&&full){
    fill(40,180,60);noStroke();ellipse(0,-len,10,10);stroke(120,70,20);
  }
  if(depth>=maxDepth)return;
  let a=PI/5,w=sin(frameCount*0.02+depth*0.1)*0.3;
  push();translate(0,-len);
  rotate(a+w);branch(len*0.7,depth+1,maxDepth);
  rotate(-2*a);branch(len*0.7,depth+1,maxDepth);
  pop();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Draw leaf at tip if(depth==maxDepth&&full){ fill(40,180,60);noStroke();ellipse(0,-len,10,10);stroke(120,70,20); }

Once the tree is fully grown and this branch is at the deepest reached level, draws a small green ellipse (a leaf) at the branch tip.

conditional Recursion base case if(depth>=maxDepth)return;

Stops the recursion once the current branch has reached the target depth, so the tree doesn't grow forever.

calculation Two child branches rotate(a+w);branch(len*0.7,depth+1,maxDepth); rotate(-2*a);branch(len*0.7,depth+1,maxDepth);

Rotates by the branch angle plus wind, recurses into a shorter child branch, then rotates the opposite way for the second child - this is what creates the forking, tree-like shape.

strokeWeight(maxDepth-depth+2)
Makes branches closer to the trunk (lower depth) thicker, and branches near the tips (closer to maxDepth) thinner, mimicking how real branches taper.
line(0,0,0,-len)
Draws the branch itself as a straight vertical line going upward (negative y) from the current origin, whose position and rotation were set up by the parent call.
if(depth==maxDepth&&full){...}
Checks if this is a tip branch at the currently active maximum depth AND the tree has finished growing; if so, switches to green fill, draws a small circle as a leaf, then switches stroke back to brown wood color for any sibling branches.
if(depth>=maxDepth)return;
This is the recursion's base case - without it, branch() would call itself forever. Once depth reaches the target maxDepth, the function simply returns and stops branching further.
let a=PI/5,w=sin(frameCount*0.02+depth*0.1)*0.3;
Sets a fixed splay angle a (36 degrees) for how far child branches diverge, and computes a wind offset w using a sine wave driven by frameCount and depth so each branch sways slightly out of sync with the others.
push();translate(0,-len);
Saves the current transformation, then moves the origin to the tip of the branch just drawn - all child branches will be drawn relative to this new origin.
rotate(a+w);branch(len*0.7,depth+1,maxDepth);
Rotates the coordinate system by the splay angle plus wind sway, then recursively draws the first child branch, 70% as long as this one, one depth level deeper.
rotate(-2*a);branch(len*0.7,depth+1,maxDepth);
Rotates back by twice the angle (undoing the first rotation and mirroring it to the other side), then recursively draws the second child branch in the opposite direction.
pop();
Restores the transformation saved earlier, so the rotation and translation used for this branch's children don't affect any other branches drawn afterward.

windowResized()

This is a standard p5.js responsive-canvas pattern: pairing createCanvas(windowWidth, windowHeight) in setup() with resizeCanvas(windowWidth, windowHeight) inside windowResized() keeps full-screen sketches responsive to window resizing.

function windowResized(){resizeCanvas(windowWidth,windowHeight);}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth,windowHeight)
p5.js automatically calls windowResized() whenever the browser window changes size; this line resizes the canvas to match the new window dimensions so the tree scene always fills the screen.

📦 Key Variables

maxD number

The maximum recursion depth the tree can reach - controls how many generations of branches the fully-grown tree has.

let maxD=8;
growT number

The number of milliseconds the growth animation takes to go from a bare trunk to the fully grown tree.

let growT=10000;
sky object

Stores a p5.Color for the top of the background gradient (light blue sky), created once in setup() to avoid recreating it every frame.

sky=color(180,220,255);
grass object

Stores a p5.Color for the bottom of the background gradient (green grass), created once in setup().

grass=color(90,180,90);
full boolean

Becomes true once the growth timer reaches 100%, telling branch() it's now safe to draw leaves at the tips.

let full=false;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw() gradient loop

The background gradient is redrawn every single frame by looping over every pixel row and calling stroke() plus line() (potentially 1000+ draw calls per frame on a tall screen), even though the gradient never changes.

💡 Draw the gradient once into an offscreen createGraphics() buffer (or use a CSS/canvas background) and simply image() it each frame, saving a large amount of per-frame work.

STYLE entire sketch.js

The code is heavily minified (multiple statements per line, no spacing around operators), which makes it much harder to read, debug, and modify than idiomatic p5.js code.

💡 Reformat with one statement per line and consistent spacing (e.g. 'let a = PI / 5;' instead of 'let a=PI/5'), which costs nothing at runtime but greatly improves readability for future editing.

FEATURE branch()

Both child branches always split at exactly the same fixed angle (PI/5) and length ratio (0.7), so every branch of the tree is a perfect mirror image, which looks slightly artificial.

💡 Add small random variation to the angle and length ratio per branch (e.g. using random(-0.05,0.05) offsets seeded once per branch) so the tree looks less symmetric and more organic, like a real tree.

BUG branch() leaf condition

The leaf-drawing condition checks depth==maxDepth, but maxDepth here is actually the *currently active* target depth (parameter d from draw()), not the sketch's true maxD - so as the tree grows deeper, leaves only ever appear at the current tips and old leaf positions from earlier growth stages are simply redrawn over by new branches, which is mostly fine but means leaves can flicker off mid-growth if depth briefly changes.

💡 Consider explicitly checking depth==maxD instead of maxDepth, or caching the tree so growth stages don't have to fully redraw the same branches with the same random wind offsets every frame.

🔄 Code Flow

Code flow showing setup, draw, branch, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> gradientloop[gradient-loop] gradientloop --> growthcalc[growth-calc] growthcalc --> basecase[base-case] basecase --> recursivecalls[recursive-calls] recursivecalls --> draw click setup href "#fn-setup" click draw href "#fn-draw" click gradientloop href "#sub-gradient-loop" click growthcalc href "#sub-growth-calc" click basecase href "#sub-base-case" click recursivecalls href "#sub-recursive-calls"

❓ Frequently Asked Questions

What visual experience does the AI Fractal Tree sketch provide?

The sketch visually simulates a tree growing from a trunk into a detailed fractal structure, with branches swaying gently in the wind and green leaves appearing at the tips when fully grown.

Is there any interaction available in the AI Fractal Tree sketch?

The sketch is primarily a visual experience with no direct user interaction; however, it dynamically responds to window resizing.

What creative coding concepts are showcased in the AI Fractal Tree sketch?

This sketch demonstrates recursive branching techniques and the application of wind physics to create a naturalistic animation of a growing tree.

Preview

AI Fractal Tree - Growing Branches in the Wind - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Fractal Tree - Growing Branches in the Wind - Code flow showing setup, draw, branch, windowresized
Code Flow Diagram