AI Temporal Dendrite - Time-Driven Fractal Tree - xelsed.ai

This sketch grows a recursive fractal tree whose shape is driven entirely by the current time of day - the hour sets how many main branches sprout from the trunk, the minute controls how wide those branches fan out, and the second (plus milliseconds) creates a gentle continuous sway. Branch color shifts from dark brown at the roots to spring green at the outermost twigs, and Perlin noise adds organic, non-repeating wobble to every tip.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up and amplify the sway — Increasing both the amplitude and speed inside the sine wave makes the whole tree sway more dramatically and quickly, like it's caught in wind.
  2. Force a fixed branch count — Overriding mainBranches with a constant number lets you preview what the tree looks like at any hour, instead of waiting for the real clock to change.
  3. Grow a much taller tree — Raising the height percentage and pixel cap used for the trunk length makes the entire tree scale up dramatically.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch turns the current time of day into a growing fractal tree: hour() decides how many main branches fan out of the trunk, minute() sets how wide that fan spreads, and second() combined with millis() drives a soft, continuous sway. The tree is built with a recursive function that calls itself to split each branch into two smaller children, using push()/pop(), rotate(), and translate() to position every segment relative to its parent. Color is computed in HSB mode and interpolated with lerp() so the trunk starts dark brown and the outermost twigs end up spring green. Perlin noise (noise()) adds organic, non-jittery motion to the branch tips, making the whole tree feel alive rather than static.

The code has just two moving parts to study: setup(), which configures the canvas and color mode once, and draw(), which reads the clock every frame and kicks off the recursive drawBranch() function. By reading drawBranch() closely you'll learn how recursion, coordinate transforms, and depth-based interpolation combine to generate complex, natural-looking shapes from a handful of simple rules.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, switches to HSB color mode for easier hue interpolation, and turns off fill since only lines are drawn.
  2. Every frame, draw() clears the background and reads hour(), minute(), and second() to compute the branch count, spread angle, and sway amount for this instant.
  3. draw() moves the origin to the bottom-center of the canvas and draws the trunk as a single thick, swaying brown line using the trunk angle and length it just calculated.
  4. From the top of the trunk, draw() either draws one branch straight up or fans multiple branches evenly across the spread angle, one for each hour on a 12-hour clock.
  5. Each branch calls drawBranch() recursively: it rotates by its assigned angle plus sway plus noise-based wobble, draws a line, moves to its tip, and spawns two smaller child branches at slightly different angles.
  6. Color and stroke width are recalculated at every recursion depth using lerp() and map(), so branches naturally fade from brown to green and taper from thick to thin as the recursion approaches MAX_DEPTH.

🎓 Concepts You'll Learn

Recursionpush()/pop() matrix stackingtranslate() and rotate() transformsHSB color mode and lerp() color interpolationPerlin noise for organic motionReading system time with hour()/minute()/second()map() for range conversion

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the right place to configure global settings like canvas size and color mode that the rest of the sketch will rely on every frame.

function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSB, 360, 100, 100);
  noFill();
  strokeCap(ROUND);
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window so the tree has as much room as possible to grow.
colorMode(HSB, 360, 100, 100);
Switches p5's color system from RGB to Hue-Saturation-Brightness, making it much easier to smoothly shift color (like brown to green) by just changing the hue number.
noFill();
Turns off shape fill since the tree is drawn entirely with line() strokes, not filled shapes.
strokeCap(ROUND);
Rounds the ends of every line segment so branches connect smoothly instead of showing sharp square edges.

draw()

draw() runs continuously, so every frame it re-reads the system clock and rebuilds the entire tree from scratch based on the current time. This is a good example of driving generative art with a real-world data source instead of random values.

🔬 This loop spawns one main branch for every hour on the 12-hour clock, fanning them out evenly. What happens if you change spreadRad * 0.8 to spreadRad * 2, making each branch's own children spread out much wider than the main fan itself?

    for (let i = 0; i < mainBranches; i++) {
      const angleOffset = startAngle + step * i;
      drawBranch(firstBranchLength, 1, spreadRad * 0.8, angleOffset, i + 1);
    }

