Sketch 2026-02-19 21:46h

This sketch creates an interactive lightning storm scene where a tree stands in a landscape and randomly gets struck by branching lightning bolts. When lightning hits the tree, it animates falling to the ground over one second with a realistic rotation, and the background flashes white during each strike.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make lightning strike more often — Increase the probability that lightning triggers each frame, making strikes happen more frequently
  2. Make the tree fall faster — Reduce the time it takes for the tree to rotate to the ground after being struck
  3. Make lightning much more jagged — Increase the displacement amount so the lightning bolt deviates further from a straight path
  4. Change the grass color — Make the grass a different color—try a bright green or brown, or even red for an alien landscape
  5. Make the tree flip upside down — Change the fall angle so the tree rotates all the way to 180 degrees instead of just 90
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch animates a dramatic lightning storm with a single tree that falls when struck. The lightning is drawn using a recursive algorithm that creates jagged, branching bolts with a glowing effect, and collision detection determines whether each bolt hits the tree. When struck, the tree rotates smoothly to the ground over one second using lerp interpolation. The scene includes sky and grass layers that flash white during lightning strikes, creating a complete atmospheric effect.

The code is organized into several helper functions that each handle a specific visual layer: the sky and grass background, the tree with rotation, and the lightning bolt with its recursive jagged line algorithm. By reading it, you'll learn how recursion creates natural-looking fractals, how to detect if a drawn line intersects a shape on the canvas, and how to animate falling objects using angle interpolation and push/pop transforms.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas and positions a tree at a random location on the grass line (70% down the canvas)
  2. Every frame, draw() renders the sky and grass background, then has a 1% chance per frame to trigger a new lightning bolt
  3. When lightning is active, drawLightningBolt() creates a jagged bolt by calling drawJaggedLine(), which recursively subdivides line segments and displaces their midpoints randomly to simulate the jagged path of real lightning
  4. As drawJaggedLine() draws each small segment, it checks if that segment intersects the tree's x-range and y-position; if so, it sets treeHit to true and records the collision time
  5. Once the tree is hit, draw() animates it falling by using lerp() to smoothly interpolate the tree's rotation angle from 0 to 90 degrees (PI/2 radians) over treeFallDuration milliseconds
  6. The tree is drawn using push(), translate(), and rotate() to rotate it around its base position, creating the illusion of tipping over

🎓 Concepts You'll Learn

Recursion and fractalsCollision detectionAnimation with lerp and timingTransform matrices (push, pop, translate, rotate)Procedural generationGlow effects and layered drawing

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the perfect place to initialize variables and set up your canvas. Notice how windowResized() later re-runs similar code to keep the tree in proportion if the window size changes.

