sunrise

This sketch animates a peaceful day-night cycle, showing the sky transitioning from starry night through dawn, bright day, sunset, and back to night. Clouds drift across the sky, birds glide through the scene, and a glowing sun arcs from the bottom to the top of the canvas, while layered silhouettes form a rolling ground at the horizon.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the sun bigger — The last argument of circle() is its diameter in pixels as a fraction of canvas width; doubling it makes the sun twice as large
  2. Speed up the entire animation — Dividing frameCount by a smaller number makes progress increase faster, so the day-night cycle completes in less time
  3. Remove stars completely — Setting starAlpha to 0 everywhere makes stars invisible throughout the entire animation
  4. Make clouds more visible at night — Adding a few lines to show clouds even in night phase creates a different mood
  5. Change the sun's glow color — The drawingContext.shadowColor controls the soft halo around the sun; change the RGB values to make it redder or yellower
  6. Add more birds to the sky — Increasing the loop count in setup() creates more birds flying around during the day
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a mesmerizing day-night cycle that plays over 40 seconds, transforming the sky from a starry night through brilliant dawn, warm day, and fiery sunset before returning to night. It combines four p5.js techniques at once: color gradients with lerpColor to smoothly blend sky hues, arrays of objects (stars, clouds, and birds) that animate independently, alpha transparency to fade elements in and out at the right moments, and layered shapes with curveVertex to build an organic ground silhouette. The result feels alive—nothing is static, and every element enters and exits the scene at a meaningful time.

The code is organized into three custom classes (Star, Cloud, Bird) that each handle their own drawing and movement, plus a draw() function that orchestrates the entire cycle by calculating a progress value from 0 to 1 and using it to control colors, transparency levels, and object positions. By studying this sketch, you will learn how to use progress variables to drive complex multi-phase animations, how to fade objects in and out using alpha values, and how to make an array of objects behave naturally through randomized properties like speed and appearance timing.

⚙️ How It Works

  1. When the sketch first loads, setup() creates a full-window canvas, initializes four color objects for each time-of-day, and populates three arrays: 200 stars randomly scattered in the upper half of the sky, 5 clouds at mid-height with random sizes and speeds, and 10 birds that will appear at staggered times during the day.
  2. Every frame, draw() calculates a progress value (0 to 1) based on frameCount divided by 1200, so the full cycle takes 40 seconds. This single progress number becomes the master control: it determines which phase the animation is in (night, dawn, day, sunset), which drives every other decision.
  3. The sky gradient is drawn using lerpColor to smoothly blend between four pairs of sky colors depending on progress: night purples fade to dawn pinks, which fade to bright day blues, which fade to orange sunsets, which fade back to night. This color blending creates the illusion of a real sky changing throughout the day.
  4. Stars fade in during night and fade out during dawn using an alpha value calculated from progress; they twinkle using a sine wave based on frameCount. Clouds appear during dawn with their alpha increasing, stay visible and drift horizontally during the day, then fade out during sunset.
  5. A glowing sun moves upward as progress increases (lerped from the bottom of the canvas to one-quarter height), giving it a realistic arc across the sky. The sun has a soft shadow blur effect to make it glow.
  6. Birds move horizontally across the canvas and fade in at random points during dawn, stay visible during the day, and fade out at random points during sunset. Each bird has its own speed and appearance timing, so they never coordinate—they feel independent.
  7. Three layered ground shapes at the bottom use curveVertex to create a rolling, organic horizon silhouette. These layers have different darkness values (0, 20, 40) to create a sense of depth. After each complete cycle (progress >= 1), frameCount resets to 0 so the animation loops seamlessly.

🎓 Concepts You'll Learn

Color interpolation with lerpColorObject-oriented programming with classesArray iteration and object managementAlpha transparency and fade effectsProgress-driven animationParametric curves with curveVertexCanvas gradients with drawingContextFrame-based timing and looping

📝 Code Breakdown

Star class

The Star class demonstrates two key ideas: storing per-object randomized data (like twinkleOffset) in the constructor, and using trigonometric functions (sine) to create smooth, cyclical animations. Every star gets a unique phase offset so they twinkle at different times, which feels more natural than all stars pulsing in unison.

