Swaying Fractal Tree - xelsed.ai

This sketch creates a beautiful animated fractal tree that grows from a seed over time, with branches that sway gently in the wind. The tree uses recursive branching to create a natural-looking structure with brown bark that transitions from dark to light, lush green leaves at the tips, and a serene sky gradient background.

๐ŸŽ“ Concepts You'll Learn

RecursionFractal geometryAnimation and growthWind simulation with sine wavesColor interpolationTransformations (translate, rotate)Responsive canvasStroke and line drawing

๐Ÿ”„ 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 --> skygradient[sky-gradient] draw --> responsive[responsive-sizing] draw --> treegrowth[tree-growth] draw --> windsim[wind-simulation] draw --> treeposition[tree-positioning] treeposition --> depthcheck[depth-check] depthcheck -->|if depth < max| growthmapping[growth-mapping] growthmapping --> growthcheck[growth-check] growthcheck -->|if grown| progressclamping[progress-clamping] progressclamping --> thickness[thickness-calculation] thickness --> colorinterp[color-interpolation] colorinterp --> swaystrength[sway-strength] swaystrength --> branchdrawing[branch-drawing] branchdrawing --> leafdrawing[leaf-drawing] leafdrawing -->|if leaf| childspawning[child-spawning] childspawning --> drawbranch[drawbranch] drawbranch -->|recursive| drawbranch drawbranch --> leafstyling[leaf-styling] leafstyling --> leafdimensions[leaf-dimensions] leafdimensions --> leafdrawing[leaf-drawing] draw --> gradientloop[gradient-loop] gradientloop --> colorinterp[color-interpolation] click setup href "#fn-setup" click draw href "#fn-draw" click skygradient href "#sub-sky-gradient" click responsive href "#sub-responsive-sizing" click treegrowth href "#sub-tree-growth" click windsim href "#sub-wind-simulation" click treeposition href "#sub-tree-positioning" click depthcheck href "#sub-depth-check" click growthmapping href "#sub-growth-mapping" click growthcheck href "#sub-growth-check" click progressclamping href "#sub-progress-clamping" click thickness href "#sub-thickness-calculation" click colorinterp href "#sub-color-interpolation" click swaystrength href "#sub-sway-strength" click branchdrawing href "#sub-branch-drawing" click leafdrawing href "#sub-leaf-drawing" click childspawning href "#sub-child-spawning" click leafstyling href "#sub-leaf-styling" click leafdimensions href "#sub-leaf-dimensions" click gradientloop href "#sub-gradient-loop"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas size, calculates the initial tree trunk length based on window dimensions, and defines the colors and angles that will be used throughout the animation.

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:

createCanvas(windowWidth, windowHeight)
Creates a full-window canvas that responds to the browser window size
branchAngle = radians(25)
Converts 25 degrees to radians and stores it - this is the angle at which branches split from their parent
baseLength = min(width, height) * 0.25
Sets the trunk length to 25% of the smaller dimension, so the tree scales to fit any window size
trunkColor = color(80, 42, 15)
Defines a dark brown color for the thick trunk branches
twigColor = color(150, 95, 45)
Defines a lighter brown color for the thin twig branches
strokeCap(ROUND)
Makes line endpoints rounded instead of square, creating a smoother, more natural look

draw()

draw() runs 60 times per second, creating the animation. Each frame, it redraws the background, updates the growth progress, calculates wind sway, and draws the entire tree. The tree grows gradually because drawBranch() uses treeGrowth to decide how much of each branch to draw.

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();
}

๐Ÿ”ง Subcomponents:

function-call Sky Gradient Background drawSkyGradient()

Draws the animated background gradient from light blue to white

calculation Responsive Base Length baseLength = lerp(baseLength, targetBaseLength, 0.1)

Smoothly adjusts tree size when window is resized using linear interpolation

calculation Tree Growth Animation treeGrowth = min(1, treeGrowth + 0.0035)

Gradually increases growth value from 0 to 1 over approximately 5 seconds

calculation Wind Sway Calculation const wind = sin(t * 0.8) * radians(6)

Creates smooth oscillating wind effect using sine wave, ranging from -6 to +6 degrees

transformation Tree Position and Draw translate(width / 2, height); drawBranch(baseLength, 0, wind)

Positions the tree root at the bottom center of the canvas and initiates recursive branch drawing