🔬 The trunk only uses 40% of the global sway amount so it moves less than the branches above it. What happens if you change globalSway * 0.4 to globalSway * 2, making the whole trunk sway even more than its branches?

  const trunkAngle = -HALF_PI + globalSway * 0.4; // trunk gently sways as one piece
  const trunkEndX = baseLength * cos(trunkAngle);
  const trunkEndY = baseLength * sin(trunkAngle);
function draw() {
  // Soft cream / paper background
  // HSB: warm hue, low saturation, high brightness
  background(45, 12, 96);

  // --- Time-based parameters ---
  // Hour → number of main branches (1–12)
  let h = hour() % 12;
  if (h === 0) h = 12;
  const mainBranches = h;

  // Minute → angle spread (15°–45°)
  const m = minute();
  const spreadDeg = map(m, 0, 59, 15, 45);
  const spreadRad = radians(spreadDeg);

  // Second → subtle global sway (smooth using millis())
  timeSeconds = second() + millis() / 1000.0;
  globalSway = radians(2) * sin(timeSeconds * 0.6); // ±2° in radians

  // Base trunk length relative to canvas
  const baseLength = min(height * 0.35, 280);

  // --- Draw the tree from bottom center ---
  push();
  translate(width / 2, height); // bottom center origin
  noFill();

  // Draw trunk (depth 0)
  const trunkAngle = -HALF_PI + globalSway * 0.4; // trunk gently sways as one piece
  const trunkEndX = baseLength * cos(trunkAngle);
  const trunkEndY = baseLength * sin(trunkAngle);

  // Trunk color: dark brown
  strokeWeight(MAX_STROKE);
  const trunkHue = 30;   // brownish
  const trunkSat = 80;
  const trunkBri = 40;
  stroke(trunkHue, trunkSat, trunkBri);
  line(0, 0, trunkEndX, trunkEndY);

  // Move to top of trunk where main branches start
  translate(trunkEndX, trunkEndY);

  // --- Spawn main branches based on hour() ---
  const firstBranchLength = baseLength * LENGTH_FACTOR;

  if (mainBranches === 1) {
    // Single main branch straight up
    drawBranch(firstBranchLength, 1, spreadRad * 0.8, 0, 1);
  } else {
    // Fan of branches within [-spread, +spread]
    const totalSpread = spreadRad * 2;
    const step = totalSpread / (mainBranches - 1);
    const startAngle = -spreadRad;
    for (let i = 0; i < mainBranches; i++) {
      const angleOffset = startAngle + step * i;
      drawBranch(firstBranchLength, 1, spreadRad * 0.8, angleOffset, i + 1);
    }
  }

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

🔧 Subcomponents:

conditional Single Branch vs Fan Check if (mainBranches === 1) {

Handles the special case of exactly one main branch (avoids dividing by zero when computing the angle step for a fan).

for-loop Main Branch Fan Loop for (let i = 0; i < mainBranches; i++) {

Spawns one main branch per hour value, spacing each one evenly across the minute-controlled spread angle.

background(45, 12, 96);
Repaints the whole canvas each frame with a warm, low-saturation, bright cream color so the previous frame's tree doesn't leave trails.
let h = hour() % 12;
Gets the current hour (0-23) and reduces it to a 12-hour clock using modulo, so both 3am and 3pm give the same value.
if (h === 0) h = 12;
Fixes the edge case where midnight or noon would otherwise map to 0 branches - forces it to 12 instead.
const mainBranches = h;
Stores the final branch count, which will directly control how many main limbs grow from the trunk.
const spreadDeg = map(m, 0, 59, 15, 45);
Converts the current minute (0-59) into a spread angle between 15 and 45 degrees using map(), a core p5 function for rescaling ranges.
timeSeconds = second() + millis() / 1000.0;
Combines the whole second with the fractional milliseconds since the sketch started, producing a smoothly increasing time value instead of a value that jumps once per second.
globalSway = radians(2) * sin(timeSeconds * 0.6); // ±2° in radians
Uses a sine wave over time to produce a smooth back-and-forth sway of up to 2 degrees, converted to radians for use with rotate().
const baseLength = min(height * 0.35, 280);
Sets the trunk's length to 35% of the window height, but never more than 280 pixels, keeping the tree proportional on both small and huge screens.
translate(width / 2, height); // bottom center origin
Moves the drawing origin (0,0) to the bottom-center of the canvas so the tree grows upward from there like a real tree from the ground.
const trunkAngle = -HALF_PI + globalSway * 0.4; // trunk gently sways as one piece
Points the trunk straight up (-HALF_PI is 'up' in p5's angle system) and adds a small fraction of the sway so the whole trunk gently rocks.
line(0, 0, trunkEndX, trunkEndY);
Draws the trunk as a single straight line from the ground origin to its calculated end point.
translate(trunkEndX, trunkEndY);
Shifts the origin to the top of the trunk so all the main branches are drawn starting from that point instead of the ground.
const firstBranchLength = baseLength * LENGTH_FACTOR;
Shrinks the trunk length by the LENGTH_FACTOR to get the length of the first level of branches, following the same shrink rule used at every recursion level.
const step = totalSpread / (mainBranches - 1);
Calculates the even angular gap between each main branch so they spread out symmetrically across the full spread angle.
drawBranch(firstBranchLength, 1, spreadRad * 0.8, angleOffset, i + 1);
Kicks off the recursive branch-drawing function for this particular main branch, passing its length, starting depth, spread, angle, and a unique ID used for noise variation.

drawBranch()

drawBranch() is the heart of the fractal - it calls itself twice each time it runs, which is the classic recursive pattern for generating trees, ferns, and other branching natural forms from a handful of simple rules applied over and over.

🔬 This block uses Perlin noise so tips wobble smoothly instead of jittering randomly every frame. What happens if you change timeSeconds * 0.2 to timeSeconds * 2, making the noise change much faster over time?

  const noiseInput = branchId * 0.35 + depth * 0.7;
  const n = noise(noiseInput, timeSeconds * 0.2); // [0,1]
  const noiseAmp = radians(1 + 5 * t);           // up to ~6° at tips
  const noiseAngle = map(n, 0, 1, -noiseAmp, noiseAmp);

🔬 Every branch currently splits into exactly two children. What happens if you add a third drawBranch() call here with angle 0 (straight ahead), turning this into a three-way split?

    // Left child
    drawBranch(nextLen, depth + 1, spread * 0.9, -childSpread, branchId * 2 + 1);
    // Right child
    drawBranch(nextLen, depth + 1, spread * 0.9, childSpread, branchId * 2 + 2);
function drawBranch(len, depth, spread, angle, branchId) {
  if (depth > MAX_DEPTH || len < 2) return;

  push();

  // Normalized depth 0..1
  const t = depth / MAX_DEPTH;

  // Local sway increases toward the tips
  const localSway = globalSway * (0.4 + 0.6 * t);

  // Noise-based tip motion (stronger toward tips)
  const noiseInput = branchId * 0.35 + depth * 0.7;
  const n = noise(noiseInput, timeSeconds * 0.2); // [0,1]
  const noiseAmp = radians(1 + 5 * t);           // up to ~6° at tips
  const noiseAngle = map(n, 0, 1, -noiseAmp, noiseAmp);

  // Apply rotation for this branch
  rotate(angle + localSway + noiseAngle);

  // Depth-based color from dark brown → spring green
  // Brownish: H≈30, S≈80, B≈40
  // Spring green: H≈110, S≈60, B≈90
  const hCol = lerp(30, 110, t);
  const sCol = lerp(80, 60, t);
  const bCol = lerp(40, 90, t);
  stroke(hCol, sCol, bCol);

  // Stroke weight decreases with depth
  const sw = map(depth, 0, MAX_DEPTH, MAX_STROKE, MIN_STROKE);
  strokeWeight(sw);

  // Draw the branch segment
  line(0, 0, 0, -len);
  translate(0, -len);

  // Compute next branch length
  const nextLen = len * LENGTH_FACTOR;

  // Recurse: each branch splits into two children
  if (depth < MAX_DEPTH) {
    const childSpread = spread;

    // Left child
    drawBranch(nextLen, depth + 1, spread * 0.9, -childSpread, branchId * 2 + 1);
    // Right child
    drawBranch(nextLen, depth + 1, spread * 0.9, childSpread, branchId * 2 + 2);
  }

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

🔧 Subcomponents:

conditional Recursion Base Case if (depth > MAX_DEPTH || len < 2) return;

Stops the recursion once the branch is too deep or too short to draw, preventing infinite recursion and wasted tiny segments.

conditional Two-Child Recursive Split if (depth < MAX_DEPTH) {

Spawns two smaller child branches at slightly different angles, which is what turns a single line into a branching fractal structure.

if (depth > MAX_DEPTH || len < 2) return;
The recursion's exit condition: stop drawing once branches get too deep or too short to be visible, otherwise the function would call itself forever.
const t = depth / MAX_DEPTH;
Converts the current recursion depth into a 0-to-1 fraction, used throughout the function to blend colors and sizes smoothly from trunk to tip.
const localSway = globalSway * (0.4 + 0.6 * t);
Makes branches near the tips (higher t) sway more than branches near the trunk, mimicking how real branch tips whip around more than thick limbs.
const n = noise(noiseInput, timeSeconds * 0.2); // [0,1]
Samples Perlin noise using a unique input per branch (so each branch wobbles differently) and time (so it animates smoothly rather than jumping randomly).
const noiseAngle = map(n, 0, 1, -noiseAmp, noiseAmp);
Converts the 0-to-1 noise value into an angle that can swing both left and right of straight, for a natural wobbling effect.
rotate(angle + localSway + noiseAngle);
Applies all three angle influences at once - the branch's fixed angle, the time-based sway, and the noise wobble - before drawing this segment.
const hCol = lerp(30, 110, t);
Blends the hue from 30 (brown) at the trunk to 110 (spring green) at the tips, using the depth fraction t as the blend amount.
const sw = map(depth, 0, MAX_DEPTH, MAX_STROKE, MIN_STROKE);
Maps the current depth to a stroke width between the thick trunk width and the thin tip width, so branches visibly taper as they get smaller.
line(0, 0, 0, -len);
Draws this branch segment as a straight line 'upward' in its own rotated coordinate system (negative y is up after the rotate() call).
translate(0, -len);
Moves the origin to the tip of the branch just drawn, so any child branches start from the correct point.
const nextLen = len * LENGTH_FACTOR;
Shrinks the length for the next level of branches, which is what makes the tree taper naturally instead of every branch being the same size.
drawBranch(nextLen, depth + 1, spread * 0.9, -childSpread, branchId * 2 + 1);
Recursively calls itself to draw the left child branch, one depth deeper, slightly less spread, and a new unique branchId for its own noise pattern.
drawBranch(nextLen, depth + 1, spread * 0.9, childSpread, branchId * 2 + 2);
Recursively calls itself to draw the right child branch, mirroring the left one at a positive angle.

windowResized()

windowResized() is a p5.js callback that automatically fires whenever the browser window is resized, letting you keep responsive sketches that adapt to any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Whenever the browser window changes size, this resizes the canvas to match, keeping the tree centered and full-screen instead of leaving old canvas edges visible.

📦 Key Variables

MAX_DEPTH number

The maximum recursion depth for branches - controls how many times the tree splits and therefore how bushy/detailed it looks.

const MAX_DEPTH = 9;
LENGTH_FACTOR number

The multiplier applied to branch length at each recursion level, controlling how quickly branches taper down toward the tips.

const LENGTH_FACTOR = 0.67;
MAX_STROKE number

The stroke weight used for the trunk (depth 0), the thickest part of the tree.

const MAX_STROKE = 14;
MIN_STROKE number

The stroke weight used at the deepest recursion level, giving the thinnest twig tips.

const MIN_STROKE = 1.2;
globalSway number

The current overall sway angle in radians, recalculated every frame from a sine wave over time and applied throughout the tree with varying strength.

let globalSway = 0;
timeSeconds number

A smoothly increasing time value (seconds plus fractional milliseconds) used to animate both the sway and the Perlin noise so motion looks continuous rather than stepping once per second.

let timeSeconds = 0;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw() and drawBranch()

The entire fractal (up to 12 main branches, each recursing 9 levels deep, meaning up to a few thousand line() and noise() calls) is fully recomputed and redrawn from scratch every single frame at 60fps, even though the tree's shape barely changes between frames since it's driven by slow-moving hour/minute values.

💡 Consider caching the static parts of the tree (trunk and lower branches) to an offscreen buffer with createGraphics(), only redrawing when the minute/hour actually changes, and reserving the per-frame recursion for just the swaying tip motion.

STYLE draw() and drawBranch()

Color values like the trunk hue (30), spring green hue (110), and saturation/brightness numbers are hardcoded as magic numbers scattered across both functions, making them hard to tweak consistently.

💡 Extract them into named constants near the top of the file (e.g. TRUNK_HUE, TIP_HUE, TRUNK_SAT, TIP_SAT) so the whole color palette can be adjusted in one place.

FEATURE draw()

The tree currently has no interactivity - it only responds to the system clock, so a visitor can't influence it directly.

💡 Add mouseX-based wind strength (e.g. blend globalSway with a term derived from mouseX) so moving the mouse lets viewers 'blow' on the tree, adding an interactive layer on top of the time-based growth.

BUG drawBranch()

Because branchId grows exponentially (branchId * 2 + 1 / + 2 at every level), by depth 9 branchId can reach into the tens of thousands; combined with the noise() call this is harmless numerically but makes debugging or logging branch identity confusing at deep levels.

💡 Consider normalizing branchId (e.g. modulo some fixed range) before using it in noise() so noise inputs stay in a smaller, more predictable range while still giving each branch a distinct pattern.

🔄 Code Flow

Code flow showing setup, draw, drawbranch, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> timecheck[Read System Clock] timecheck --> branchfanloop[branch-fan-loop] branchfanloop --> singlevsfancheck[single-vs-fan-check] singlevsfancheck -->|One Branch| recursionbasecase[recursion-base-case] singlevsfancheck -->|Multiple Branches| branchfanloop branchfanloop --> recursiveSplit[recursive-split] recursiveSplit --> recursionbasecase recursionbasecase -->|Too Deep/Short| end[End] recursionbasecase -->|Continue| recursiveSplit draw --> windowresized[windowresized] click setup href "#fn-setup" click draw href "#fn-draw" click singlevsfancheck href "#sub-single-vs-fan-check" click branchfanloop href "#sub-branch-fan-loop" click recursionbasecase href "#sub-recursion-base-case" click recursiveSplit href "#sub-recursive-split"

❓ Frequently Asked Questions

What visual experience does the AI Temporal Dendrite sketch provide?

The sketch creates a mesmerizing fractal tree that evolves over time, with branches that sway gently and transition in color from brown roots to green tips, reflecting the passage of time.

Is there any user interaction in the AI Temporal Dendrite sketch?

The sketch is primarily time-driven, adjusting its visual parameters based on the current hour, minute, and second, providing a unique experience without direct user interaction.

What creative coding concept is showcased in the AI Temporal Dendrite sketch?

This sketch demonstrates the use of time-based parameters to influence organic growth patterns and visual aesthetics, effectively merging coding with natural forms.

Preview

AI Temporal Dendrite - Time-Driven Fractal Tree - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Temporal Dendrite - Time-Driven Fractal Tree - xelsed.ai - Code flow showing setup, draw, drawbranch, windowresized
Code Flow Diagram