CitySkyline

This sketch creates a mesmerizing day-night cycle of a city skyline that continuously transforms over 60 seconds. Stars twinkle at night, clouds drift across the sky, birds fly at dawn, shooting stars streak overhead, and glowing lanterns rise from rooftops at dusk—all set against a smoothly gradient-shifting sky that transitions from deep night through sunrise, midday blue, sunset, and back again.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up or slow down the day-night cycle — Change cycleDuration to make the entire day-night cycle faster or slower—lower values speed up time, higher values slow it down
  2. Fill the night sky with more or fewer stars — Increase or decrease the number of twinkling stars visible at night by changing the numStars constant
  3. Make the sun and moon much larger — Increase sunMoonSize to make the sun and moon dominate the sky more dramatically
  4. Keep windows lit all the time — Modify the Building.update() method to skip its time-based logic and always keep windows on, creating a different atmosphere
  5. Change lantern colors to bright red
  6. Make clouds move faster — Increase cloud speed by adjusting the range in the Cloud constructor
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a living city scene that breathes through a continuous 60-second day-night cycle. The visual magic comes from smooth sky gradients that shift through deep night purple, dawn pinks, bright midday blue, warm sunsets, and back to night—layered with stars that flicker, clouds that drift, birds that fly in formation at sunrise, shooting stars that streak across the night sky, and glowing lanterns that rise from rooftops at dusk. The effect is deeply atmospheric and meditative, perfect for understanding how state management and color interpolation can create emotional, time-driven animations.

The code is organized around a central dayTime variable (0 to 1) that drives nearly every visual change in the sketch. You'll learn how to use an array of color 'waypoints' to smoothly interpolate between colors over time, how to conditionally show and hide objects based on time ranges, how classes can encapsulate animated entities like buildings with flickering windows, clouds, birds, and lanterns, and how off-screen buffering with createGraphics can optimize expensive gradient rendering. This is a masterclass in state-driven animation.

⚙️ How It Works

  1. When the sketch loads, setup() initializes the canvas, creates a sky gradient buffer for optimization, and generates five arrays: buildings with procedurally-placed windows, drifting clouds, birds waiting in formation, shooting stars, and static stars scattered across the night sky.
  2. Every frame, draw() calculates the current dayTime (0 to 1) based on elapsed milliseconds since the cycle started, creating a loop that repeats every 60 seconds.
  3. The sky gradient is re-rendered to a cached buffer only when dayTime changes significantly (every 0.005 units), which is far more efficient than redrawing the gradient 60 times per second—the buffer is then displayed immediately.
  4. Based on the current dayTime, different visual elements are shown or hidden: stars and their flicker brighten during night (dayTime < 0.25 or > 0.85), clouds appear during day/dusk/dawn, birds fly only during sunrise (0.3 to 0.45), and shooting stars streak during night.
  5. Building windows update their on/off state based on time—they flicker and gradually turn on during night and dusk, gradually turn off at dawn, and stay off all day. Each window has its own state and flicker timing.
  6. The sun and moon follow an arc across the screen using sine waves, appearing as a yellow sun during day and a blue moon during night, with a glowing halo created using additive blending.
  7. During the dusk phase (dayTime between 0.8 and 0.95), lanterns are spawned once per cycle, floating upward from random rooftops while fading and shrinking, with light pools beneath them that glow and fade faster.

🎓 Concepts You'll Learn

Time-based state managementColor interpolation and gradientsConditional rendering based on time rangesObject-oriented design with classesGraphics buffer optimizationPerlin noise for organic motionBlending modes for glow effectsWindow flickering and flicker delaysSpawn conditions and cycle flags

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It's where you initialize all the 'infrastructure' your animation needs: canvas size, global variables, and data structures. The order matters—for instance, we record cycleStartTime before any draw() frame runs, so that the first frame will have dayTime = 0.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke(); // Most shapes will not have a border

  sunColor = color(255, 200, 0); // Yellow for the sun
  moonColor = color(200, 200, 255); // Light blue/white for the moon

  // Initialize the cycle timer
  cycleStartTime = millis();

  // Create the createGraphics buffer for the sky
  skyBuffer = createGraphics(width, height);
  skyBuffer.noStroke(); // Ensure no stroke is drawn in the buffer for the gradient

  // Generate buildings
  generateCity();

  // Generate clouds
  generateClouds();

  // Generate birds (will only appear at dawn)
  generateBirds();

  // Generate shooting stars (will only appear at night)
  generateShootingStars();

  // Generate stars for the night sky
  generateStars();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

variable-assignment Sun and Moon Colors sunColor = color(255, 200, 0); // Yellow for the sun moonColor = color(200, 200, 255); // Light blue/white for the moon

Pre-define the sun's yellow and moon's pale blue colors so they don't need to be created each frame

calculation Sky Buffer Initialization skyBuffer = createGraphics(width, height); skyBuffer.noStroke();

Create an off-screen graphics buffer to cache expensive sky gradient rendering for performance

function-call Scene Generation generateCity(); generateClouds(); generateBirds(); generateShootingStars(); generateStars();

Populate all the animated objects (buildings, clouds, birds, stars) before the first draw

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window; windowWidth and windowHeight update if the window is resized
noStroke();
Turns off outlines for all shapes drawn after this point—cleaner, simpler visuals
sunColor = color(255, 200, 0);
Defines the sun's warm yellow color (red=255, green=200, blue=0) as a global variable so it's ready to use in draw()
moonColor = color(200, 200, 255);
Defines the moon's pale blue color as a global variable
cycleStartTime = millis();
Records the current time in milliseconds when the sketch starts; later, (millis() - cycleStartTime) will measure elapsed time
skyBuffer = createGraphics(width, height);
Creates an invisible canvas the same size as the main canvas; we'll draw the expensive gradient to this once and reuse it
skyBuffer.noStroke();
Ensures the buffer also has no strokes when drawing the gradient
generateCity();
Calls generateCity() to create the buildings array with random widths and heights, then sorts them by depth
generateClouds();
Populates the clouds array with 10 drifting clouds at random positions and sizes
generateBirds();
Creates a formation of 10 birds in a V-shape flying pattern off-screen
generateShootingStars();
Adds 3 initial shooting stars to the array; more will be spawned dynamically during the night
generateStars();
Scatters 200 static stars randomly across the upper 40% of the sky for the night background

draw()

draw() runs ~60 times per second and contains the heart of the animation. The dayTime variable (0-1 looping) is the master control—almost every visual change depends on it. Notice how we use many time-based conditionals (if dayTime > ... && dayTime < ...) to show different objects at different times. This is the core pattern for creating day-night cycles and time-driven scenes. The buffer optimization is also crucial for performance: expensive gradients are cached and only re-rendered when dayTime changes significantly.

🔬 This code fades stars in and out at night. What happens if you swap the last two numbers in each map() call—e.g., map(dayTime, 0, 0.25, 0, 1) instead of map(dayTime, 0, 0.25, 1, 0)? Will stars brighten or dim?

      if (dayTime < 0.25) { // Early night (0 to 0.25)
        deepNightFactor = map(dayTime, 0, 0.25, 1, 0); // 1 at midnight, 0 at 0.25
      } else { // Late night (0.85 to 1)
        deepNightFactor = map(dayTime, 0.85, 1, 0, 1); // 0 at 0.85, 1 at midnight
      }

🔬 This conditional shows clouds only between dayTime 0.2 and 0.9. What happens if you change it to 0.3 and 0.8? When do clouds disappear?

  // Draw clouds and their shadows (only during day/dusk/dawn)
  if (dayTime > 0.2 && dayTime < 0.9) {
    clouds.forEach(cloud => {
      cloud.update();
      cloud.display();
    });
  }