Line by Line:

drawSkyGradient()
Redraws the sky gradient background each frame to create a clean slate
const targetBaseLength = min(width, height) * 0.25
Calculates what the trunk length should be based on current window size
baseLength = lerp(baseLength, targetBaseLength, 0.1)
Smoothly transitions the current trunk length toward the target (10% of the way each frame) so resizing looks fluid
treeGrowth = min(1, treeGrowth + 0.0035)
Increases growth by 0.0035 each frame (60fps = ~5.8 seconds to reach 1.0), but caps it at 1.0 so it doesn't overgrow
const t = millis() * 0.001
Converts milliseconds to seconds for use in the sine wave calculation
const wind = sin(t * 0.8) * radians(6)
Creates a smooth oscillating wind value between -6ยฐ and +6ยฐ that changes over time
push()
Saves the current transformation state so changes don't affect other drawings
translate(width / 2, height)
Moves the drawing origin to the bottom center of the canvas, where the tree root will be
drawBranch(baseLength, 0, wind)
Starts the recursive tree drawing with the full trunk length, at depth 0 (the root), with current wind value
pop()
Restores the previous transformation state

drawBranch(len, depth, wind)

This is the core of the sketch - a recursive function that draws one branch and calls itself twice to create child branches. The recursion creates the fractal tree structure. Key concepts: (1) Recursion depth prevents infinite loops, (2) Growth progress is mapped to depth so branches appear level-by-level, (3) Thickness and color change with depth for visual realism, (4) Wind sway is stronger at tips for natural movement, (5) Child branches are 72% the length of their parent, creating the fractal pattern.

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();
}

๐Ÿ”ง Subcomponents:

conditional Recursion Depth Limit if (depth > maxDepth) return

Stops recursion when reaching maximum depth to prevent infinite loops

calculation Growth to Depth Mapping const g = treeGrowth * (maxDepth + 1); const local = g - depth

Converts global growth value to per-depth progress, so branches grow level by level

conditional Branch Not Yet Grown if (local <= 0) return

Exits early if this branch hasn't started growing yet

calculation Progress Clamping const progress = local < 1 ? local : 1

Ensures progress stays between 0 and 1 (0% to 100% drawn)

calculation Thickness Tapering const thickness = map(depth, 0, maxDepth, 12, 1.5)

Makes branches thinner as they get deeper (trunk is thick, twigs are thin)

calculation Color Blending const c = lerpColor(trunkColor, twigColor, depth / maxDepth)

Blends from dark brown (trunk) to light brown (twigs) based on depth

calculation Depth-Based Sway const swayStrength = map(depth, 0, maxDepth, 0.2, 1.0); const localSway = wind * swayStrength

Makes tips sway more than the trunk (tips are more flexible)

calculation Branch Segment Drawing const segLen = len * progress; line(0, 0, 0, -segLen)

conditional Leaf at Tips if (depth === maxDepth && progress >= 1.0) drawLeaf()

Adds leaves only at the outermost branches when fully grown

conditional Child Branch Spawning if (depth < maxDepth && progress >= 1.0) { ... drawBranch(...) }

Creates two child branches (left and right) when parent is fully grown

Line by Line:

if (depth > maxDepth) return
Base case for recursion - stops when we've reached the maximum branch depth
const g = treeGrowth * (maxDepth + 1)
Scales the global growth value (0 to 1) into a range that matches our depth levels (0 to maxDepth+1)
const local = g - depth
Calculates how much this specific depth level has grown by subtracting its depth from the scaled growth
if (local <= 0) return
If this branch hasn't started growing yet (local growth is negative), skip drawing it
const progress = local < 1 ? local : 1
Converts growth progress to a 0-1 value: if local is less than 1, use it; otherwise cap at 1 (fully grown)
const thickness = map(depth, 0, maxDepth, 12, 1.5)
Maps depth (0 to maxDepth) to thickness (12 to 1.5), making the trunk thick and twigs thin
const c = lerpColor(trunkColor, twigColor, depth / maxDepth)
Blends between dark brown and light brown proportionally to depth, creating color gradient
const swayStrength = map(depth, 0, maxDepth, 0.2, 1.0)
Maps depth to sway strength (0.2 to 1.0), so the trunk barely sways but tips sway a lot
const localSway = wind * swayStrength
Multiplies the wind value by sway strength to get the actual rotation angle for this branch
push()
Saves the current transformation state before applying rotations and translations
rotate(localSway)
Rotates the coordinate system by the wind sway amount, making the branch lean
stroke(c); strokeWeight(thickness); noFill()
Sets the line color, thickness, and ensures no fill for drawing just the outline
const segLen = len * progress
Calculates how much of this branch segment should be drawn based on growth progress
line(0, 0, 0, -segLen)
Draws a line from the current position upward (negative Y) by the segment length
translate(0, -segLen)
Moves the origin to the end of the branch so child branches start from the correct position
if (depth === maxDepth && progress >= 1.0) drawLeaf()
Draws a leaf cluster only at the deepest branches when they're fully grown
if (depth < maxDepth && progress >= 1.0) { const childLen = len * 0.72
If not at max depth and this branch is fully grown, calculate child branch length as 72% of parent
push(); rotate(branchAngle); drawBranch(childLen, depth + 1, wind); pop()
Rotates right by the branch angle and recursively draws the right child branch
push(); rotate(-branchAngle); drawBranch(childLen, depth + 1, wind); pop()
Rotates left by the branch angle and recursively draws the left child branch
pop()
Restores the transformation state after drawing this branch and its children

drawLeaf()

drawLeaf() is called at the tip of every fully-grown branch. It draws two overlapping ellipses to create a simple but effective leaf cluster. The slight transparency (alpha 220) allows leaves to blend naturally with the background. This function is positioned at the end of each branch by drawBranch().

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);
}

๐Ÿ”ง Subcomponents:

styling Leaf Appearance noStroke(); fill(34, 139, 34, 220)

Sets leaf color to forest green with slight transparency

calculation Leaf Size const leafW = 10; const leafH = 14

Defines the width and height of individual leaf ellipses

drawing Leaf Cluster ellipse(0, 0, leafW, leafH); ellipse(3, -2, leafW * 0.8, leafH * 0.8)

Draws two overlapping ellipses to create a natural-looking leaf cluster

Line by Line:

noStroke()
Removes the outline so leaves are solid filled shapes
fill(34, 139, 34, 220)
Sets the fill color to forest green (RGB: 34, 139, 34) with alpha 220 (slightly transparent)
const leafW = 10
Sets the width of the main leaf ellipse to 10 pixels
const leafH = 14
Sets the height of the main leaf ellipse to 14 pixels (taller than wide for a leaf shape)
ellipse(0, 0, leafW, leafH)
Draws the first (main) leaf ellipse at the current position (0, 0)
ellipse(3, -2, leafW * 0.8, leafH * 0.8)
Draws a smaller second leaf (80% size) offset to the right and up, creating a clustered appearance

drawSkyGradient()

drawSkyGradient() creates a smooth color transition from light blue at the top to white at the bottom by drawing many horizontal lines with gradually changing colors. This is called per-pixel gradient rendering. While it's simple to understand, it's not the most efficient method - see improvements for optimization suggestions.

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);
  }
}

๐Ÿ”ง Subcomponents:

calculation Gradient Colors const topColor = color(180, 220, 255); const bottomColor = color(255, 255, 255)

Defines the start (light blue) and end (white) colors for the gradient

for-loop Horizontal Line Loop for (let y = 0; y < height; y++)

Iterates through every pixel row to draw horizontal lines with interpolated colors

calculation Color Blending const t = y / max(height - 1, 1); const c = lerpColor(topColor, bottomColor, t)

Calculates the blend factor and interpolates between top and bottom colors

Line by Line:

const topColor = color(180, 220, 255)
Defines a light sky blue color for the top of the gradient
const bottomColor = color(255, 255, 255)
Defines white for the bottom of the gradient
noFill()
Ensures we're only drawing strokes (lines), not filled shapes
for (let y = 0; y < height; y++)
Loops through every horizontal line from top (y=0) to bottom (y=height)
const t = y / max(height - 1, 1)
Calculates a value from 0 to 1 representing how far down the canvas we are (0 at top, 1 at bottom)
const c = lerpColor(topColor, bottomColor, t)
Blends between top and bottom colors based on position (0 = top color, 1 = bottom color)
stroke(c)
Sets the line color to the interpolated color
line(0, y, width, y)
Draws a horizontal line across the full width at the current y position

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window is resized. It ensures the canvas always fills the entire window. Combined with the responsive calculations in draw() and setup(), this makes the sketch adapt beautifully to any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Updates the canvas size to match the new window dimensions whenever the browser window is resized