function setup() {
  createCanvas(windowWidth, windowHeight);
  drawSkyAndGrass(220); // Draw initial background

  // Position the tree slightly off-center
  treeX = width / 2 + random(-100, 100);
  treeY = height * 0.7; // Base of the tree on the grass line
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window - windowWidth and windowHeight are p5.js variables that automatically get the browser window dimensions
drawSkyAndGrass(220);
Calls a helper function to draw the initial sky and grass background with brightness 220 (light blue/green)
treeX = width / 2 + random(-100, 100);
Places the tree horizontally at the center of the canvas plus a random offset between -100 and 100 pixels, so it's positioned roughly in the middle but with variety
treeY = height * 0.7;
Places the tree's base at 70% down the canvas, which corresponds to the grass line (the bottom of the sky)

draw()

draw() is the heart of any p5.js sketch—it runs 60 times per second and handles all animation and interaction. Notice how this code uses timing with millis() to track how long lightning has been active and how long the tree has been falling. The combination of millis(), constrain(), and lerp() is a powerful pattern for smooth animations that happen over specific durations.

function draw() {
  // Always draw the background first based on the lightning state
  if (lightningOn) {
    // Flash the background white briefly
    if (millis() - lightningStartTime < flashDuration) {
      background(255); // White flash
    } else {
      // Return to a slightly darker sky/grass background after the flash
      drawSkyAndGrass(200);
    }
  } else {
    // Normal sky/grass background
    drawSkyAndGrass(220);
  }

  // Animate the tree falling if it has been hit
  if (treeHit) {
    let elapsedTime = millis() - treeHitTime;
    // 't' is a value between 0 and 1, representing the progress of the fall
    let t = constrain(elapsedTime / treeFallDuration, 0, 1);
    treeAngle = lerp(0, PI / 2, t); // Interpolate angle from 0 to 90 degrees (PI/2 radians)
  }

  // Draw the tree (will be rotated if treeHit is true)
  drawTree(treeX, treeY, treeAngle);

  // Check if it's time to draw lightning
  if (lightningOn) {
    drawLightningBolt(); // This function now also checks for tree collision
    // If lightning duration is over, turn it off
    if (millis() - lightningStartTime > lightningDuration) {
      lightningOn = false;
    }
  } else {
    // Randomly trigger lightning (1% chance per frame)
    // Only trigger if the tree hasn't been hit yet
    if (random() < 0.01 && !treeHit) {
      lightningOn = true;
      lightningStartTime = millis();
    }
  }
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Lightning Flash Effect if (lightningOn) { ... if (millis() - lightningStartTime < flashDuration) { background(255); }

Displays a white flash for the first flashDuration milliseconds when lightning is active, then returns to normal brightness

calculation Tree Fall Interpolation let t = constrain(elapsedTime / treeFallDuration, 0, 1); treeAngle = lerp(0, PI / 2, t);

Smoothly animates the tree's rotation from 0 to 90 degrees over treeFallDuration milliseconds

conditional Random Lightning Trigger if (random() < 0.01 && !treeHit) { lightningOn = true; lightningStartTime = millis(); }

Has a 1% chance each frame to start a new lightning bolt, but only if the tree hasn't been hit yet

if (lightningOn) {
Checks if lightning is currently active to determine what background to draw
if (millis() - lightningStartTime < flashDuration) {
Checks if less than flashDuration milliseconds have elapsed since the lightning started; if so, draw a white flash
background(255);
Fills the entire canvas with white (255) to create the bright flash effect
drawSkyAndGrass(200);
After the flash duration ends, draw the normal sky and grass with slightly darker brightness (200) to show the lightning is still active
drawSkyAndGrass(220);
When no lightning is active, draw the normal sky and grass with standard brightness (220)
let elapsedTime = millis() - treeHitTime;
Calculates how many milliseconds have passed since the tree was hit by subtracting the hit time from the current time
let t = constrain(elapsedTime / treeFallDuration, 0, 1);
Converts elapsed time into a value between 0 and 1: at 0ms t=0, at treeFallDuration t=1, and constrain() keeps it clamped in that range even after the animation finishes
treeAngle = lerp(0, PI / 2, t);
Uses lerp() to smoothly interpolate the tree's angle from 0 radians (upright) to PI/2 radians (90 degrees, lying flat) based on progress t
drawTree(treeX, treeY, treeAngle);
Draws the tree at its base position with the current rotation angle (0 if not hit, or gradually increasing to 90 degrees if falling)
if (lightningOn) {
Checks if it's time to draw the lightning bolt
drawLightningBolt();
Calls the function that draws the lightning bolt and checks for collision with the tree
if (millis() - lightningStartTime > lightningDuration) {
Checks if the lightning has been visible for longer than lightningDuration milliseconds
lightningOn = false;
Stops drawing the lightning bolt and returns to normal mode
if (random() < 0.01 && !treeHit) {
Rolls a random number; if it's less than 0.01 (1% chance) AND the tree hasn't been hit yet, start a new lightning bolt
lightningOn = true;
Sets the flag to start drawing lightning
lightningStartTime = millis();
Records the current time in milliseconds so we can track how long the lightning has been active

drawSkyAndGrass()

This helper function is called every frame to redraw the background. By parameterizing the skyBrightness, the main draw() function can pass different values (220 for normal, 200 for lightning, 255 for white flash) to show the scene's mood changing. This is a clean pattern for reusable background drawing.

function drawSkyAndGrass(skyBrightness) {
  // Sky
  noStroke();
  fill(0, 0, skyBrightness); // Dark blue sky
  rect(0, 0, width, height * 0.7); // Occupies the top 70% of the canvas

  // Grass
  fill(34, 139, 34); // Forest green
  rect(0, height * 0.7, width, height * 0.3); // Occupies the bottom 30% of the canvas
}
Line-by-line explanation (6 lines)
function drawSkyAndGrass(skyBrightness) {
Defines a function that takes one parameter, skyBrightness, which controls how bright the blue sky is (passed in as 220 or 200 from draw())
noStroke();
Disables outlines around shapes so the sky and grass are solid blocks with no borders
fill(0, 0, skyBrightness);
Sets the fill color to dark blue using RGB: red=0, green=0, blue=skyBrightness (which varies between 200 and 220)
rect(0, 0, width, height * 0.7);
Draws a rectangle from the top-left corner (0, 0) with width = full canvas width and height = 70% of canvas height, filling the sky area
fill(34, 139, 34);
Sets the fill color to forest green using RGB values that create a natural grass color
rect(0, height * 0.7, width, height * 0.3);
Draws a rectangle starting at 70% down the canvas with width = full canvas width and height = 30% of canvas height, filling the grass area below the sky

drawTree()

This function demonstrates the power of push(), translate(), and rotate(). By moving the origin to the tree's base and then rotating, we can make the tree fall naturally—it stays anchored at its base and tips over. This is much easier than calculating the positions of every part of the tree if it were rotating around a distant point. The negative y-coordinates for the trunk and canopy are because in p5.js, y increases downward, so negative y means 'upward.'

🔬 Two triangles create the canopy. The first one is wider (-treeWidth/2 to treeWidth/2) and the second is narrower (-treeWidth*0.6 to treeWidth*0.6). What if you added a third triangle with even different dimensions, like -treeWidth * 0.3 to treeWidth * 0.3?

  // Canopy (simplified as two triangles)
  fill(34, 139, 34); // Forest green
  triangle(
    -treeWidth / 2, -treeHeight * 0.8,
    treeWidth / 2, -treeHeight * 0.8,
    0, -treeHeight * 1.2
  );
  triangle(
    -treeWidth * 0.6, -treeHeight * 0.6,
    treeWidth * 0.6, -treeHeight * 0.6,
    0, -treeHeight * 1.0
  );
function drawTree(x, y, angle) {
  push(); // Save current drawing state
  translate(x, y); // Move origin to the base of the tree
  rotate(angle);   // Rotate around the new origin

  // Trunk
  fill(139, 69, 19); // Brown
  noStroke();
  // Draw trunk upwards from the base (which is at (0,0) in this translated system)
  rect(-treeWidth / 4, -treeHeight, treeWidth / 2, treeHeight);

  // Canopy (simplified as two triangles)
  fill(34, 139, 34); // Forest green
  triangle(
    -treeWidth / 2, -treeHeight * 0.8,
    treeWidth / 2, -treeHeight * 0.8,
    0, -treeHeight * 1.2
  );
  triangle(
    -treeWidth * 0.6, -treeHeight * 0.6,
    treeWidth * 0.6, -treeHeight * 0.6,
    0, -treeHeight * 1.0
  );

  pop(); // Restore original drawing state
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Trunk Shape rect(-treeWidth / 4, -treeHeight, treeWidth / 2, treeHeight);

Draws a brown rectangular trunk centered horizontally and extending upward from the base (y position)

calculation Canopy Triangles triangle(-treeWidth / 2, -treeHeight * 0.8, treeWidth / 2, -treeHeight * 0.8, 0, -treeHeight * 1.2); triangle(-treeWidth * 0.6, -treeHeight * 0.6, treeWidth * 0.6, -treeHeight * 0.6, 0, -treeHeight * 1.0);

Draws two overlapping green triangles to form the tree's foliage, creating a classic tree silhouette

function drawTree(x, y, angle) {
Defines a function that draws a tree at position (x, y) rotated by angle radians
push();
Saves the current drawing state (position, rotation, fill color, etc.) so we can restore it later—this is crucial when using transforms
translate(x, y);
Moves the origin (0, 0) point to position (x, y), so all subsequent drawing happens relative to the tree's base
rotate(angle);
Rotates everything drawn after this point by angle radians around the new origin (the tree's base)
fill(139, 69, 19);
Sets the fill color to brown for the trunk
noStroke();
Disables outlines so shapes are solid
rect(-treeWidth / 4, -treeHeight, treeWidth / 2, treeHeight);
Draws the trunk as a rectangle: starts at x = -treeWidth/4 (left edge of trunk) and y = -treeHeight (top of trunk, which is upward since y increases downward), with width = treeWidth/2 and height = treeHeight
fill(34, 139, 34);
Sets the fill color to forest green for the canopy
triangle(...);
Draws the first (larger) triangle for the tree's foliage
triangle(...);
Draws the second (slightly smaller) triangle overlapping the first for a fuller canopy
pop();
Restores the drawing state that was saved by push(), undoing the translate() and rotate() so subsequent drawing is back to normal coordinates

drawLightningBolt()

This function creates a convincing lightning effect using layered strokes. By drawing multiple semi-transparent lines of decreasing thickness on top of each other, you get a glow effect that mimics how real lightning appears in photographs. The 50% random branch adds visual complexity and makes each lightning strike look more natural and varied.

🔬 The lightning is drawn in three layers: outer glow (10px, alpha 50), middle (6px, alpha 100), and core (3px, alpha 200). What if you removed one of the layers? Try commenting out the first stroke/strokeWeight/drawJaggedLine call by putting // in front of it. Or what if you added a 4th layer with strokeWeight(15) and alpha 20?

  stroke(255, 255, 255, 50); // White with 50 alpha
  strokeWeight(10);
  drawJaggedLine(startX, startY, startX, endY, displacement * 0.3, segmentLength);

  stroke(255, 255, 255, 100); // White with 100 alpha
  strokeWeight(6);
  drawJaggedLine(startX, startY, startX, endY, displacement * 0.5, segmentLength);

  // Draw the main lightning bolt (thinnest and most opaque)
  stroke(255, 255, 255, 200); // White with 200 alpha
  strokeWeight(3);
  drawJaggedLine(startX, startY, startX, endY, displacement, segmentLength);
function drawLightningBolt() {
  let startX = width / 2; // Start in the middle top of the canvas
  let startY = 0;
  let endY = height * 0.7; // End at the grass line
  let displacement = 100; // How much the lightning can "wobble"
  let segmentLength = 20; // Length of each segment in the lightning bolt

  noFill();

  // Draw the glow effect first (thickest and most transparent)
  stroke(255, 255, 255, 50); // White with 50 alpha
  strokeWeight(10);
  drawJaggedLine(startX, startY, startX, endY, displacement * 0.3, segmentLength);

  stroke(255, 255, 255, 100); // White with 100 alpha
  strokeWeight(6);
  drawJaggedLine(startX, startY, startX, endY, displacement * 0.5, segmentLength);

  // Draw the main lightning bolt (thinnest and most opaque)
  stroke(255, 255, 255, 200); // White with 200 alpha
  strokeWeight(3);
  drawJaggedLine(startX, startY, startX, endY, displacement, segmentLength);

  // Add smaller branches to the lightning bolt
  if (random() < 0.5) { // 50% chance for a branch to appear
    let branchX = startX + random(-50, 50); // Random horizontal offset from the main bolt
    let branchY = random(height * 0.2, height * 0.5); // Random point along the main bolt's path
    let branchLength = random(50, 150);
    // Angle towards the grass, roughly between 45 and 135 degrees (PI/4 to 3*PI/4 radians)
    let branchAngle = random(PI / 4, 3 * PI / 4);
    let branchEndX = branchX + branchLength * cos(branchAngle);
    let branchEndY = branchY + branchLength * sin(branchAngle);

    // Limit the branch end to not go below the grass line
    branchEndY = min(branchEndY, endY);

    stroke(255, 255, 255, 150); // White with 150 alpha
    strokeWeight(2);
    drawJaggedLine(branchX, branchY, branchEndX, branchEndY, displacement * 0.2, segmentLength * 0.5);
  }
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

calculation Outer Glow stroke(255, 255, 255, 50); strokeWeight(10); drawJaggedLine(startX, startY, startX, endY, displacement * 0.3, segmentLength);

Draws the outermost, thickest, most transparent white layer for a glowing effect

calculation Middle Glow stroke(255, 255, 255, 100); strokeWeight(6); drawJaggedLine(startX, startY, startX, endY, displacement * 0.5, segmentLength);

Draws an intermediate layer for the glow effect

calculation Main Lightning Bolt stroke(255, 255, 255, 200); strokeWeight(3); drawJaggedLine(startX, startY, startX, endY, displacement, segmentLength);

Draws the central, thinnest, most opaque white line—the core of the lightning

conditional Random Branch Generation if (random() < 0.5) { ... drawJaggedLine(branchX, branchY, branchEndX, branchEndY, displacement * 0.2, segmentLength * 0.5); }

Has a 50% chance to create a secondary branching bolt from the main lightning

let startX = width / 2;
Sets the horizontal starting point of the lightning to the center of the canvas
let startY = 0;
Sets the vertical starting point to the very top of the canvas
let endY = height * 0.7;
Sets the vertical ending point to 70% down the canvas, which is the grass line (where the tree stands)
let displacement = 100;
Controls how much the lightning can deviate from a straight line—higher values create more jagged, wild bolts
let segmentLength = 20;
Controls how small each segment of the lightning fractal gets before stopping the recursion—smaller values create finer detail
noFill();
Disables fill so only the stroke (outline) is drawn, creating lines rather than filled shapes
stroke(255, 255, 255, 50);
Sets the stroke color to white (255, 255, 255) with alpha 50, creating a semi-transparent glow
strokeWeight(10);
Sets the thickness of the stroke to 10 pixels for the outer glow layer
drawJaggedLine(startX, startY, startX, endY, displacement * 0.3, segmentLength);
Draws the outer glow layer with reduced displacement (0.3x) so it's less jagged than the main bolt
stroke(255, 255, 255, 100);
Sets the stroke to white with alpha 100 for the middle glow layer
strokeWeight(6);
Makes the middle glow layer 6 pixels thick
stroke(255, 255, 255, 200);
Sets the stroke to white with alpha 200 (quite opaque) for the main bolt core
strokeWeight(3);
Makes the main bolt 3 pixels thick—thin and bright
if (random() < 0.5) {
Has a 50% chance to draw a branch each time this function is called
let branchX = startX + random(-50, 50);
Randomly offsets the branch horizontally from the main bolt by -50 to 50 pixels
let branchY = random(height * 0.2, height * 0.5);
Places the branch start point at a random height between 20% and 50% down the canvas
let branchLength = random(50, 150);
Randomizes how long the branch is: between 50 and 150 pixels
let branchAngle = random(PI / 4, 3 * PI / 4);
Randomizes the angle of the branch to point downward-ish: between 45 degrees (PI/4) and 135 degrees (3*PI/4)
let branchEndX = branchX + branchLength * cos(branchAngle);
Calculates where the branch ends horizontally using trigonometry: branchX plus the horizontal component of the angle and length
let branchEndY = branchY + branchLength * sin(branchAngle);
Calculates where the branch ends vertically using trigonometry: branchY plus the vertical component of the angle and length
branchEndY = min(branchEndY, endY);
Limits the branch end so it doesn't go below the grass line—keeps it within the visible area
drawJaggedLine(branchX, branchY, branchEndX, branchEndY, displacement * 0.2, segmentLength * 0.5);
Draws the branch using the same jagged line algorithm but with lower displacement and smaller segments for finer detail

drawJaggedLine()

This is the core of the sketch—a recursive fractal algorithm called 'recursive midpoint displacement.' It works by repeatedly breaking a line in half, moving the midpoint randomly, and then recursing on the two new segments. The random perpendicular displacement (using sin/cos trigonometry) creates a natural, jagged pattern that mimics real lightning. The collision detection is performed while drawing, checking each small segment to see if it hits the tree's bounding box. This is elegant because the lightning is being drawn AND checked for collisions in one pass.

function drawJaggedLine(x1, y1, x2, y2, displace, segmentLength) {
  let distance = dist(x1, y1, x2, y2); // Calculate the length of the current segment

  // Base case: If the segment is too short or displacement is too small, draw a straight line
  if (distance < segmentLength || displace < 1) {
    line(x1, y1, x2, y2);

    // Collision check for this short segment with the tree
    // Only check if the tree hasn't been hit yet
    if (!treeHit) {
      // Check if this segment intersects the tree's x-range
      // and is above the grass (where the tree stands)
      if (
        (x1 >= treeX - treeWidth / 2 && x1 <= treeX + treeWidth / 2 && y1 < height * 0.7) ||
        (x2 >= treeX - treeWidth / 2 && x2 <= treeX + treeWidth / 2 && y2 < height * 0.7)
      ) {
        treeHit = true;
        treeHitTime = millis();
        // You could add a sound effect here if p5.sound was included
        console.log("Tree hit!"); // Log to console for debugging
      }
    }
  } else {
    let midX = (x1 + x2) / 2;
    let midY = (y1 + y2) / 2;

    // Calculate a random displacement perpendicular to the line segment
    let angle = atan2(y2 - y1, x2 - x1); // Angle of the current segment
    let displacementX = sin(angle) * random(-displace, displace);
    let displacementY = -cos(angle) * random(-displace, displace);

    midX += displacementX;
    midY += displacementY;

    // Recursively call drawJaggedLine for the two new segments
    drawJaggedLine(x1, y1, midX, midY, displace * 0.5, segmentLength); // First half
    drawJaggedLine(midX, midY, x2, y2, displace * 0.5, segmentLength); // Second half
  }
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

conditional Base Case: Draw and Collide if (distance < segmentLength || displace < 1) { line(x1, y1, x2, y2); ... }

When the line segment is short enough, stop recursing and draw a straight line, then check if it hit the tree

conditional Tree Collision Detection if ((x1 >= treeX - treeWidth / 2 && x1 <= treeX + treeWidth / 2 && y1 < height * 0.7) || (x2 >= treeX - treeWidth / 2 && x2 <= treeX + treeWidth / 2 && y2 < height * 0.7))

Checks if either endpoint of the line segment is within the tree's x-range and y-range (above the grass line)

calculation Recursive Subdivision let midX = (x1 + x2) / 2; let midY = (y1 + y2) / 2; ... drawJaggedLine(x1, y1, midX, midY, displace * 0.5, segmentLength); drawJaggedLine(midX, midY, x2, y2, displace * 0.5, segmentLength);

Calculates the midpoint with random perpendicular displacement, then recursively draws two smaller segments, creating a jagged fractal pattern

function drawJaggedLine(x1, y1, x2, y2, displace, segmentLength) {
Defines a recursive function that draws a jagged line from (x1, y1) to (x2, y2) with displacement amount 'displace' and stops recursing when segments are smaller than 'segmentLength'
let distance = dist(x1, y1, x2, y2);
Calculates the straight-line distance between the two points using p5.js's dist() function
if (distance < segmentLength || displace < 1) {
Checks the stopping condition: if the segment is too short (< segmentLength pixels) OR the displacement is too small (< 1 pixel), we've recursed enough
line(x1, y1, x2, y2);
Draws a straight line from the start to the end point—this is the actual visible lightning stroke
if (!treeHit) {
Only checks for collision if the tree hasn't been hit yet (no need to check after it's already struck)
if ((x1 >= treeX - treeWidth / 2 && x1 <= treeX + treeWidth / 2 && y1 < height * 0.7) || ...
Checks if the first point of the line segment is within the tree's bounding box: x between treeX ± (treeWidth/2), and y above the grass line (height * 0.7)
(x2 >= treeX - treeWidth / 2 && x2 <= treeX + treeWidth / 2 && y2 < height * 0.7)
Also checks if the second point of the line segment is within the tree's bounding box
treeHit = true;
Sets the global variable treeHit to true, which triggers the falling animation in draw()
treeHitTime = millis();
Records the current time so the tree knows when to start falling
console.log("Tree hit!");
Prints a debug message to the browser console—you can open the Developer Tools (F12) to see this
let midX = (x1 + x2) / 2;
Calculates the x-coordinate of the midpoint between the start and end points
let midY = (y1 + y2) / 2;
Calculates the y-coordinate of the midpoint
let angle = atan2(y2 - y1, x2 - x1);
Calculates the angle of the line segment in radians using atan2()—this tells us which direction the line is pointing
let displacementX = sin(angle) * random(-displace, displace);
Calculates a random displacement in the x-direction perpendicular to the line: sin(angle) rotates the displacement 90 degrees sideways
let displacementY = -cos(angle) * random(-displace, displace);
Calculates a random displacement in the y-direction perpendicular to the line: -cos(angle) continues the 90-degree rotation
midX += displacementX;
Moves the midpoint sideways by the random perpendicular displacement
midY += displacementY;
Moves the midpoint perpendicular to the line segment
drawJaggedLine(x1, y1, midX, midY, displace * 0.5, segmentLength);
Recursively calls itself for the first half of the line (from start to displaced midpoint), with half the displacement for finer detail
drawJaggedLine(midX, midY, x2, y2, displace * 0.5, segmentLength);
Recursively calls itself for the second half of the line (from displaced midpoint to end), with half the displacement

windowResized()

windowResized() is a p5.js special function that gets called automatically when the browser window size changes. This is crucial for responsive sketches that need to adapt to different screen sizes. Without it, the canvas would stay the same size while the window changed, leaving empty space or cutting off content.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  drawSkyAndGrass(lightningOn ? 200 : 220);
  // Re-position the tree on resize to keep it relative to the canvas size
  treeX = width / 2 + random(-100, 100);
  treeY = height * 0.7;
}
Line-by-line explanation (5 lines)
function windowResized() {
This is a special p5.js function that is automatically called whenever the browser window is resized
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions so the sketch fills the screen
drawSkyAndGrass(lightningOn ? 200 : 220);
Redraws the background at the appropriate brightness: 200 if lightning is currently on, 220 if not
treeX = width / 2 + random(-100, 100);
Repositions the tree's x-coordinate relative to the new canvas width so it stays roughly centered
treeY = height * 0.7;
Repositions the tree's y-coordinate relative to the new canvas height so it stays on the grass line

📦 Key Variables

lightningOn boolean

Tracks whether lightning is currently visible and being drawn; used to control when lightning bolts are generated and displayed

let lightningOn = false;
lightningStartTime number

Stores the timestamp (in milliseconds) when the current lightning bolt started; used to calculate when to stop drawing it

let lightningStartTime;
lightningDuration number

Constant that defines how long a lightning bolt stays visible in milliseconds (500ms = 0.5 seconds)

const lightningDuration = 500;
flashDuration number

Constant that defines how long the background flashes white in milliseconds (100ms = 0.1 seconds) when lightning strikes

const flashDuration = 100;
treeHit boolean

Tracks whether the tree has been struck by lightning; prevents further lightning strikes and triggers the falling animation

let treeHit = false;
treeAngle number

Stores the current rotation angle of the tree in radians; animates from 0 to PI/2 (0 to 90 degrees) when falling

let treeAngle = 0;
treeHitTime number

Stores the timestamp when the tree was hit; used to calculate the progress of the falling animation

let treeHitTime;
treeFallDuration number

Constant that defines how long the tree takes to fall from upright to flat in milliseconds (1000ms = 1 second)

const treeFallDuration = 1000;
treeX number

Stores the x-coordinate of the tree's base (center position) on the canvas

let treeX;
treeY number

Stores the y-coordinate of the tree's base on the canvas (positioned at the grass line)

let treeY;
treeWidth number

Constant that defines the width of the tree trunk and canopy in pixels (used for drawing and collision detection)

const treeWidth = 80;
treeHeight number

Constant that defines the height of the tree in pixels (used for drawing and collision detection)

const treeHeight = 200;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG drawJaggedLine() collision detection

Collision detection only checks if line endpoints are in the tree's bounding box, not if the line segment itself passes through the box. A lightning bolt could pass right next to the tree and miss.

💡 Implement true line-to-rectangle collision detection by calculating the closest point on the line segment to the tree's center and checking if that distance is within the tree's width/height. This would make the collision detection much more reliable.

BUG setup() and windowResized()

The tree position is randomized even on window resize, which means the tree can jump to a new location. This breaks the immersion if the user resizes their browser while watching.

💡 Remove the random() offset from windowResized(), or store the tree's original randomized offset and apply it consistently: e.g., let treeOffsetX = random(-100, 100); in setup(), then treeX = width / 2 + treeOffsetX; in both setup() and windowResized().

STYLE drawLightningBolt()

The branch generation logic (50% random chance, random angle, random length) could produce branches that extend below the grass line or look unrealistic. The branch angles are constrained to PI/4 to 3*PI/4, which is good, but some branches might still not look natural.

💡 Add more constraints: limit branchLength based on how far the branch is from the main bolt, or ensure branches only extend toward the tree rather than in all directions.

FEATURE Global / overall

Once the tree is hit and falls, the sketch becomes static—no more lightning strikes, no animation. The user can only reset by refreshing the page.

💡 Add a reset mechanic: either automatically reset after a few seconds, or allow the user to click the canvas to reset the scene and start a new lightning storm with the tree standing upright again.

PERFORMANCE drawJaggedLine() recursive calls

The recursive algorithm could potentially go very deep if segmentLength is very small or displace stays large, creating many function calls and stack pressure. This might cause performance issues on slower devices.

💡 Add a maximum recursion depth check, or ensure that the displace parameter reduces quickly enough that recursion always terminates within a reasonable depth (currently it reduces by 0.5 each time, which is good, but explicit limits would help).

🔄 Code Flow

Code flow showing setup, draw, drawskyandgrass, drawtree, drawlightningbolt, drawjaggedline, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> lightning-flash-check[lightning-flash-check] draw --> tree-fall-animation[tree-fall-animation] draw --> lightning-trigger[lightning-trigger] draw --> drawskyandgrass[drawskyandgrass] draw --> drawtree[drawtree] draw --> drawlightningbolt[drawlightningbolt] draw --> drawjaggedline[drawjaggedline] draw --> windowresized[windowresized] lightning-flash-check -->|if lightning active| drawskyandgrass lightning-flash-check -->|else| drawskyandgrass click lightning-flash-check href "#sub-lightning-flash-check" tree-fall-animation -->|calculate rotation| drawtree click tree-fall-animation href "#sub-tree-fall-animation" lightning-trigger -->|1% chance| drawlightningbolt lightning-trigger -->|else| draw click lightning-trigger href "#sub-lightning-trigger" drawskyandgrass -->|pass skyBrightness| draw click drawskyandgrass href "#fn-drawskyandgrass" drawtree --> trunk-drawing[trunk-drawing] drawtree --> canopy-triangles[canopy-triangles] click drawtree href "#fn-drawtree" click trunk-drawing href "#sub-trunk-drawing" click canopy-triangles href "#sub-canopy-triangles" drawlightningbolt --> glow-layer-1[glow-layer-1] drawlightningbolt --> glow-layer-2[glow-layer-2] drawlightningbolt --> main-bolt[main-bolt] drawlightningbolt --> branch-logic[branch-logic] click drawlightningbolt href "#fn-drawlightningbolt" click glow-layer-1 href "#sub-glow-layer-1" click glow-layer-2 href "#sub-glow-layer-2" click main-bolt href "#sub-main-bolt" click branch-logic href "#sub-branch-logic" drawjaggedline --> base-case[base-case] drawjaggedline --> collision-check[collision-check] drawjaggedline --> recursive-case[recursive-case] click drawjaggedline href "#fn-drawjaggedline" click base-case href "#sub-base-case" click collision-check href "#sub-collision-check" click recursive-case href "#sub-recursive-case" windowresized --> setup click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual elements are featured in the Sketch 2026-02-19 21:46h?

This sketch visually represents a stormy scene with a tree that appears to fall when struck by lightning, accompanied by dynamic background changes simulating lightning flashes.

How can users interact with the Sketch 2026-02-19 21:46h?

Users can interact with the sketch by triggering lightning strikes, which causes the tree to fall and the background to flash white.

What creative coding concepts are showcased in this p5.js sketch?

The sketch demonstrates concepts such as animation through interpolation, event-based triggers for visual effects, and dynamic background rendering.

Preview

Sketch 2026-02-19 21:46h - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-02-19 21:46h - Code flow showing setup, draw, drawskyandgrass, drawtree, drawlightningbolt, drawjaggedline, windowresized
Code Flow Diagram