class Star {
  constructor() {
    this.x = random(width);
    this.y = random(height / 2);
    this.size = random(1, 3);
    this.twinkleOffset = random(TWO_PI);
  }

  show(alpha) {
    // Only show if alpha is greater than 0
    if (alpha <= 0) return;

    // Calculate twinkling effect
    let twinkleAlpha = map(sin(frameCount * 0.1 + this.twinkleOffset), -1, 1, 0.5, 1);
    fill(255, alpha * twinkleAlpha);
    circle(this.x, this.y, this.size);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

constructor Star constructor this.twinkleOffset = random(TWO_PI);

Gives each star a unique phase offset so they don't all twinkle in sync

calculation Twinkling effect let twinkleAlpha = map(sin(frameCount * 0.1 + this.twinkleOffset), -1, 1, 0.5, 1);

Uses sine wave to smoothly vary opacity, creating a realistic twinkle

this.x = random(width);
Places each star at a random horizontal position across the full canvas width
this.y = random(height / 2);
Places each star in the upper half of the sky only, not in the lower ground area
this.size = random(1, 3);
Each star is a tiny circle between 1 and 3 pixels wide for a natural look
this.twinkleOffset = random(TWO_PI);
Stores a random angle (0 to 2π) that shifts the sine wave timing for this star alone
if (alpha <= 0) return;
Skips drawing if the alpha is zero or negative—a performance optimization that avoids invisible pixels
let twinkleAlpha = map(sin(frameCount * 0.1 + this.twinkleOffset), -1, 1, 0.5, 1);
The sine function oscillates between -1 and 1 over time; map() converts it to 0.5–1.0 opacity so stars fade in and out smoothly
fill(255, alpha * twinkleAlpha);
Multiplies the incoming alpha (from the draw function) by the twinkle alpha to fade the entire star group out while keeping individual twinkles
circle(this.x, this.y, this.size);
Draws a white circle at the star's position with its size

Cloud class

The Cloud class shows how to build a complex shape by combining simple primitives (ellipses). By drawing pairs of overlapping ellipses in a loop, you create the illusion of a fluffy cloud without needing image files. The move() method demonstrates a common pattern: update position each frame, then wrap around the screen to reuse the object.

🔬 This loop draws the cloud's puffy bumps. What happens if you change the offset from this.size * 0.2 to this.size * 0.4? Will the bumps spread farther apart?

    for (let i = 0; i < 3; i++) {
      let offset = this.size * 0.2;
      ellipse(this.x + i * offset, this.y, this.size, this.size * 0.6);
      ellipse(this.x + i * offset - this.size * 0.3, this.y + this.size * 0.1, this.size * 0.8, this.size * 0.5);
    }
class Cloud {
  constructor() {
    this.x = random(width);
    this.y = random(height * 0.2, height * 0.4);
    this.size = random(width * 0.1, width * 0.3);
    this.speed = random(0.5, 1.5);
  }

  show(alpha) {
    // Only show if alpha is greater than 0
    if (alpha <= 0) return;

    fill(255, alpha); // Translucent white
    noStroke();
    // Draw overlapping ellipses to create a cloud shape
    for (let i = 0; i < 3; i++) {
      let offset = this.size * 0.2;
      ellipse(this.x + i * offset, this.y, this.size, this.size * 0.6);
      ellipse(this.x + i * offset - this.size * 0.3, this.y + this.size * 0.1, this.size * 0.8, this.size * 0.5);
    }
  }

  move() {
    this.x += this.speed;
    // Wrap around the screen
    if (this.x > width + this.size) {
      this.x = -this.size;
    }
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Cloud shape drawing for (let i = 0; i < 3; i++) {

Draws 3 pairs of overlapping ellipses to build a puffy cloud silhouette

conditional Off-screen wrapping if (this.x > width + this.size) {

Teleports the cloud back to the left when it drifts off the right edge

this.x = random(width);
Each cloud starts at a random horizontal position
this.y = random(height * 0.2, height * 0.4);
Clouds are positioned in the upper-middle area, between 20% and 40% down from the top
this.size = random(width * 0.1, width * 0.3);
Cloud size is randomized relative to canvas width so it scales gracefully on different screens
this.speed = random(0.5, 1.5);
Each cloud drifts at its own speed between 0.5 and 1.5 pixels per frame
if (alpha <= 0) return;
Avoids drawing invisible clouds during night and sunset phases
for (let i = 0; i < 3; i++) {
Loops 3 times to draw 3 horizontal 'bumps' that make up the cloud shape
ellipse(this.x + i * offset, this.y, this.size, this.size * 0.6);
Draws the main bulge of each cloud bump, slightly wider than it is tall
ellipse(this.x + i * offset - this.size * 0.3, this.y + this.size * 0.1, this.size * 0.8, this.size * 0.5);
Draws a second, smaller ellipse overlapping the first to add puffiness and depth
this.x += this.speed;
Moves the cloud rightward each frame by its speed value
if (this.x > width + this.size) {
Checks if the cloud has drifted completely off the right edge of the canvas
this.x = -this.size;
Teleports the cloud back to the left edge, just out of view, so it drifts across again

Bird class

The Bird class demonstrates several advanced techniques: storing per-object timing data (appearsAtProgress, disappearsAtProgress) to orchestrate when each bird enters and exits the scene independently, using translate and scale for object-relative drawing, and building a recognizable shape from simple polygons. The wingAngle variable shows how sine waves create smooth, natural motion. Note that the wing angle is calculated but not currently used to rotate the wing shape—a learner could add rotation to make the wings actually flap!

🔬 This draws the left wing using two bezier curves. What happens if you change the -10 to -20? Will the wing stretch out longer?

    // Left wing (bezier curve)
    beginShape();
    vertex(0, 0);
    bezierVertex(-2, -2, -5, -4, -10, 0);
    bezierVertex(-5, 4, -2, 2, 0, 0);
    endShape();
class Bird {
  constructor() {
    this.x = random(width);
    this.y = random(height * 0.2, height * 0.5);
    this.speed = random(2, 4);
    this.size = random(10, 20); // Scale factor for the bird shape
    this.wingPhase = random(TWO_PI); // For wing animation
    this.appearsAtProgress = random(0.2, 0.7); // When the bird starts appearing
    this.disappearsAtProgress = random(0.8, 1.0); // When the bird starts disappearing
  }

  show(alpha) {
    if (alpha <= 0) return;

    fill(0, alpha); // Black birds
    noStroke();
    push();
    translate(this.x, this.y);
    scale(this.size / 10); // Scale bird based on its size property

    // Animate wings based on sin wave
    let wingAngle = sin(frameCount * 0.2 + this.wingPhase) * HALF_PI * 0.1;

    // Left wing (bezier curve)
    beginShape();
    vertex(0, 0);
    bezierVertex(-2, -2, -5, -4, -10, 0);
    bezierVertex(-5, 4, -2, 2, 0, 0);
    endShape();

    // Right wing (bezier curve)
    beginShape();
    vertex(0, 0);
    bezierVertex(2, -2, 5, -4, 10, 0);
    bezierVertex(5, 4, 2, 2, 0, 0);
    endShape();

    // Body (simple V shape)
    beginShape();
    vertex(-2, 0);
    vertex(0, 2);
    vertex(2, 0);
    endShape(CLOSE);

    pop();
  }

  move() {
    this.x += this.speed;
    // Wrap around the screen and reset y position for variety
    if (this.x > width + this.size) {
      this.x = -this.size;
      this.y = random(height * 0.2, height * 0.5);
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Wing flapping let wingAngle = sin(frameCount * 0.2 + this.wingPhase) * HALF_PI * 0.1;

Oscillates wing angle over time to simulate flapping motion

shape-drawing Bird silhouette beginShape(); vertex(0, 0); bezierVertex(-2, -2, -5, -4, -10, 0); bezierVertex(-5, 4, -2, 2, 0, 0); endShape();

Uses bezier curves to draw a single wing with a smooth, organic curve

this.x = random(width);
Each bird starts at a random horizontal position
this.y = random(height * 0.2, height * 0.5);
Birds fly in the upper-middle sky, between 20% and 50% down from the top
this.speed = random(2, 4);
Each bird flies at its own speed between 2 and 4 pixels per frame
this.size = random(10, 20);
Stores a scale factor so some birds appear closer (larger) and others farther away (smaller)
this.wingPhase = random(TWO_PI);
Gives each bird a unique wing-flap phase so they don't all flap in sync
this.appearsAtProgress = random(0.2, 0.7);
Stores a progress value between 0.2 and 0.7 (dawn phase) when this bird will start fading in
this.disappearsAtProgress = random(0.8, 1.0);
Stores a progress value between 0.8 and 1.0 (sunset phase) when this bird will start fading out
if (alpha <= 0) return;
Skips drawing if the bird is completely transparent
push();
Saves the current drawing state (transformations, fills) so changes only affect this bird
translate(this.x, this.y);
Moves the drawing origin to the bird's position so the bird is drawn relative to its own center
scale(this.size / 10);
Scales the entire bird up or down based on its size property (10 is the default, so a size of 20 doubles it)
let wingAngle = sin(frameCount * 0.2 + this.wingPhase) * HALF_PI * 0.1;
Creates a smoothly oscillating angle using sine; the 0.2 controls flap speed, and HALF_PI * 0.1 limits the angle to a small amount
beginShape(); vertex(0, 0); bezierVertex(-2, -2, -5, -4, -10, 0); bezierVertex(-5, 4, -2, 2, 0, 0); endShape();
Draws the left wing using two bezier curves; the first curves up and outward, the second curves back inward to close the wing shape
pop();
Restores the drawing state so subsequent draws are not affected by this bird's transformations

setup()

setup() runs exactly once when the sketch starts, making it the perfect place to initialize everything: the canvas, global arrays, and constant values like colors. This sketch demonstrates a pattern where setup() calls multiple loops to populate arrays with custom objects (stars, clouds, birds). Each object has its own random properties, so even though they follow the same pattern, they feel varied and natural.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  frameRate(30);

  // Initialize skyColors (moved from global scope)
  skyColors = {
    nightTop: color(20, 0, 50),
    nightBottom: color(50, 0, 100),
    dawnTop: color(255, 100, 100),
    dawnBottom: color(255, 200, 100),
    dayTop: color(100, 150, 255),
    dayBottom: color(200, 220, 255),
    sunsetTop: color(255, 150, 50),
    sunsetBottom: color(255, 80, 0)
  };

  // Initialize stars array and its elements
  stars = [];
  for (let i = 0; i < 200; i++) {
    stars.push(new Star());
  }

  // Initialize clouds array and its elements
  clouds = [];
  for (let i = 0; i < 5; i++) {
    clouds.push(new Cloud());
  }

  // Initialize birds array and its elements
  birds = [];
  for (let i = 0; i < 10; i++) {
    birds.push(new Bird());
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

object-initialization Sky color palette skyColors = { nightTop: color(20, 0, 50), ... };

Stores all the colors used for the four time-of-day phases so they're easy to adjust

for-loop Star creation for (let i = 0; i < 200; i++) {

Creates 200 Star objects with randomized positions and sizes

for-loop Cloud creation for (let i = 0; i < 5; i++) {

Creates 5 Cloud objects with randomized positions, sizes, and speeds

for-loop Bird creation for (let i = 0; i < 10; i++) {

Creates 10 Bird objects with randomized positions, speeds, and appearance/disappearance timings

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, using p5.js variables that track window size
noStroke();
Disables outlines on all shapes, so everything is filled only—no black borders
frameRate(30);
Sets the animation to run at 30 frames per second (slower than the default 60, which is gentler and more cinematic for this sketch)
skyColors = {
Creates an object to store color values for each time-of-day phase, making it easy to tweak the color scheme
stars = [];
Initializes the global stars array as empty before filling it with Star objects
for (let i = 0; i < 200; i++) {
Loops 200 times to create 200 stars; each star has random position and size
stars.push(new Star());
Creates a new Star object and adds it to the stars array; the Star constructor handles all randomization
clouds = [];
Initializes the global clouds array as empty before filling it with Cloud objects
birds = [];
Initializes the global birds array as empty before filling it with Bird objects

draw()

draw() is where all the magic happens: it runs 30 times per second and orchestrates every element on screen. The key insight is the progress variable, which is a single 0–1 value that everything depends on. By checking progress against different thresholds (0.25, 0.5, 0.75, 1), you divide the animation into four phases. Within each phase, you use map() to rescale the progress to another 0–1 range, then use lerpColor() to blend between two colors. This pattern—use progress to select a phase, then remap and interpolate within that phase—is incredibly powerful and reusable in any multi-stage animation. The sky gradient is drawn using drawingContext, which gives direct access to the HTML canvas API and allows efficient gradient rendering that would be slow to achieve with hundreds of rectangles.

🔬 This code controls how the sky changes color during the first half of the animation. What happens if you swap the order—put the day colors at 0.25 instead of 0.5? Will the sunrise happen faster?

  if (progress <= 0.25) { // Night to Dawn phase
    let p = map(progress, 0, 0.25, 0, 1);
    topC = lerpColor(skyColors.nightTop, skyColors.dawnTop, p);
    bottomC = lerpColor(skyColors.nightBottom, skyColors.dawnBottom, p);
  } else if (progress <= 0.5) { // Dawn to Day phase

🔬 This controls when stars are visible. What happens if you change starAlpha = 255 to starAlpha = 100? Will the night sky have fewer stars?

  // --- Stars ---
  let starAlpha = 0;
  if (progress <= 0.25) { // Full night
    starAlpha = 255;
  } else if (progress <= 0.5) { // Dawn fade out
    starAlpha = map(progress, 0.25, 0.5, 255, 0);
function draw() {
  // Slow down the progress: now takes 1200 frames (40 seconds)
  progress = constrain(frameCount / 1200, 0, 1);
  sunY = lerp(height, height / 4, progress);

  // Determine sky colors based on progress
  let topC, bottomC;

  if (progress <= 0.25) { // Night to Dawn phase
    let p = map(progress, 0, 0.25, 0, 1);
    topC = lerpColor(skyColors.nightTop, skyColors.dawnTop, p);
    bottomC = lerpColor(skyColors.nightBottom, skyColors.dawnBottom, p);
  } else if (progress <= 0.5) { // Dawn to Day phase
    let p = map(progress, 0.25, 0.5, 0, 1);
    topC = lerpColor(skyColors.dawnTop, skyColors.dayTop, p);
    bottomC = lerpColor(skyColors.dawnBottom, skyColors.dayBottom, p);
  } else if (progress <= 0.75) { // Day to Sunset phase
    let p = map(progress, 0.5, 0.75, 0, 1);
    topC = lerpColor(skyColors.dayTop, skyColors.sunsetTop, p);
    bottomC = lerpColor(skyColors.dayBottom, skyColors.sunsetBottom, p);
  } else { // Sunset to Night phase
    let p = map(progress, 0.75, 1, 0, 1);
    topC = lerpColor(skyColors.sunsetTop, skyColors.nightTop, p);
    bottomC = lerpColor(skyColors.sunsetBottom, skyColors.nightBottom, p);
  }

  // Draw sky gradient using Web Audio Context for better performance
  let gradient = drawingContext.createLinearGradient(0, 0, 0, height);
  gradient.addColorStop(0, topC.toString());
  gradient.addColorStop(1, bottomC.toString());
  drawingContext.fillStyle = gradient;
  drawingContext.fillRect(0, 0, width, height);

  // --- Stars ---
  let starAlpha = 0;
  if (progress <= 0.25) { // Full night
    starAlpha = 255;
  } else if (progress <= 0.5) { // Dawn fade out
    starAlpha = map(progress, 0.25, 0.5, 255, 0);
  } else if (progress <= 0.75) { // Day - no stars
    starAlpha = 0;
  } else { // Sunset fade in
    starAlpha = map(progress, 0.75, 1, 0, 255);
  }
  for (let star of stars) {
    star.show(starAlpha);
  }

  // --- Clouds ---
  let cloudAlpha = 0;
  if (progress <= 0.25) { // Night - no clouds
    cloudAlpha = 0;
  } else if (progress <= 0.5) { // Dawn fade in
    cloudAlpha = map(progress, 0.25, 0.5, 0, 100);
  } else if (progress <= 0.75) { // Day
    cloudAlpha = 100;
  } else { // Sunset fade out
    cloudAlpha = map(progress, 0.75, 1, 100, 0);
  }
  for (let cloud of clouds) {
    cloud.show(cloudAlpha);
    cloud.move();
  }

  // --- Sun ---
  fill(255, 160, 0);
  drawingContext.shadowBlur = width / 20;
  // Convert p5 color object to CSS string for drawingContext
  drawingContext.shadowColor = color(255, 200, 0, 150).toString();
  circle(width / 2, sunY, width / 10);
  drawingContext.shadowBlur = 0; // Reset shadow blur to avoid affecting other elements

  // --- Birds ---
  for (let bird of birds) {
    let birdAlpha = 0;
    if (progress >= bird.appearsAtProgress && progress <= 0.5) { // Appears at random point in dawn
      birdAlpha = map(progress, bird.appearsAtProgress, 0.5, 0, 255);
    } else if (progress > 0.5 && progress <= bird.disappearsAtProgress) { // Day
      birdAlpha = 255;
    } else if (progress > bird.disappearsAtProgress && progress <= 1) { // Disappears at random point in sunset
      birdAlpha = map(progress, bird.disappearsAtProgress, 1, 255, 0);
    }
    bird.show(birdAlpha);
    bird.move();
  }

  // --- Ground Layers (more organic with curveVertex) ---
  noStroke();

  // Darkest layer
  fill(0);
  beginShape();
  vertex(0, height);
  // Using curveVertex for a more natural, rolling ground
  curveVertex(0, height); // Control point for smooth start
  curveVertex(width * 0.15, height * 0.85);
  curveVertex(width * 0.35, height * 0.7);
  curveVertex(width * 0.6, height * 0.9);
  curveVertex(width * 0.8, height * 0.75);
  vertex(width, height * 0.95); // Using vertex at edges to close shape correctly with curveVertex
  vertex(width, height);
  vertex(0, height); // Close shape by returning to start
  endShape(CLOSE);

  // Slightly lighter layer
  fill(20);
  beginShape();
  vertex(0, height);
  curveVertex(0, height); // Control point for smooth start
  curveVertex(width * 0.1, height * 0.9);
  curveVertex(width * 0.3, height * 0.8);
  curveVertex(width * 0.5, height * 0.95);
  curveVertex(width * 0.7, height * 0.8);
  curveVertex(width * 0.9, height * 0.9);
  vertex(width, height);
  vertex(0, height); // Close shape
  endShape(CLOSE);

  // Even lighter layer
  fill(40);
  beginShape();
  vertex(0, height);
  curveVertex(0, height); // Control point for smooth start
  curveVertex(width * 0.2, height * 0.95);
  curveVertex(width * 0.4, height * 0.88);
  curveVertex(width * 0.7, height * 0.98);
  vertex(width, height * 0.9);
  vertex(width, height);
  vertex(0, height); // Close shape
  endShape(CLOSE);

  // Reset frameCount after one full cycle to loop the animation
  if (progress >= 1) {
    frameCount = 0;
  }
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

calculation Progress value progress = constrain(frameCount / 1200, 0, 1);

Converts frame count into a normalized 0–1 value that drives the entire animation timeline

switch-case Sky color transitions if (progress <= 0.25) { ... } else if (progress <= 0.5) { ... }

Selects which pair of colors to interpolate based on the current phase of the day

conditional Star visibility if (progress <= 0.25) { starAlpha = 255; }

Determines the opacity of stars based on time of day

conditional Cloud visibility if (progress <= 0.25) { cloudAlpha = 0; }

Determines the opacity of clouds based on time of day

conditional Bird visibility if (progress >= bird.appearsAtProgress && progress <= 0.5) {

Determines each bird's individual opacity based on its unique appearance and disappearance timings

shape-drawing Ground silhouette beginShape(); vertex(0, height); curveVertex(0, height);

Draws three layered ground shapes using curveVertex for organic rolling hills

progress = constrain(frameCount / 1200, 0, 1);
Divides the current frame number by 1200 to get a 0–1 value over 40 seconds (at 30 fps); constrain() clamps it so it never exceeds 1
sunY = lerp(height, height / 4, progress);
Interpolates the sun's Y position from the bottom of the canvas (height) to one-quarter height as progress increases
if (progress <= 0.25) {
Checks if we're in the night-to-dawn phase (first 25% of the animation)
let p = map(progress, 0, 0.25, 0, 1);
Remaps the current progress (0–0.25) to a new 0–1 range so we can use it to interpolate colors smoothly within this phase
topC = lerpColor(skyColors.nightTop, skyColors.dawnTop, p);
Smoothly blends between the night-top color and the dawn-top color based on the remapped phase progress
let gradient = drawingContext.createLinearGradient(0, 0, 0, height);
Uses the canvas's native drawing context to create a gradient that runs vertically from top (0) to bottom (height)
gradient.addColorStop(0, topC.toString());
Adds the top color to the gradient at position 0 (the top of the canvas)
gradient.addColorStop(1, bottomC.toString());
Adds the bottom color to the gradient at position 1 (the bottom of the canvas)
drawingContext.fillRect(0, 0, width, height);
Fills the entire canvas with the gradient, creating the sky background
for (let star of stars) {
Loops through all stars and calls their show() method to draw them with the current alpha value
for (let cloud of clouds) {
Loops through all clouds, draws each one with the current alpha value, and calls move() to drift them horizontally
fill(255, 160, 0);
Sets the fill color to orange for the sun
drawingContext.shadowBlur = width / 20;
Enables a soft shadow (glow) around the sun; the blur amount scales with canvas width
drawingContext.shadowColor = color(255, 200, 0, 150).toString();
Sets the shadow color to a soft yellow with some transparency to create the sun's glow
circle(width / 2, sunY, width / 10);
Draws the sun at the center of the canvas horizontally, at the Y position controlled by progress, with a diameter of 10% of the canvas width
drawingContext.shadowBlur = 0;
Resets the shadow blur so subsequent shapes don't have unwanted glows
if (progress >= bird.appearsAtProgress && progress <= 0.5) {
Checks if we're within this bird's appearance window (which is unique to each bird)
birdAlpha = map(progress, bird.appearsAtProgress, 0.5, 0, 255);
Fades the bird in from transparent (0) to fully opaque (255) as progress moves from the bird's appearance point to the end of dawn (0.5)
curveVertex(width * 0.15, height * 0.85);
Places a control point for the curve at 15% across and 85% down; curveVertex creates smooth spline curves through all these points
if (progress >= 1) {
Checks if the animation has completed a full cycle
frameCount = 0;
Resets the frame counter to 0 so the animation loops seamlessly from the start

windowResized()

windowResized() is a p5.js built-in function that is called automatically whenever the user resizes the browser window. It's essential for full-screen sketches that should adapt to different screen sizes. By calling setup() again, you reinitialize all the arrays with new random positions and sizes that fit the new canvas dimensions. Without this function, the sketch would continue using the old canvas size and might draw off-screen or have elements positioned incorrectly.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Calling setup() again handles all re-initialization of dynamic elements
  // and skyColors for the new canvas size.
  setup();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new browser window dimensions when the user resizes their screen
setup();
Re-runs the setup() function to reinitialize all arrays and variables so objects scale properly to the new canvas size

📦 Key Variables

progress number

A value from 0 to 1 that represents how far through the day-night cycle the animation is; controls all color transitions and object visibility timings

let progress = 0;
sunY number

The vertical position of the sun on the canvas, lerped from the bottom to the top as progress increases

let sunY = 0;
skyColors object

Stores four color pairs (day, dusk, noon, sunset) so the code can easily access and interpolate between sky colors throughout the day

let skyColors = { nightTop: color(20, 0, 50), ... };
stars array

An array of Star objects; each star has random position, size, and twinkling phase

let stars = [];
clouds array

An array of Cloud objects; each cloud has random position, size, and horizontal drift speed

let clouds = [];
birds array

An array of Bird objects; each bird has random position, speed, and unique appearance/disappearance times

let birds = [];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - star and cloud loops

Stars and clouds continue to be updated (calling show() and move()) even when they are completely transparent, wasting CPU cycles on invisible objects

💡 Add an early return in the Star and Cloud show() methods if alpha <= 0 (already done), and consider not calling bird.move() or cloud.move() when they're fully transparent, as the position won't be seen anyway

BUG Bird class - wingAngle variable

The wingAngle variable is calculated but never used to rotate the wing shapes, so the wings don't actually flap despite the variable being computed each frame

💡 Use rotate(wingAngle) after the translate() and scale() calls to make the wings actually animate based on the computed angle

STYLE draw() - sky color phase logic

The four if-else blocks that determine topC and bottomC colors are repetitive and difficult to maintain; changing the phase breakpoints requires updating multiple lines

💡 Consider refactoring into a helper function that takes phase boundaries and returns interpolated colors, or using an array of phase objects to reduce duplication

FEATURE Global scope

The sketch does not respond to mouse interaction; the description mentions controlling the sun's position with the mouse, but this feature is not implemented

💡 Add code to map mouseX to the progress value so the user can drag the sun across the sky manually—something like: if (mouseIsPressed) { progress = constrain(mouseX / width, 0, 1); }

BUG draw() - bird alpha calculation

The bird visibility logic hardcodes progress values (0.5, bird.disappearsAtProgress) which may not match the actual appear/disappear times stored in each bird, leading to inconsistent fade transitions

💡 Use bird.appearsAtProgress consistently in the first condition, and ensure the fade-out transition starts exactly when bird.disappearsAtProgress is reached

PERFORMANCE setup() and windowResized()

Calling setup() on every window resize recreates all 200 stars, 5 clouds, and 10 birds even though the canvas is already filled with visible objects; this causes a visual stutter and wasted memory

💡 Only reinitialize objects whose sizes or positions depend directly on canvas dimensions (like cloud and bird positions). Consider storing the old canvas width/height and only recalculating positions if the ratio changed significantly

🔄 Code Flow

Code flow showing star, cloud, bird, setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> stars-loop[Star Creation Loop] setup --> clouds-loop[Cloud Creation Loop] setup --> birds-loop[Bird Creation Loop] setup --> skycolors-init[Sky Color Palette Initialization] setup --> draw[draw loop] click setup href "#fn-setup" click stars-loop href "#sub-stars-loop" click clouds-loop href "#sub-clouds-loop" click birds-loop href "#sub-birds-loop" click skycolors-init href "#sub-skycolors-init" draw --> progress-calc[Progress Value Calculation] draw --> sky-color-phases[Sky Color Phases] draw --> star-alpha-phase[Star Visibility Phase] draw --> cloud-alpha-phase[Cloud Visibility Phase] draw --> bird-alpha-phase[Bird Visibility Phase] draw --> ground-layers[Ground Layers Drawing] draw --> star[Star Loop] draw --> cloud[Cloud Loop] draw --> bird[Bird Loop] click draw href "#fn-draw" click progress-calc href "#sub-progress-calc" click sky-color-phases href "#sub-sky-color-phases" click star-alpha-phase href "#sub-star-alpha-phase" click cloud-alpha-phase href "#sub-cloud-alpha-phase" click bird-alpha-phase href "#sub-bird-alpha-phase" click ground-layers href "#sub-ground-layers" star --> star-constructor[Star Constructor] star --> twinkle-calculation[Twinkle Calculation] click star href "#fn-star" click star-constructor href "#sub-star-constructor" click twinkle-calculation href "#sub-twinkle-calculation" cloud --> cloud-ellipse-loop[Cloud Ellipse Loop] cloud --> cloud-wrapping[Cloud Off-Screen Wrapping] click cloud href "#fn-cloud" click cloud-ellipse-loop href "#sub-cloud-ellipse-loop" click cloud-wrapping href "#sub-cloud-wrapping" bird --> bird-wing-animation[Bird Wing Animation] bird --> bird-shape-drawing[Bird Shape Drawing] click bird href "#fn-bird" click bird-wing-animation href "#sub-bird-wing-animation" click bird-shape-drawing href "#sub-bird-shape-drawing" windowresized --> setup click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual experience does the sunrise sketch provide?

The sketch creates a serene transition from a starry night to a bright daytime scene, featuring drifting clouds and birds gliding across the horizon.

How can users interact with the sunrise sketch?

Users can interact by moving their mouse horizontally, which controls the sun's journey and the pace of the day-night transition.

What creative coding concepts are showcased in the sunrise sketch?

The sketch demonstrates concepts like object-oriented programming with classes for stars, clouds, and birds, as well as dynamic visual effects through animation and user interaction.

Preview

sunrise - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of sunrise - Code flow showing star, cloud, bird, setup, draw, windowresized
Code Flow Diagram