๐Ÿ“ฆ Key Variables

maxDepth number

Controls the maximum recursion depth - higher values create bushier trees with more branches. Set to 9 for a balanced tree.

let maxDepth = 9;
baseLength number

The length of the trunk at the root. Calculated as 25% of the smaller canvas dimension so the tree scales to fit any window.

let baseLength = min(width, height) * 0.25;
branchAngle number

The angle (in radians) at which child branches split from their parent. Set to 25 degrees (0.436 radians) for natural-looking branching.

let branchAngle = radians(25);
treeGrowth number

A value from 0 to 1 representing how much the tree has grown. Increases over time to create the growth animation.

let treeGrowth = 0;
trunkColor color

The dark brown color used for thick trunk branches. Blends toward twigColor as branches get deeper.

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

The lighter brown color used for thin twig branches. The endpoint of the color gradient from trunkColor.

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

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change maxDepth from 9 to 12 to create a much bushier, more complex tree. Notice how it takes longer to fully grow. Try values between 6-14 to find your favorite complexity level.
  2. Modify branchAngle from radians(25) to radians(35) or radians(15) to change how wide the branches spread. Larger angles create wider trees, smaller angles create narrower, more columnar trees.
  3. Change the 0.72 multiplier in the line 'const childLen = len * 0.72' to 0.65 or 0.80 to make branches taper faster or slower. Smaller values create thinner, more delicate trees.
  4. Adjust the growth speed by changing 0.0035 in 'treeGrowth + 0.0035' to 0.01 (faster) or 0.001 (slower) to control how quickly the tree grows.
  5. Modify the wind strength from radians(6) to radians(12) or radians(3) to make the tree sway more dramatically or more subtly.
  6. Change the leaf color from color(34, 139, 34, 220) to color(255, 100, 0, 200) to create an autumn tree with orange leaves, or experiment with other RGB values.
  7. Modify the sky gradient by changing topColor to color(255, 150, 100) for a sunset effect, or bottomColor to color(200, 200, 200) for an overcast sky.
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

PERFORMANCE drawSkyGradient()

Drawing a horizontal line for every single pixel row (height iterations) is inefficient. At 1080p, this means 1080 line draws every frame.

๐Ÿ’ก Use createGraphics() to draw the gradient once and cache it, or use a larger step size (every 2-4 pixels) and let p5.js interpolate between them. Alternatively, use a shader or canvas gradient API for better performance.

BUG drawSkyGradient() - line 't = y / max(height - 1, 1)'

The max(height - 1, 1) is defensive but unnecessary since height is always > 0. This adds a tiny bit of complexity.

๐Ÿ’ก Simplify to 'const t = y / (height - 1)' or 'const t = y / height' depending on whether you want the gradient to reach exactly to the bottom.

FEATURE drawBranch()

All trees look similar because branch angle and length ratio are fixed. Users can't customize the tree shape easily.

๐Ÿ’ก Add interactive controls: let users press arrow keys or use sliders to adjust branchAngle and the 0.72 multiplier in real-time to experiment with different tree shapes.

STYLE Global variables

The color values (80, 42, 15) and (150, 95, 45) are hardcoded in multiple places. If you want to change the tree colors, you have to find and edit multiple lines.

๐Ÿ’ก Define constants at the top: const TRUNK_COLOR = color(80, 42, 15); const TWIG_COLOR = color(150, 95, 45); const LEAF_COLOR = color(34, 139, 34, 220); Then use these throughout the code for easier customization.

FEATURE drawLeaf()

All leaves are identical - same size, same shape, same position. Real trees have variation in their leaves.

๐Ÿ’ก Add randomness: use random() to vary leaf size slightly, or use random rotation to make leaves point in different directions. This would make the tree look more natural.

PERFORMANCE draw() - baseLength lerp

The baseLength is recalculated and lerped every frame even when the window hasn't been resized, wasting computation.

๐Ÿ’ก Only recalculate baseLength inside windowResized() instead of every frame. Store targetBaseLength as a global variable and only update it when the window size actually changes.

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