function draw() {
  // Update dayTime (0 to 1 over 60 seconds)
  dayTime = ((millis() - cycleStartTime) % cycleDuration) / cycleDuration;

  // Render sky gradient to buffer only if dayTime has changed significantly
  if (abs(dayTime - lastRenderedDayTime) > 0.005) {
    renderSkyToBuffer(dayTime, skyBuffer);
    lastRenderedDayTime = dayTime;
  }

  // Display the cached sky gradient
  image(skyBuffer, 0, 0);

  // Draw stars during night
  if (dayTime < 0.25 || dayTime > 0.85) {
    push();
    noStroke();
    for (let i = 0; i < stars.length; i++) {
      let star = stars[i];
      let deepNightFactor; // How deep into night are we (0 to 1)

      if (dayTime < 0.25) { // Early night (0 to 0.25)
        deepNightFactor = map(dayTime, 0, 0.25, 1, 0); // 1 at midnight, 0 at 0.25
      } else { // Late night (0.85 to 1)
        deepNightFactor = map(dayTime, 0.85, 1, 0, 1); // 0 at 0.85, 1 at midnight
      }

      // Add a subtle flicker to the alpha
      let alpha = star.brightness * deepNightFactor * (0.8 + 0.2 * sin(frameCount * 0.1 + star.flickerOffset));
      alpha = constrain(alpha, 0, 255); // Ensure alpha is within 0-255

      fill(255, alpha);
      circle(star.x, star.y, 1.5); // Slightly larger dot for visibility
    }
    pop();
  }

  // Draw Sun or Moon
  drawSunMoon(dayTime);

  // --- Lantern Spawning Logic ---
  if (dayTime > 0.8 && dayTime < 0.95 && !hasSpawnedLanternsThisCycle) {
    hasSpawnedLanternsThisCycle = true;
    generateLanterns();
  } else if (dayTime < 0.8 || dayTime > 0.95) {
    hasSpawnedLanternsThisCycle = false; // Reset flag for next cycle
    // Optionally clear lanterns if they're not needed after this phase
    // lanterns = [];
  }

  // Update lanterns (and their internal light pools)
  for (let i = lanterns.length - 1; i >= 0; i--) {
    lanterns[i].update();
    // Remove lantern if it's off-screen or completely faded out
    if (lanterns[i].y < -50 || lanterns[i].alpha <= 0) {
      lanterns.splice(i, 1);
    }
  }

  // --- DRAWING ORDER: Light Pools (on rooftops, before clouds/buildings) ---
  lanterns.forEach(lantern => lantern.lightPool.display());


  // Draw clouds and their shadows (only during day/dusk/dawn)
  if (dayTime > 0.2 && dayTime < 0.9) {
    clouds.forEach(cloud => {
      cloud.update();
      cloud.display();
    });}

  // Draw building shadows (daytime only)
  if (dayTime > 0.4 && dayTime < 0.75) {
    buildings.forEach(building => building.displayShadow());
  }

  // Draw buildings and their windows
  buildings.forEach(building => {
    building.update(dayTime);
    building.display();
  });

  // Draw birds (only at dawn)
  if (dayTime > 0.3 && dayTime < 0.45) { // Visible during sunrise
    birds.forEach(bird => {
      bird.update();
      bird.display();
    });
  }

  // Draw shooting stars (only at night)
  if (dayTime < 0.25 || dayTime > 0.9) { // Visible during night
    shootingStars.forEach(star => {
      star.update();
      star.display();
    });
  }

  // --- DRAWING ORDER: Floating Lanterns themselves (after everything else) ---
  lanterns.forEach(lantern => lantern.display());
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

calculation Day Time Calculation dayTime = ((millis() - cycleStartTime) % cycleDuration) / cycleDuration;

Converts elapsed milliseconds into a normalized 0-to-1 value that loops every 60 seconds

conditional Sky Buffer Optimization if (abs(dayTime - lastRenderedDayTime) > 0.005) { renderSkyToBuffer(dayTime, skyBuffer); lastRenderedDayTime = dayTime; }

Only re-renders the expensive gradient when dayTime changes by 0.005 or more, avoiding wasteful recalculation

for-loop Star Flickering for (let i = 0; i < stars.length; i++) { let star = stars[i]; let deepNightFactor; if (dayTime < 0.25) { deepNightFactor = map(dayTime, 0, 0.25, 1, 0); } else { deepNightFactor = map(dayTime, 0.85, 1, 0, 1); } let alpha = star.brightness * deepNightFactor * (0.8 + 0.2 * sin(frameCount * 0.1 + star.flickerOffset)); alpha = constrain(alpha, 0, 255); fill(255, alpha); circle(star.x, star.y, 1.5); }

Draws all stars with brightness that fades based on time of night, and flickers using sine waves

conditional Lantern Spawning Logic if (dayTime > 0.8 && dayTime < 0.95 && !hasSpawnedLanternsThisCycle) { hasSpawnedLanternsThisCycle = true; generateLanterns(); } else if (dayTime < 0.8 || dayTime > 0.95) { hasSpawnedLanternsThisCycle = false; }

Ensures lanterns spawn exactly once per cycle during the dusk window, using a flag to prevent multiple spawns

conditional Conditional Element Rendering if (dayTime > 0.3 && dayTime < 0.45) { birds.forEach(bird => { bird.update(); bird.display(); }); } if (dayTime < 0.25 || dayTime > 0.9) { shootingStars.forEach(star => { star.update(); star.display(); }); }

Shows birds only during sunrise and shooting stars only during night, creating time-of-day-specific visuals

dayTime = ((millis() - cycleStartTime) % cycleDuration) / cycleDuration;
Converts elapsed time into a 0-to-1 value that repeats every 60 seconds: (millis() - cycleStartTime) is elapsed milliseconds, % cycleDuration wraps it, and / cycleDuration normalizes it to 0-1
if (abs(dayTime - lastRenderedDayTime) > 0.005) {
Only re-render the gradient if dayTime has changed by more than 0.005—this avoids recalculating the expensive gradient 60 times per second
renderSkyToBuffer(dayTime, skyBuffer);
Calls the function that draws the full gradient into the off-screen buffer
lastRenderedDayTime = dayTime;
Updates the tracking variable so we only re-render when dayTime changes significantly next time
image(skyBuffer, 0, 0);
Displays the cached sky gradient buffer at the canvas origin—this is much faster than redrawing the gradient each frame
if (dayTime < 0.25 || dayTime > 0.85) {
Stars only render during night: early night (0 to 0.25) and late night (0.85 to 1)
deepNightFactor = map(dayTime, 0, 0.25, 1, 0);
Maps dayTime from 0-0.25 to 1-0, so stars fade in as we approach midnight (deepNightFactor = 1) and fade out as dawn approaches
let alpha = star.brightness * deepNightFactor * (0.8 + 0.2 * sin(frameCount * 0.1 + star.flickerOffset));
Combines three factors: the star's brightness, how deep into night we are, and a sine-wave flicker that oscillates between 0.8 and 1.0
alpha = constrain(alpha, 0, 255);
Clamps alpha to 0-255 to ensure it's a valid opacity value for fill()
circle(star.x, star.y, 1.5);
Draws the star as a small white circle (diameter 1.5 pixels) with the calculated alpha transparency
if (dayTime > 0.8 && dayTime < 0.95 && !hasSpawnedLanternsThisCycle) {
Spawn lanterns once during dusk (0.8-0.95) if the flag hasSpawnedLanternsThisCycle is false
hasSpawnedLanternsThisCycle = true;
Sets the flag to true so lanterns won't spawn again until dayTime exits the 0.8-0.95 window
generateLanterns();
Creates 20-30 lanterns at random positions on building rooftops
} else if (dayTime < 0.8 || dayTime > 0.95) {
If we're outside the spawn window, reset the flag for the next cycle
for (let i = lanterns.length - 1; i >= 0; i--) {
Loop through lanterns backwards so we can safely remove them with splice() without skipping any
lanterns[i].update();
Updates each lantern's position (rising, wobbling) and fading alpha
if (lanterns[i].y < -50 || lanterns[i].alpha <= 0) {
Checks if the lantern is off-screen above the canvas or fully invisible
lanterns.splice(i, 1);
Removes the lantern from the array to save memory and stop updating it
if (dayTime > 0.2 && dayTime < 0.9) {
Clouds only render during day/dusk/dawn, not during night
if (dayTime > 0.4 && dayTime < 0.75) {
Building shadows only render during daytime when the sun is high, not at dawn or dusk
building.update(dayTime);
Updates each building's windows based on the current time of day—they turn on/off and flicker differently at night vs. day
if (dayTime > 0.3 && dayTime < 0.45) {
Birds only render during sunrise, creating a fleeting, atmospheric moment
if (dayTime < 0.25 || dayTime > 0.9) {
Shooting stars only render during night (early and late night)
lanterns.forEach(lantern => lantern.display());
Final drawing pass for lanterns after everything else, so they appear on top of clouds and buildings

renderSkyToBuffer()

This function is called only when dayTime changes significantly, not every frame. It draws the entire sky gradient into an off-screen buffer (like pre-rendering) so that draw() can just display the cached image. This is a crucial optimization: gradient rendering is expensive (200-300 horizontal lines per render), so caching it in a buffer saves huge amounts of computation. The function loops through every pixel row and calls getSkyColor() to determine the color at that height and time of day, then draws a line in that color.

🔬 This loop draws horizontal lines to create a vertical gradient. What if you swapped 'i' and 'buffer.width' in the line() call to make vertical lines instead—would the gradient run left-to-right or stay vertical?

  for (let i = 0; i < buffer.height; i++) {
    let inter = map(i, 0, buffer.height, 0, 1);
    let c1 = getSkyColor(time, inter);
    let c2 = getSkyColor(time, inter + 0.01);
    let lerpedColor = lerpColor(c1, c2, inter % 0.01 * 100);
    buffer.stroke(lerpedColor);
    buffer.line(0, i, buffer.width, i);
  }
function renderSkyToBuffer(time, buffer) {
  buffer.clear(); // Clear the buffer before drawing
  buffer.noStroke(); // Ensure no stroke for the main sky gradient lines

  // Draw the sky gradient using the buffer's context
  for (let i = 0; i < buffer.height; i++) {
    let inter = map(i, 0, buffer.height, 0, 1);
    let c1 = getSkyColor(time, inter);
    let c2 = getSkyColor(time, inter + 0.01);
    let lerpedColor = lerpColor(c1, c2, inter % 0.01 * 100);
    buffer.stroke(lerpedColor);
    buffer.line(0, i, buffer.width, i);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Buffer Clear buffer.clear();

Removes all previous drawings from the buffer so we start with a blank slate

for-loop Horizontal Line Gradient for (let i = 0; i < buffer.height; i++) { let inter = map(i, 0, buffer.height, 0, 1); let c1 = getSkyColor(time, inter); let c2 = getSkyColor(time, inter + 0.01); let lerpedColor = lerpColor(c1, c2, inter % 0.01 * 100); buffer.stroke(lerpedColor); buffer.line(0, i, buffer.width, i); }

Draws horizontal lines across the entire width, each with a slightly different color to create a smooth vertical gradient

buffer.clear();
Erases all pixels in the buffer, giving us a blank canvas to draw the new gradient
buffer.noStroke();
Disables stroke drawing on the buffer (though the loop will override this with buffer.stroke() for each line)
for (let i = 0; i < buffer.height; i++) {
Loops through every pixel row from top (i=0) to bottom (i=buffer.height), drawing one horizontal line per row
let inter = map(i, 0, buffer.height, 0, 1);
Converts the row index i (0 to buffer.height) into a normalized value inter (0 to 1) representing vertical position
let c1 = getSkyColor(time, inter);
Calls getSkyColor() to get the color at this vertical position (inter) for the current time of day
let c2 = getSkyColor(time, inter + 0.01);
Gets the color slightly lower on the screen (inter + 0.01), creating a smooth transition between rows
let lerpedColor = lerpColor(c1, c2, inter % 0.01 * 100);
Blends c1 and c2 together to create a smooth interpolation—this line is complex but essentially smooths the gradient further
buffer.stroke(lerpedColor);
Sets the stroke color to the lerped color for the next line
buffer.line(0, i, buffer.width, i);
Draws a horizontal line from the left edge (0) to the right edge (buffer.width) at pixel row i, using the stroke color we just set

getSkyColor()

This function is the workhorse of the sky gradient. It takes a time (0-1) representing the current point in the day-night cycle, plus a verticalInter (0-1) representing vertical position on the screen. It returns a single color. By calling this function for every pixel row in renderSkyToBuffer(), we build a complete gradient. The skyPalette array is the 'color map'—it defines what color the sky should be at each hour. The function finds which two palette colors the current time falls between, blends them together, and then applies a vertical gradient (darker at bottom) for depth. This is how the smooth, multi-layered sky effect is achieved.

function getSkyColor(time, verticalInter) {
  // Find the two palette entries that dayTime falls between
  let c1 = skyPalette[0];
  let c2 = skyPalette[skyPalette.length - 1];

  for (let i = 0; i < skyPalette.length - 1; i++) {
    if (time >= skyPalette[i].time && time <= skyPalette[i + 1].time) {
      c1 = skyPalette[i];
      c2 = skyPalette[i + 1];
      break;
    }
  }

  let interTime = map(time, c1.time, c2.time, 0, 1);
  let baseColor = lerpColor(color(c1.color), color(c2.color), interTime);

  // Apply a simple vertical darkening gradient (darker at bottom)
  let darkColor = lerpColor(baseColor, color(0), 0.5); // Darken base color by 50%
  return lerpColor(baseColor, darkColor, verticalInter);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Color Interpolation let interTime = map(time, c1.time, c2.time, 0, 1); let baseColor = lerpColor(color(c1.color), color(c2.color), interTime);

Blends the two palette colors together smoothly based on how far 'time' is between them

calculation Vertical Darkening let darkColor = lerpColor(baseColor, color(0), 0.5); return lerpColor(baseColor, darkColor, verticalInter);

Darkens the color as we move down the screen (higher verticalInter), creating depth

let c1 = skyPalette[0];
Sets a default starting color (deep night) so we have a fallback if time is outside all palette ranges
let c2 = skyPalette[skyPalette.length - 1];
Sets a default ending color (also deep night) as the fallback
for (let i = 0; i < skyPalette.length - 1; i++) {
Loops through the skyPalette array (which has 15 time/color waypoints) to find the two that sandwich the current time
if (time >= skyPalette[i].time && time <= skyPalette[i + 1].time) {
Checks if the current time falls between palette entry i and palette entry i+1
c1 = skyPalette[i]; c2 = skyPalette[i + 1]; break;
If we found the matching pair, store them in c1 and c2 and exit the loop
let interTime = map(time, c1.time, c2.time, 0, 1);
Converts the current time into a 0-1 blend factor between c1 and c2—0 means use c1, 1 means use c2, 0.5 means 50/50 blend
let baseColor = lerpColor(color(c1.color), color(c2.color), interTime);
Blends the two palette colors together using the blend factor, creating a smooth transition between them
let darkColor = lerpColor(baseColor, color(0), 0.5);
Creates a darker version of baseColor by blending it 50% toward black
return lerpColor(baseColor, darkColor, verticalInter);
Blends baseColor (bright) and darkColor (dark) based on vertical position: bright at top (verticalInter=0), dark at bottom (verticalInter=1)

drawSunMoon()

This function draws either a yellow sun or pale blue moon depending on the time of day, and positions it along an arc path using trigonometry. The sine wave creates a natural-looking parabolic arc (common in astronomy), and the size variation near the horizon mimics the optical illusion we experience in real life. The two circles (main body + glow halo) with additive blending create a convincing glow effect. Notice how the draw order in draw() ensures the sun/moon is drawn early so clouds and lanterns appear in front of it.

🔬 These two lines create the sun/moon's path: the first maps a sine wave to vertical position (the arc), the second maps time to horizontal position. What if you swap the map ranges for yPos—e.g., map(..., height * 0.2, height * 1.2) instead? Will the arc flip upside down?

  yPos = map(sin(time * TWO_PI - HALF_PI), -1, 1, height * 1.2, height * 0.2);
  xPos = map(time, 0, 1, -sunMoonSize, width + sunMoonSize);
function drawSunMoon(time) {
  let xPos, yPos, currentSize, currentColor;

  // Path of the sun/moon (an arc)
  // At time 0.0 (midnight) and 1.0 (midnight), it's below the horizon
  // At time 0.5 (midday), it's at its highest point

  // Using a sine wave for vertical position to create an arc
  // Adjusting the phase and amplitude to fit the day-night cycle
  // yPos is lowest (highest in canvas) at midnight, highest (lowest in canvas) at midday
  yPos = map(sin(time * TWO_PI - HALF_PI), -1, 1, height * 1.2, height * 0.2);
  xPos = map(time, 0, 1, -sunMoonSize, width + sunMoonSize); // Moves across the screen

  // Size variation (slightly larger when low on the horizon, like atmospheric distortion)
  let yRatio = map(yPos, height * 0.2, height * 1.2, 0, 1); // 0 at peak, 1 at lowest point
  currentSize = sunMoonSize * lerp(1.2, 0.8, yRatio); // Larger when lower

  push();
  blendMode(ADD); // Additive blending for a glowing effect

  // Determine if it's sun or moon
  if (time > 0.35 && time < 0.75) { // Day (sunrise to sunset)
    currentColor = sunColor;
    fill(currentColor, 200); // Slightly transparent for glow
    circle(xPos, yPos, currentSize);
    fill(currentColor, 100);
    circle(xPos, yPos, currentSize * 1.5); // Larger, fainter glow
  } else { // Night (dusk to dawn)
    currentColor = moonColor;
    fill(currentColor, 180);
    circle(xPos, yPos, currentSize * 0.8); // Moon is slightly smaller
    fill(currentColor, 80);
    circle(xPos, yPos, currentSize * 1.2); // Larger, fainter glow
  }
  pop();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Arc Path Calculation yPos = map(sin(time * TWO_PI - HALF_PI), -1, 1, height * 1.2, height * 0.2); xPos = map(time, 0, 1, -sunMoonSize, width + sunMoonSize);

Creates an arc path using sine wave: the sun/moon rises from bottom-left to top-center and sets at bottom-right

calculation Horizon Size Distortion let yRatio = map(yPos, height * 0.2, height * 1.2, 0, 1); currentSize = sunMoonSize * lerp(1.2, 0.8, yRatio);

Makes the sun/moon appear slightly larger near the horizon (yPos high) and smaller at the peak, mimicking atmospheric perspective

conditional Sun or Moon Selection if (time > 0.35 && time < 0.75) { currentColor = sunColor; fill(currentColor, 200); circle(xPos, yPos, currentSize); fill(currentColor, 100); circle(xPos, yPos, currentSize * 1.5); } else { currentColor = moonColor; fill(currentColor, 180); circle(xPos, yPos, currentSize * 0.8); fill(currentColor, 80); circle(xPos, yPos, currentSize * 1.2); }

Switches between sun (yellow, during day) and moon (pale blue, during night), each with a core circle and a larger glow halo

yPos = map(sin(time * TWO_PI - HALF_PI), -1, 1, height * 1.2, height * 0.2);
Uses sin(time * TWO_PI) to create a wave that oscillates -1 to 1 over the full day. The -HALF_PI phase shift makes it start at the bottom. The map() converts this to y positions from 1.2*height (below screen) to 0.2*height (near top), creating an arc path
xPos = map(time, 0, 1, -sunMoonSize, width + sunMoonSize);
Simply maps time 0-1 to xPos moving from left off-screen to right off-screen, so the sun/moon travels across the full width
let yRatio = map(yPos, height * 0.2, height * 1.2, 0, 1);
Converts y position to a 0-1 ratio: 0 at the peak (height*0.2, high in canvas), 1 at the bottom (height*1.2, low in canvas)
currentSize = sunMoonSize * lerp(1.2, 0.8, yRatio);
Blends from 1.2x size (when yRatio=0, at peak) to 0.8x size (when yRatio=1, at horizon), making the sun/moon look flattened near the horizon
push();
Saves the current drawing settings (blendMode, fill, etc.) so changes only apply to the sun/moon
blendMode(ADD);
Sets additive blending, which makes overlapping bright colors add together instead of replacing—perfect for a glowing effect
if (time > 0.35 && time < 0.75) {
During the day (0.35 to 0.75), draw the sun; otherwise draw the moon
fill(currentColor, 200); circle(xPos, yPos, currentSize);
Draws the main sun/moon body with 200 alpha (slightly transparent) at the calculated arc position
fill(currentColor, 100); circle(xPos, yPos, currentSize * 1.5);
Draws a larger, much fainter glow halo (100 alpha) around the main body, creating the glowing effect
circle(xPos, yPos, currentSize * 0.8);
Moon is drawn 0.8x the size to differentiate it from the sun
pop();
Restores the saved drawing settings, so blendMode(ADD) no longer applies to subsequent drawings

generateCity()

generateCity() procedurally creates a city skyline by placing random-sized rectangles from left to right across the canvas. The key insight is that in p5.js drawing, the order matters—draw earlier = appears behind. By sorting buildings by y position after placing them all, we ensure taller buildings are drawn first (and appear behind) shorter buildings, creating a convincing sense of depth even though it's all 2D. This is a simple but powerful technique in creative coding.

function generateCity() {
  const minBuildingHeight = height * 0.2;
  const maxBuildingHeight = height * 0.8;
  const minBuildingWidth = width * 0.03;
  const maxBuildingWidth = width * 0.1;
  const numBuildings = 30; // More buildings for a dense look

  // Place buildings from left to right
  let currentX = 0;
  while (currentX < width) {
    let buildingWidth = random(minBuildingWidth, maxBuildingWidth);
    let buildingHeight = random(minBuildingHeight, maxBuildingHeight);
    let buildingY = height - buildingHeight;

    buildings.push(new Building(currentX, buildingY, buildingWidth, buildingHeight));

    currentX += buildingWidth * random(0.8, 1.2); // Add some spacing variability
  }

  // Sort buildings by height so taller ones are drawn behind shorter ones
  // This creates a better sense of depth.
  buildings.sort((a, b) => a.y - b.y); // Sort by y position (lower y = taller building)
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

while-loop Building Placement Loop while (currentX < width) { let buildingWidth = random(minBuildingWidth, maxBuildingWidth); let buildingHeight = random(minBuildingHeight, maxBuildingHeight); let buildingY = height - buildingHeight; buildings.push(new Building(currentX, buildingY, buildingWidth, buildingHeight)); currentX += buildingWidth * random(0.8, 1.2); }

Places buildings across the entire canvas width with random sizes and variable spacing

calculation Depth Sort buildings.sort((a, b) => a.y - b.y);

Sorts buildings so taller ones (lower y values) are drawn first, creating proper depth layering

const minBuildingHeight = height * 0.2;
Sets the minimum building height to 20% of the canvas height
const maxBuildingHeight = height * 0.8;
Sets the maximum building height to 80% of the canvas height
const minBuildingWidth = width * 0.03;
Sets the minimum building width to 3% of the canvas width
const maxBuildingWidth = width * 0.1;
Sets the maximum building width to 10% of the canvas width
let currentX = 0;
Starts placing buildings at x = 0 (left edge)
while (currentX < width) {
Continues placing buildings until we reach the right edge of the canvas
let buildingWidth = random(minBuildingWidth, maxBuildingWidth);
Chooses a random width for this building between the min and max
let buildingHeight = random(minBuildingHeight, maxBuildingHeight);
Chooses a random height for this building
let buildingY = height - buildingHeight;
Positions the building's top at y = height - buildingHeight, anchoring the bottom to the bottom of the canvas
buildings.push(new Building(currentX, buildingY, buildingWidth, buildingHeight));
Creates a new Building object and adds it to the buildings array
currentX += buildingWidth * random(0.8, 1.2);
Moves currentX forward by the building width plus some variability (0.8 to 1.2x multiplier), creating irregular spacing
buildings.sort((a, b) => a.y - b.y);
Sorts the buildings array by y position (top to bottom). Taller buildings have lower y values and are drawn first, so shorter buildings appear in front

generateClouds()

generateClouds() creates an array of Cloud objects with random positions and sizes. It's straightforward procedural generation—10 clouds, each with slightly different dimensions, are scattered across the upper part of the canvas. Each Cloud object handles its own movement (drifting left-to-right) and drawing, so this function just needs to instantiate them.

function generateClouds() {
  const numClouds = 10;
  for (let i = 0; i < numClouds; i++) {
    let cloudWidth = random(width * 0.1, width * 0.3);
    let cloudHeight = random(height * 0.05, height * 0.15);
    let cloudX = random(width);
    let cloudY = random(height * 0.05, height * 0.35);
    clouds.push(new Cloud(cloudX, cloudY, cloudWidth, cloudHeight));
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Cloud Generation Loop for (let i = 0; i < numClouds; i++) { let cloudWidth = random(width * 0.1, width * 0.3); let cloudHeight = random(height * 0.05, height * 0.15); let cloudX = random(width); let cloudY = random(height * 0.05, height * 0.35); clouds.push(new Cloud(cloudX, cloudY, cloudWidth, cloudHeight)); }

Creates 10 Cloud objects with random positions and sizes, then adds them to the clouds array

const numClouds = 10;
Defines how many clouds to create
for (let i = 0; i < numClouds; i++) {
Loops 10 times, creating one cloud per iteration
let cloudWidth = random(width * 0.1, width * 0.3);
Chooses a random width between 10% and 30% of the canvas width
let cloudHeight = random(height * 0.05, height * 0.15);
Chooses a random height between 5% and 15% of the canvas height
let cloudX = random(width);
Places the cloud at a random horizontal position anywhere on the canvas
let cloudY = random(height * 0.05, height * 0.35);
Places the cloud at a random vertical position in the upper 35% of the canvas (sky area)
clouds.push(new Cloud(cloudX, cloudY, cloudWidth, cloudHeight));
Creates a new Cloud object with the random position and size, then adds it to the clouds array

generateBirds()

generateBirds() creates a flock in a V-formation, a common pattern in nature and animation. The formation leader position is off-screen to the left, and each bird is positioned relative to that leader using modulo (%) and floor division. All birds move at the same speed so the formation stays intact. The random jitter adds natural imperfection so it doesn't look like a rigid grid.

🔬 This loop creates a V-formation: (i % 3) creates 3 columns. What if you changed it to (i % 5) instead—would you get 5 columns per row instead of 3?

  for (let i = 0; i < numBirds; i++) {
    let offsetX = (i % 3) * formationSpacing * 1.5 + random(-10, 10);
    let offsetY = floor(i / 3) * formationSpacing + random(-10, 10);
    birds.push(new Bird(formationLeaderX + offsetX, formationLeaderY + offsetY, formationSpeed));
  }
function generateBirds() {
  const numBirds = 10;
  let formationLeaderX = -width * 0.2; // Start off-screen left
  let formationLeaderY = random(height * 0.2, height * 0.3);
  let formationSpacing = 30;
  let formationSpeed = random(2, 4);

  for (let i = 0; i < numBirds; i++) {
    let offsetX = (i % 3) * formationSpacing * 1.5 + random(-10, 10);
    let offsetY = floor(i / 3) * formationSpacing + random(-10, 10);
    birds.push(new Bird(formationLeaderX + offsetX, formationLeaderY + offsetY, formationSpeed));
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

variable-assignment Formation Parameters let formationLeaderX = -width * 0.2; let formationLeaderY = random(height * 0.2, height * 0.3); let formationSpacing = 30; let formationSpeed = random(2, 4);

Defines the formation's starting position, spacing, and speed for all birds in the group

for-loop Bird Offset Calculation for (let i = 0; i < numBirds; i++) { let offsetX = (i % 3) * formationSpacing * 1.5 + random(-10, 10); let offsetY = floor(i / 3) * formationSpacing + random(-10, 10); birds.push(new Bird(formationLeaderX + offsetX, formationLeaderY + offsetY, formationSpeed)); }

Creates 10 birds in a V-formation (3 birds per row) with slight randomness for a natural look

const numBirds = 10;
Creates 10 birds total
let formationLeaderX = -width * 0.2;
Starts the formation off-screen to the left (negative x position)
let formationLeaderY = random(height * 0.2, height * 0.3);
Starts the formation at a random height in the upper-middle area of the sky
let formationSpacing = 30;
Sets the spacing between birds in the formation to 30 pixels
let formationSpeed = random(2, 4);
Each bird in the formation flies at the same random speed (2-4 pixels per frame), so they stay together
let offsetX = (i % 3) * formationSpacing * 1.5 + random(-10, 10);
Calculates horizontal offset: (i % 3) creates columns (0, 1, or 2), multiplied by spacing to spread them horizontally, plus random jitter (-10 to 10)
let offsetY = floor(i / 3) * formationSpacing + random(-10, 10);
Calculates vertical offset: floor(i/3) creates rows (0, 1, 2, 3), multiplied by spacing to spread them vertically, plus random jitter
birds.push(new Bird(formationLeaderX + offsetX, formationLeaderY + offsetY, formationSpeed));
Creates a Bird at the formation leader position plus the offset, giving it the formation speed

generateShootingStars()

generateShootingStars() seeds the shootingStars array with 3 initial meteors. The ShootingStar class handles its own lifecycle—when a star goes off-screen, it calls reset() to spawn a new meteor at a random position. This keeps the stream of shooting stars alive during the night without spawning all at once.

function generateShootingStars() {
  // Stars are generated on the fly in draw() for randomness,
  // but we'll have a few initially
  const numInitialStars = 3;
  for (let i = 0; i < numInitialStars; i++) {
    shootingStars.push(new ShootingStar());
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Initial Shooting Stars for (let i = 0; i < numInitialStars; i++) { shootingStars.push(new ShootingStar()); }

Creates 3 ShootingStar objects to seed the array

const numInitialStars = 3;
Starts with 3 shooting stars
for (let i = 0; i < numInitialStars; i++) {
Loops 3 times
shootingStars.push(new ShootingStar());
Creates a new ShootingStar object and adds it to the array

generateStars()

generateStars() creates 200 simple point objects (not class instances) representing the static night sky. Each has a position, brightness, and a flicker offset. Unlike buildings or clouds, stars don't need complex methods—they're just data. During night (in draw()), these data objects are looped through and their flicker brightness is calculated dynamically using sine waves.

function generateStars() {
  stars = []; // Clear existing stars if windowResized is called
  for (let i = 0; i < numStars; i++) {
    stars.push({
      x: random(width),
      y: random(height * 0.4), // Stars in upper 40% of the screen
      brightness: random(100, 255),
      flickerOffset: random(TWO_PI) // For independent flickering
    });
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Star Creation Loop for (let i = 0; i < numStars; i++) { stars.push({ x: random(width), y: random(height * 0.4), brightness: random(100, 255), flickerOffset: random(TWO_PI) }); }

Creates 200 static star objects with random positions, brightness levels, and independent flicker offsets

stars = [];
Clears the existing stars array (in case this is called again, like after a window resize)
for (let i = 0; i < numStars; i++) {
Loops numStars times (200 times)
x: random(width),
Gives the star a random x position across the full width
y: random(height * 0.4),
Gives the star a random y position in the upper 40% of the screen (sky area)
brightness: random(100, 255),
Gives each star a random base brightness value (100-255) so some appear dimmer, some brighter
flickerOffset: random(TWO_PI),
Gives each star a unique phase offset for the flicker sine wave, so stars don't all brighten/dim in sync

generateLanterns()

generateLanterns() spawns a batch of glowing lanterns at dusk, distributed randomly across building rooftops. Each lantern is a Lantern object that handles its own animation (rising, wobbling, fading). This function is called once per cycle during the dusk window (draw() checks the time and calls it when appropriate).

function generateLanterns() {
  const numLanterns = random(20, 30);
  for (let i = 0; i < numLanterns; i++) {
    // Pick a random building top
    if (buildings.length > 0) {
      let randomBuilding = random(buildings);
      let startX = random(randomBuilding.x, randomBuilding.x + randomBuilding.w);
      let startY = randomBuilding.y; // Top of the building
      lanterns.push(new Lantern(startX, startY));
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Lantern Spawning for (let i = 0; i < numLanterns; i++) { if (buildings.length > 0) { let randomBuilding = random(buildings); let startX = random(randomBuilding.x, randomBuilding.x + randomBuilding.w); let startY = randomBuilding.y; lanterns.push(new Lantern(startX, startY)); } }

Creates 20-30 Lantern objects at random positions on top of random buildings

const numLanterns = random(20, 30);
Chooses a random number of lanterns (20-30) to spawn
let randomBuilding = random(buildings);
Picks a random building from the buildings array
let startX = random(randomBuilding.x, randomBuilding.x + randomBuilding.w);
Chooses a random x position along the top of that building (between its left and right edges)
let startY = randomBuilding.y;
Sets the lantern's starting y position to the top of the building
lanterns.push(new Lantern(startX, startY));
Creates a Lantern object at that position and adds it to the lanterns array

windowResized()

windowResized() is a p5.js function that fires automatically when the browser window is resized. This sketch's approach is to completely regenerate the scene—clearing all arrays and calling the generate functions again. This ensures that buildings, clouds, and other objects are sized and positioned appropriately for the new canvas dimensions. Without this, the scene would look stretched or broken after a resize.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  skyBuffer = createGraphics(width, height); // Recreate buffer
  skyBuffer.noStroke(); // Ensure no stroke in the buffer rendering
  lastRenderedDayTime = -1; // Force re-render of sky

  buildings = []; // Clear existing buildings
  clouds = [];    // Clear existing clouds
  birds = [];     // Clear existing birds
  shootingStars = []; // Clear existing stars
  lanterns = []; // Clear existing lanterns
  hasSpawnedLanternsThisCycle = false; // Reset flag

  generateCity();
  generateClouds();
  generateBirds();
  generateShootingStars();
  generateStars(); // Regenerate stars
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Canvas and Buffer Resize resizeCanvas(windowWidth, windowHeight); skyBuffer = createGraphics(width, height); skyBuffer.noStroke(); lastRenderedDayTime = -1;

Resizes the main canvas and recreates the sky buffer to fit the new dimensions, forcing a re-render

variable-assignment Array Clearing buildings = []; clouds = []; birds = []; shootingStars = []; lanterns = []; hasSpawnedLanternsThisCycle = false;

Clears all animated object arrays and resets the lantern spawn flag

function-call Scene Regeneration generateCity(); generateClouds(); generateBirds(); generateShootingStars(); generateStars();

Recreates all objects to fit the new canvas size

resizeCanvas(windowWidth, windowHeight);
p5.js automatically calls this function when the window is resized; it updates the canvas to match the new window dimensions
skyBuffer = createGraphics(width, height);
Recreates the sky gradient buffer to match the new canvas size
skyBuffer.noStroke();
Re-applies noStroke() to the new buffer
lastRenderedDayTime = -1;
Resets the optimization flag so the sky gradient is re-rendered on the next frame (since the buffer size changed)
buildings = [];
Clears the buildings array so old buildings aren't drawn at the wrong positions
clouds = [];
Clears the clouds array
birds = [];
Clears the birds array
shootingStars = [];
Clears the shooting stars array
lanterns = [];
Clears the lanterns array
hasSpawnedLanternsThisCycle = false;
Resets the spawn flag so lanterns will spawn fresh in the new window
generateCity(); generateClouds(); generateBirds(); generateShootingStars(); generateStars();
Regenerates all objects with sizes and positions appropriate for the new canvas size

Building (class)

The Building class is the heart of the day-night atmosphere. Each building stores a grid of window objects and updates their state (on/off) every frame based on the time of day. At night, windows gradually turn on and flicker, creating a sense of life in the buildings. At dawn, they turn off. During the day, they stay off. During dusk, they turn back on. This creates a convincing simulation of realistic building behavior. The window state changes aren't instant—they're probabilistic ('if random() < 0.005'), so windows turn on gradually, one at a time, rather than all at once.

🔬 This condition checks if time > 0.75 OR time < 0.25. This captures night time. What if you changed it to && (AND)—would it ever be true, or would the windows not light up at night?

    this.windows.forEach(win => {
      // Night (e.g., 0.8 to 1.0 and 0.0 to 0.3)
      if (time > 0.75 || time < 0.25) {
        // Gradually turn on windows that are off
        if (win.state === 0 && random() < 0.005) { // Small chance to turn on
          win.state = 1;
          win.lastChangeTime = millis();
        }
class Building {
  constructor(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.color = color(random(20, 60), random(20, 60), random(30, 90)); // Dark grey/blueish
    this.windows = [];
    this.generateWindows();
  }

  generateWindows() {
    const minWinWidth = this.w * 0.05;
    const maxWinWidth = this.w * 0.15;
    const minWinHeight = this.h * 0.03;
    const maxWinHeight = this.h * 0.08;
    const padding = 10; // Padding from building edges

    let currentY = this.y + padding;
    while (currentY < this.y + this.h - padding) {
      // Declare winH here so it's accessible for both window creation and currentY update
      let winH = random(minWinHeight, maxWinHeight);
      let currentX = this.x + padding;
      while (currentX < this.x + this.w - padding) {
        let winW = random(minWinWidth, maxWinWidth);

        // Randomly decide if a window exists at this spot
        if (random() > 0.2) { // 80% chance of a window
          this.windows.push({
            x: currentX,
            y: currentY,
            w: winW,
            h: winH, // Use the winH declared for this row
            state: 0, // 0 = off, 1 = on
            lastChangeTime: 0,
            flickerDelay: random(500, 2000) // How often it might flicker
          });
        }
        currentX += winW + random(5, 15); // Horizontal spacing
      }
      currentY += winH + random(10, 25); // Vertical spacing, uses the winH for the current row
    }
  }

  update(time) {
    this.windows.forEach(win => {
      // Night (e.g., 0.8 to 1.0 and 0.0 to 0.3)
      if (time > 0.75 || time < 0.25) {
        // Gradually turn on windows that are off
        if (win.state === 0 && random() < 0.005) { // Small chance to turn on
          win.state = 1;
          win.lastChangeTime = millis();
        }
        // Flicker windows that are on
        if (win.state === 1 && millis() - win.lastChangeTime > win.flickerDelay && random() < 0.1) {
          win.state = random() > 0.9 ? 0 : 1; // 10% chance to flicker off, 90% chance to flicker back on
          win.lastChangeTime = millis();
          win.flickerDelay = random(500, 2000); // New flicker delay
        }
      }
      // Dawn (e.g., 0.25 to 0.4)
      else if (time > 0.25 && time < 0.4) {
        // Gradually turn off windows that are on
        if (win.state === 1 && random() < 0.01) { // Small chance to turn off
          win.state = 0;
          win.lastChangeTime = millis();
        }
      }
      // Day (e.g., 0.4 to 0.7)
      else if (time >= 0.4 && time <= 0.7) {
        win.state = 0; // All windows off during the day
      }
      // Dusk (e.g., 0.7 to 0.75)
      else if (time > 0.7 && time < 0.75) {
        // Gradually turn on windows that are off
        if (win.state === 0 && random() < 0.01) { // Small chance to turn on
          win.state = 1;
          win.lastChangeTime = millis();
        }
      }
    });
  }

  display() {
    // Draw building body
    fill(this.color);
    rect(this.x, this.y, this.w, this.h);

    // Draw windows
    this.windows.forEach(win => {
      if (win.state === 1) {
        fill(255, 200, 0, random(180, 255)); // Bright yellow/orange, slight transparency for glow
      } else {
        fill(this.color, 150); // Darker, slightly transparent to show building color
      }
      rect(win.x, win.y, win.w, win.h);
    });
  }

  displayShadow() {
    fill(0, 0, 0, 50); // Transparent black for shadow
    rect(this.x + 10, this.y + 10, this.w, this.h);
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

function Constructor constructor(x, y, w, h) { this.x = x; this.y = y; this.w = w; this.h = h; this.color = color(random(20, 60), random(20, 60), random(30, 90)); this.windows = []; this.generateWindows(); }

Initializes a building with position, size, a dark blue-grey color, and generates its windows

method Window Generation generateWindows() { ... }

Procedurally creates window objects in a grid, with 80% probability at each grid position

method Time-Based Window Logic update(time) { ... }

Updates each window's on/off state based on time of day with flicker effects

method Drawing display() { ... }

Draws the building body and all windows (lit yellow or dark)

method Shadow Drawing displayShadow() { ... }

Draws a semi-transparent shadow below the building

constructor(x, y, w, h) {
The constructor is called when a Building is created with new Building(x, y, w, h)
this.x = x;
Stores the building's left edge x position
this.color = color(random(20, 60), random(20, 60), random(30, 90));
Gives each building a slightly different dark blue-grey color for variety
this.windows = [];
Creates an empty array to store window objects
this.generateWindows();
Calls the method that populates the windows array
let winH = random(minWinHeight, maxWinHeight);
Chooses a random height for this row of windows
if (random() > 0.2) {
80% chance to add a window at this grid position (20% chance to skip it for an empty spot)
win.state: 0,
0 = window off (dark), 1 = window on (lit). All windows start off.
flickerDelay: random(500, 2000)
Each window has a random delay before it flickers (0.5-2 seconds)
if (time > 0.75 || time < 0.25) {
During night (early night 0-0.25 or late night 0.75-1), windows gradually turn on and flicker
if (win.state === 0 && random() < 0.005) {
Every frame, there's a 0.5% chance a dark window turns on
if (win.state === 1 && millis() - win.lastChangeTime > win.flickerDelay && random() < 0.1) {
If a window is on, after its flicker delay expires, there's a 10% chance per frame it flickers
win.state = random() > 0.9 ? 0 : 1;
10% chance to turn off, 90% chance to turn back on immediately (a brief flicker)
else if (time >= 0.4 && time <= 0.7) { win.state = 0;
During daytime, all windows are forced off (no chance variation)
if (win.state === 1) { fill(255, 200, 0, random(180, 255));
Lit windows are bright yellow-orange with a slight glow (high alpha)
} else { fill(this.color, 150);
Dark windows match the building color with slight transparency, blending into the façade
fill(0, 0, 0, 50); rect(this.x + 10, this.y + 10, this.w, this.h);
Draws a semi-transparent black rectangle offset 10 pixels down and right to create a shadow effect

Cloud (class)

The Cloud class creates organic, fluffy shapes by combining multiple overlapping ellipses. Rather than drawing perfect circles, each cloud's shape is generated once in the constructor and stored in shapeDetails, then reused every frame in display(). This is an efficient pattern: expensive shape generation happens once, cheap rendering happens every frame. Clouds drift across the screen and wrap around, creating a sense of continuous wind and motion.

🔬 Each cloud is made of 3-7 random ellipses. What happens if you change random(3, 7) to random(8, 15)? Will clouds look puffier or more spiky?

    const numEllipses = random(3, 7);
    for (let i = 0; i < numEllipses; i++) {
      this.shapeDetails.push({
        offsetX: map(i, 0, numEllipses, -this.w / 3, this.w / 3) + random(-5, 5),
        offsetY: random(-this.h / 4, this.h / 4),
        ellipseW: random(this.w / numEllipses, this.w / (numEllipses * 0.5)),
        ellipseH: random(this.h / 2, this.h)
      });
    }
class Cloud {
  constructor(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.speed = random(0.5, 2);
    this.color = color(255, 255, 255, 180); // Semi-transparent white
    this.shapeDetails = []; // Store ellipse details here

    // Generate the cloud shape details once
    const numEllipses = random(3, 7);
    for (let i = 0; i < numEllipses; i++) {
      this.shapeDetails.push({
        offsetX: map(i, 0, numEllipses, -this.w / 3, this.w / 3) + random(-5, 5),
        offsetY: random(-this.h / 4, this.h / 4),
        ellipseW: random(this.w / numEllipses, this.w / (numEllipses * 0.5)),
        ellipseH: random(this.h / 2, this.h)
      });
    }
  }

  update() {
    this.x += this.speed;
    if (this.x > width + this.w / 2) {
      this.x = -this.w / 2;
      this.y = random(height * 0.05, height * 0.35); // Reset y position
    }
  }

  display() {
    fill(this.color);
    noStroke();

    // Draw the cloud shape using stored ellipse details
    this.shapeDetails.forEach(detail => {
      ellipse(this.x + detail.offsetX, this.y + detail.offsetY, detail.ellipseW, detail.ellipseH);
    });
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

function Constructor constructor(x, y, w, h) { ... }

Initializes a cloud with position, size, speed, and generates its shape from 3-7 overlapping ellipses

for-loop Shape Generation const numEllipses = random(3, 7); for (let i = 0; i < numEllipses; i++) { ... }

Creates random offsets and sizes for 3-7 ellipses to make the cloud shape unique

method Movement and Wrapping update() { ... }

Moves the cloud left-to-right; wraps it off-screen to the left and resets y position when it exits the right

method Drawing display() { ... }

Draws the cloud as a set of overlapping ellipses

this.speed = random(0.5, 2);
Each cloud drifts at a random speed (0.5-2 pixels per frame)
this.color = color(255, 255, 255, 180);
All clouds are white with alpha 180 (semi-transparent)
const numEllipses = random(3, 7);
Each cloud is made of 3-7 overlapping ellipses for a fluffy look
offsetX: map(i, 0, numEllipses, -this.w / 3, this.w / 3) + random(-5, 5),
Spreads ellipses horizontally across the cloud width with some randomness
this.x += this.speed;
Moves the cloud to the right by its speed value each frame
if (this.x > width + this.w / 2) {
When the cloud exits the right edge of the canvas
this.x = -this.w / 2;
Reset it to the left edge, creating an infinite scrolling effect
this.y = random(height * 0.05, height * 0.35);
Give it a new random y position so clouds don't return at the same height
ellipse(this.x + detail.offsetX, this.y + detail.offsetY, detail.ellipseW, detail.ellipseH);
Draws each ellipse at its offset position with its stored width and height

Bird (class)

The Bird class simulates a flying bird using simple geometry: two rotating triangles form wings that flap using a sine wave. The bird's x position increases steadily (flying forward), and its y position oscillates gently (flying up and down). Wing rotation is tied to millis() (real time), not the bird's position, so wings flap at a consistent rate independent of how fast the bird flies. This is a good example of combining multiple animations (forward movement, up-down bobbing, wing flapping) to create a convincing, life-like effect.

🔬 The bird's y position uses sin(this.x * 0.01) to create wave motion. What if you changed 0.01 to 0.001—would the waves be taller, wider, or unchanged?

  update() {
    this.x += this.speed;
    this.y += sin(this.x * 0.01) * 2; // Simple wave motion

    // Wing flapping animation
    this.wingAngle = sin(millis() * this.wingSpeed) * PI / 6; // Flap up and down
class Bird {
  constructor(x, y, speed) {
    this.x = x;
    this.y = y;
    this.speed = speed;
    this.color = color(0); // Black birds
    this.wingAngle = 0;
    this.wingSpeed = random(0.1, 0.3);
  }

  update() {
    this.x += this.speed;
    this.y += sin(this.x * 0.01) * 2; // Simple wave motion

    // Wing flapping animation
    this.wingAngle = sin(millis() * this.wingSpeed) * PI / 6; // Flap up and down

    if (this.x > width + 50) {
      this.x = -50;
      this.y = random(height * 0.2, height * 0.4);
    }
  }

  display() {
    fill(this.color);
    noStroke();

    push();
    translate(this.x, this.y); // Translate to the bird's center

    // Left wing
    push();
    rotate(this.wingAngle); // Rotate left wing up/down
    triangle(-10, 0, 0, -15, 0, 0); // Draw left wing relative to center
    pop();

    // Right wing
    push();
    rotate(-this.wingAngle); // Rotate right wing down/up (opposite)
    triangle(10, 0, 0, -15, 0, 0); // Draw right wing relative to center
    pop();

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

🔧 Subcomponents:

function Constructor constructor(x, y, speed) { ... }

Initializes a bird with position, speed, and wing animation properties

method Movement and Animation update() { ... }

Updates bird position, adds wave motion, flaps wings, and wraps off-screen

method Drawing display() { ... }

Draws the bird as two rotating triangles (wings) using transform

this.wingSpeed = random(0.1, 0.3);
Each bird flaps at a different speed (slower = 0.1 to faster = 0.3 rad/frame)
this.x += this.speed;
Moves the bird forward (left-to-right) by its speed value
this.y += sin(this.x * 0.01) * 2;
Adds a gentle up-down bobbing motion: sin(x*0.01) creates a slow wave that oscillates ±1, multiplied by 2 for amplitude
this.wingAngle = sin(millis() * this.wingSpeed) * PI / 6;
Wing angle oscillates up and down using a sine wave: sin(millis() * wingSpeed) swings -1 to 1, multiplied by PI/6 (about 30 degrees)
translate(this.x, this.y);
Moves the coordinate system to the bird's center so rotation happens around the bird, not the origin
rotate(this.wingAngle);
Rotates the left wing by the current wing angle
triangle(-10, 0, 0, -15, 0, 0);
Draws a left-pointing triangle (left wing) relative to the translated center: three points form a triangular shape
rotate(-this.wingAngle);
Right wing rotates in the opposite direction for flapping symmetry
triangle(10, 0, 0, -15, 0, 0);
Draws a right-pointing triangle (right wing) that flaps opposite to the left wing

ShootingStar (class)

The ShootingStar class creates animated meteors using simple line drawing. Each meteor is a line that extends backward from the meteor's current position along its velocity vector, creating a glowing tail effect. When a meteor exits the screen, reset() immediately spawns a new one at a random position, creating a continuous stream. The additive blending (blendMode(ADD)) makes the white lines glow against the dark night sky.

class ShootingStar {
  constructor() {
    this.reset();
  }

  reset() {
    this.x = random(width * 0.2, width * 0.8);
    this.y = random(-height * 0.1, height * 0.1);
    this.speedX = random(5, 10);
    this.speedY = random(5, 10);
    this.length = random(30, 80);
    this.color = color(255, 255, 255, random(150, 255));
    this.active = true;
  }

  update() {
    if (!this.active) return;

    this.x += this.speedX;
    this.y += this.speedY;

    // Reset if it goes off screen
    if (this.y > height + this.length || this.x > width + this.length) {
      this.reset();
    }
  }

  display() {
    if (!this.active) return;

    push();
    // Blend mode for a glowing trail
    blendMode(ADD);
    strokeWeight(2);
    stroke(this.color);
    line(this.x, this.y, this.x - this.speedX * 2, this.y - this.speedY * 2);
    pop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

function Constructor constructor() { this.reset(); }

Calls reset() to initialize a new shooting star

method Reset (Respawn) reset() { ... }

Randomizes position, speed, length, and color for a new shooting star streak

method Movement update() { ... }

Moves the star diagonally and resets it if it goes off-screen

method Drawing display() { ... }

Draws a glowing line (streak) from the star's position backward along its velocity vector

this.x = random(width * 0.2, width * 0.8);
Spawns the meteor horizontally in the middle 60% of the screen (avoiding far edges)
this.y = random(-height * 0.1, height * 0.1);
Spawns the meteor just above the screen (negative y) so it streaks downward into view
this.speedX = random(5, 10);
Meteor moves diagonally right by 5-10 pixels per frame
this.speedY = random(5, 10);
Meteor moves diagonally down by 5-10 pixels per frame
this.length = random(30, 80);
The trail length (how far the streak extends) is randomized
this.color = color(255, 255, 255, random(150, 255));
Meteors are white with varying alpha (brightness), so some appear brighter/dimmer
if (!this.active) return;
Early return if the meteor is not active (though this.active is always set to true)
if (this.y > height + this.length || this.x > width + this.length) {
Resets the meteor when it exits the screen (bottom or right edge)
line(this.x, this.y, this.x - this.speedX * 2, this.y - this.speedY * 2);
Draws a line from the meteor's current position backward along its velocity (speedX*2, speedY*2), creating a glowing trail effect

LightPool (class)

The LightPool class represents the circle of light cast on the rooftop by each lantern. It's drawn separately from the lantern itself and fades/shrinks faster than the lantern, creating the visual impression that the lantern is rising away from the rooftop, its light dimming as it goes higher. This class is a good example of composition: the Lantern class owns a LightPool instance and updates it in sync with the lantern's animation.

class LightPool {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.alpha = 150; // Initial brightness
    this.size = random(20, 40); // Initial size
  }

  update(lanternProgress) {
    // Fade and shrink as the lantern rises
    this.alpha = lerp(150, 0, lanternProgress * 2); // Fade faster than lantern
    this.size = lerp(this.size, 5, lanternProgress * 1.5); // Shrink faster
  }

  display() {
    if (this.alpha > 0) { // Only draw if visible
      push();
      blendMode(ADD);
      noStroke();
      fill(255, 165, 0, this.alpha); // Warm orange
      circle(this.x, this.y, this.size);
      pop();
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

function Constructor constructor(x, y) { ... }

Initializes a light pool at the lantern's position with starting brightness and size

method Fade and Shrink update(lanternProgress) { ... }

Fades and shrinks the light pool as the lantern rises

method Drawing display() { ... }

Draws the light pool as an orange circle with additive blending

this.alpha = 150;
Light pools start at alpha 150 (bright enough to see on the dark rooftops)
this.size = random(20, 40);
Each light pool has a random initial size (20-40 pixel diameter)
this.alpha = lerp(150, 0, lanternProgress * 2);
As lanternProgress goes from 0 (just spawned) to 1 (fully risen), alpha fades from 150 to 0. The *2 makes it fade twice as fast as the lantern itself
this.size = lerp(this.size, 5, lanternProgress * 1.5);
Shrinks from the initial size down to 5 pixels as it rises, with the *1.5 making it shrink faster than the lantern fades
fill(255, 165, 0, this.alpha);
Warm orange glow (red=255, green=165, blue=0) with alpha from update()
circle(this.x, this.y, this.size);
Draws a circle at the light pool's position with the current size

Lantern (class)

The Lantern class is the most complex animated object in the sketch. It simulates a paper lantern floating upward with a glowing effect. The key techniques are: (1) Perlin noise for natural-looking wobble, (2) progress mapping to tie multiple animations (fade, shrink) to a single variable, (3) composition with LightPool (a separate object that's updated in sync). The lantern fades and shrinks as it rises, and when it becomes invisible, the parent draw() loop removes it from the array. This is a complete lifecycle: spawn, animate, fade, remove.

🔬 This code uses Perlin noise for smooth wobbling. What if you changed the increment from 0.01 to 0.1—would the wobble speed up or stay the same?

    this.x += map(noise(this.noiseOffset), 0, 1, -1, 1); // Perlin noise horizontal wobble
    this.noiseOffset += 0.01;
class Lantern {
  constructor(startX, startY) {
    this.x = startX;
    this.y = startY;
    this.initialY = startY;
    this.size = random(8, 15);
    this.alpha = 255;
    this.speed = random(0.8, 1.5);
    this.noiseOffset = random(1000); // For Perlin noise wobble
    this.lightPool = new LightPool(startX, startY); // Each lantern has a light pool
  }

  update() {
    this.y -= this.speed;
    this.x += map(noise(this.noiseOffset), 0, 1, -1, 1); // Perlin noise horizontal wobble
    this.noiseOffset += 0.01;

    // Fade and shrink as it rises
    let progress = map(this.y, this.initialY, 0, 0, 1); // 0 at initialY, 1 at top
    progress = constrain(progress, 0, 1);
    this.alpha = lerp(255, 0, progress);
    this.size = lerp(this.size, 2, progress); // Shrink to a minimum size

    // Update the associated light pool
    this.lightPool.update(progress);
  }

  display() {
    push();
    blendMode(ADD); // For glowing effect
    noStroke();
    fill(255, 165, 0, this.alpha); // Warm orange
    circle(this.x, this.y, this.size);
    pop();
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

function Constructor constructor(startX, startY) { ... }

Initializes a lantern at a rooftop position with random size and speed, and creates its light pool

calculation Perlin Noise Wobble this.x += map(noise(this.noiseOffset), 0, 1, -1, 1); this.noiseOffset += 0.01;

Uses Perlin noise for smooth, natural-looking horizontal wobble as the lantern rises

calculation Progress Mapping let progress = map(this.y, this.initialY, 0, 0, 1); progress = constrain(progress, 0, 1);

Converts the lantern's y position into a 0-1 progress value (0 at spawn, 1 at top of screen)

calculation Fade and Shrink this.alpha = lerp(255, 0, progress); this.size = lerp(this.size, 2, progress);

Uses progress to fade and shrink the lantern as it rises

method Drawing display() { ... }

Draws the lantern as an orange circle with additive blending (glow)

this.initialY = startY;
Records the starting y position so we can measure progress as the lantern rises
this.noiseOffset = random(1000);
Each lantern gets a unique starting point in the Perlin noise function, so they don't all wobble in sync
this.lightPool = new LightPool(startX, startY);
Creates a LightPool object owned by this lantern; it will fade as the lantern rises
this.y -= this.speed;
Moves the lantern upward (decreasing y) by its speed value each frame
this.x += map(noise(this.noiseOffset), 0, 1, -1, 1);
Uses Perlin noise to add smooth, random horizontal wobble (-1 to 1 pixels per frame)
this.noiseOffset += 0.01;
Increments the noise offset each frame to progress through the Perlin noise sequence
let progress = map(this.y, this.initialY, 0, 0, 1);
Maps y position: when y = initialY (start), progress = 0; when y = 0 (top), progress = 1
progress = constrain(progress, 0, 1);
Clamps progress to 0-1 in case the lantern goes past y=0 (off the top)
this.alpha = lerp(255, 0, progress);
Fades from fully opaque (255) to invisible (0) as progress goes from 0 to 1
this.lightPool.update(progress);
Updates the light pool with the same progress value, keeping them in sync
fill(255, 165, 0, this.alpha);
Warm orange color with alpha controlled by progress (fades as it rises)

📦 Key Variables

buildings array

Array of Building objects representing the silhouetted skyline; each has windows that turn on/off based on time of day

let buildings = [];
clouds array

Array of Cloud objects that drift across the sky during day/dusk/dawn

let clouds = [];
birds array

Array of Bird objects that fly in formation during sunrise

let birds = [];
shootingStars array

Array of ShootingStar objects that streak across the night sky

let shootingStars = [];
lanterns array

Array of Lantern objects that rise and fade during dusk

let lanterns = [];
stars array

Array of simple star objects (x, y, brightness, flickerOffset) for the static night sky

let stars = [];
skyBuffer p5.Graphics

Off-screen graphics buffer that caches the expensive sky gradient rendering for performance optimization

let skyBuffer;
dayTime number

Normalized time value (0 to 1) representing progress through the day-night cycle; controls all time-based visual changes

let dayTime = 0;
cycleDuration number

Duration of one complete day-night cycle in milliseconds (default 60000 = 60 seconds)

const cycleDuration = 60 * 1000;
cycleStartTime number

Timestamp (in milliseconds) when the cycle started; used to calculate current dayTime

let cycleStartTime;
lastRenderedDayTime number

Tracks the last dayTime value at which the sky gradient was rendered; used for optimization to only re-render when dayTime changes significantly

let lastRenderedDayTime = -1;
numStars number

Number of static stars to generate in the night sky (default 200)

const numStars = 200;
skyPalette array of objects

Array of color waypoints defining the sky gradient at different times of day; each entry has 'time' (0-1) and 'color' (hex string)

const skyPalette = [{time: 0.0, color: '#030325'}, ...];
sunColor p5.Color

Pre-defined yellow color for the sun, used during daytime

let sunColor = color(255, 200, 0);
moonColor p5.Color

Pre-defined pale blue color for the moon, used during nighttime

let moonColor = color(200, 200, 255);
sunMoonSize number

Diameter of the sun/moon in pixels (default 60); controls how large they appear in the sky

let sunMoonSize = 60;
hasSpawnedLanternsThisCycle boolean

Flag to ensure lanterns spawn exactly once per cycle during the dusk phase (0.8-0.95)

let hasSpawnedLanternsThisCycle = false;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE renderSkyToBuffer() - for loop

Drawing one horizontal line per pixel row (200-300+ lines per render) is inefficient; consider using a shader or coarser lines

💡 Increase the loop step size (for (let i = 0; i < buffer.height; i += 2)) to draw every other line and interpolate between them, or use createCanvas().drawingContext to access WebGL for gradient rendering

BUG draw() - star rendering

Stars are drawn with a fixed circle size of 1.5, making them very small and hard to see even at full brightness

💡 Increase the circle size to 2-3 pixels, or make it scale with brightness: circle(star.x, star.y, map(star.brightness, 100, 255, 1.5, 3))

STYLE Multiple classes - window state management

Window state is tracked with a 0/1 number rather than a boolean, making the code less readable (e.g., 'win.state === 1' instead of 'win.isOn')

💡 Change window state to boolean: win.isOn = true/false, and update all comparisons accordingly for clarity

FEATURE draw() - lantern spawning

Lanterns are only spawned during dusk (0.8-0.95); they disappear quickly, limiting visual impact

💡 Extend the spawn window (0.75-1.0) or spawn new batches multiple times per cycle to maintain a visible population of lanterns throughout the evening

BUG Building.update() - window flicker logic

Windows can flicker excessively if flickerDelay is very short; the reset sets a new delay, but the flicker can feel chaotic

💡 Add a minimum time between flickers (e.g., store lastFlickerTime and check millis() - lastFlickerTime > 300 before allowing the next flicker) for more realistic, controlled flickering

PERFORMANCE draw() - lantern array iteration

Lantern update loop removes elements mid-iteration (splice), which is generally inefficient in JavaScript

💡 Use a filter() method at the end: lanterns = lanterns.filter(l => l.alpha > 0 && l.y > -50); instead of splice inside the loop

🔄 Code Flow

Code flow showing setup, draw, renderskytobuffer, getskycolor, drawsunmoon, generatecity, generateclouds, generatebirds, generateshootingstars, generatestars, generatelanterns, windowresized, building, cloud, bird, shootingstar, lightpool, lantern

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" draw --> daytimecalc[Day Time Calculation] draw --> bufferoptimization[Sky Buffer Optimization] draw --> conditionalrendering[Conditional Element Rendering] draw --> starrendering[Star Flickering] draw --> drawsunmoon[drawsunmoon] draw --> generateshootingstars[generateshootingstars] draw --> generateclouds[generateclouds] draw --> generatebirds[generatebirds] draw --> generatestars[generatestars] draw --> generatelanterns[generatelanterns] click daytimecalc href "#sub-daytime-calc" click bufferoptimization href "#sub-buffer-optimization" click conditionalrendering href "#sub-conditional-rendering" click starrendering href "#sub-star-rendering" click drawsunmoon href "#fn-drawsunmoon" click generateshootingstars href "#fn-generateshootingstars" click generateclouds href "#fn-generateclouds" click generatebirds href "#fn-generatebirds" click generatestars href "#fn-generatestars" click generatelanterns href "#fn-generatelanterns" daytimecalc --> buffercreation[Sky Buffer Initialization] daytimecalc --> generationcalls[Scene Generation] click buffercreation href "#sub-buffer-creation" click generationcalls href "#sub-generation-calls" bufferoptimization --> bufferclear[Buffer Clear] bufferoptimization --> buffercreation click bufferclear href "#sub-buffer-clear" conditionalrendering --> lanternspawn[Lantern Spawning Logic] conditionalrendering --> sunvmoon[Sun or Moon Selection] click lanternspawn href "#sub-lantern-spawn" click sunvmoon href "#sub-sun-vs-moon" starrendering --> stararray[Star Creation Loop] starrendering --> gradientloop[Horizontal Line Gradient] click stararray href "#sub-star-array" click gradientloop href "#sub-gradient-loop" drawsunmoon --> arcpath[Arc Path Calculation] drawsunmoon --> sizevariation[Horizon Size Distortion] click arcpath href "#sub-arc-path" click sizevariation href "#sub-size-variation" generateshootingstars --> initialspawn[Initial Shooting Stars] click initialspawn href "#sub-initial-spawn" generateclouds --> cloudloop[Cloud Generation Loop] click cloudloop href "#sub-cloud-loop" generatebirds --> formationsetup[Formation Parameters] generatebirds --> birdplacement[Bird Offset Calculation] click formationsetup href "#sub-formation-setup" click birdplacement href "#sub-bird-placement" generatestars --> stararray click stararray href "#sub-star-array" generatelanterns --> lanternloop[Lantern Spawning] click lanternloop href "#sub-lantern-loop" windowresized[windowResized] --> arrayclearing[Array Clearing] windowresized --> regeneration[Scene Regeneration] click windowresized href "#fn-windowresized" click arrayclearing href "#sub-array-clearing" click regeneration href "#sub-regeneration" renderskytobuffer[renderskytobuffer] --> gradientloop click renderskytobuffer href "#fn-renderskytobuffer" getskycolor[getskycolor] --> paletteSearch[Palette Entry Lookup] getskycolor --> colorLerp[Color Interpolation] getskycolor --> verticalGradient[Vertical Darkening] click getskycolor href "#fn-getskycolor" click paletteSearch href "#sub-palette-search" click colorLerp href "#sub-color-lerp" click verticalGradient href "#sub-vertical-gradient" building[building] --> constructor[Constructor] building --> generatewindows[Window Generation] building --> updateMethod[Time-Based Window Logic] building --> displayMethod[Drawing] building --> displayShadow[Shadow Drawing] click building href "#fn-building" click constructor href "#sub-constructor" click generatewindows href "#sub-generatewindows" click updateMethod href "#sub-update-method" click displayMethod href "#sub-display-method" click displayShadow href "#sub-displayshadow-method" cloud[cloud] --> constructorCloud[Constructor] cloud --> shapeGeneration[Shape Generation] cloud --> updateCloud[Movement and Wrapping] cloud --> displayCloud[Drawing] click cloud href "#fn-cloud" click constructorCloud href "#sub-constructor-cloud" click shapeGeneration href "#sub-shape-generation" click updateCloud href "#sub-update-cloud" click displayCloud href "#sub-display-cloud" bird[bird] --> constructorBird[Constructor] bird --> updateBird[Movement and Animation] bird --> displayBird[Drawing] click bird href "#fn-bird" click constructorBird href "#sub-constructor-bird" click updateBird href "#sub-update-bird" click displayBird href "#sub-display-bird" shootingstar[shootingstar] --> constructorShooting[Constructor] shootingstar --> resetMethod[Reset (Respawn)] shootingstar --> updateShooting[Movement] shootingstar --> displayShooting[Drawing] click shootingstar href "#fn-shootingstar" click constructorShooting href "#sub-constructor-shooting" click resetMethod href "#sub-reset-method" click updateShooting href "#sub-update-shooting" click displayShooting href "#sub-display-shooting" lightpool[lightpool] --> constructorLightpool[Constructor] lightpool --> updateLightpool[Fade and Shrink] lightpool --> displayLightpool[Drawing] click lightpool href "#fn-lightpool" click constructorLightpool href "#sub-constructor-lightpool" click updateLightpool href "#sub-update-lightpool" click displayLightpool href "#sub-display-lightpool" lantern[lantern] --> constructorLantern[Constructor] lantern --> perlinWobble[Perlin Noise Wobble] lantern --> progressCalc[Progress Mapping] lantern --> fadeShrink[Fade and Shrink] lantern --> displayLantern[Drawing] click lantern href "#fn-lantern" click constructorLantern href "#sub-constructor-lantern" click perlinWobble href "#sub-perlin-wobble" click progressCalc href "#sub-progress-calc" click fadeShrink href "#sub-fade-shrink" click displayLantern href "#sub-display-lantern"

❓ Frequently Asked Questions

What visual elements are featured in the CitySkyline p5.js sketch?

The CitySkyline sketch visually represents a city skyline transitioning through a colorful day-night cycle with elements like twinkling stars, drifting clouds, birds, and shooting stars.

Is there any user interaction available in the CitySkyline sketch?

The sketch is primarily a visual experience with no direct user interactions; it continuously evolves the atmosphere without requiring user input.

What creative coding concepts are demonstrated in the CitySkyline sketch?

This sketch showcases techniques such as smooth color transitions, animated elements like clouds and stars, and the use of arrays to manage multiple visual components in a dynamic scene.

Preview

CitySkyline - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of CitySkyline - Code flow showing setup, draw, renderskytobuffer, getskycolor, drawsunmoon, generatecity, generateclouds, generatebirds, generateshootingstars, generatestars, generatelanterns, windowresized, building, cloud, bird, shootingstar, lightpool, lantern
Code Flow Diagram