AI Moon Phase Display - Tonight's Lunar Phase

This sketch renders a peaceful night sky filled with 200 twinkling stars alongside a large moon whose illuminated shape and text label update automatically based on today's calendar date. The moon's shadow shrinks and grows across the visual, cycling through all eight named lunar phases as the day of the month changes.

🧪 Try This!

Experiment with the code by making these changes:

  1. Slow down the star twinkle — Lowering the multiplier inside the sine wave makes stars pulse more slowly and calmly.
  2. Make the moon bigger — moonDiameter controls the size of both the moon and its shadow, so increasing it makes the whole scene feel more zoomed in.
  3. Give the shadow a reddish 'blood moon' tint — Changing the shadow's fill color from dark navy to dark red creates an eclipse-like mood.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a calming space scene: a field of stars that gently pulse in brightness, and a large moon in the center of the screen whose visible shape changes to match the actual lunar phase for today's date. Text below the moon names the current phase - New Moon, Waxing Crescent, Full Moon, and so on - making the sketch both decorative and educational. The visual relies on a handful of core p5.js techniques: drawing translucent circles in a loop, using sin() combined with frameCount to animate opacity over time, and layering two ellipses (a bright moon and a dark shadow) to fake a crescent shape.

The code is organized into three functions: setup() builds the canvas and generates the star field once, draw() runs every frame to redraw the twinkling stars and recompute the moon's shadow from the real calendar date, and windowResized() keeps everything centered if the browser window changes size. Studying this sketch teaches you how to turn a JavaScript Date value into a usable animation variable, how simple trigonometry creates smooth pulsing effects, and how overlapping shapes can simulate something as complex-looking as a moon phase.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas that fills the browser window and generates 200 star objects, each with a random position, size, and a random starting point in a sine wave used for twinkling.
  2. Every frame, draw() clears the screen to black and loops through every star, using sin(frameCount * 0.1 + star.alphaOffset) to smoothly oscillate each star's brightness between dim and bright, creating a twinkling effect.
  3. draw() then reads today's day of the month with p5.js's day() function and turns it into a fraction between 0 and 1 representing how far through a ~29.53 day lunar cycle we are.
  4. A full cream-colored circle is drawn for the moon, then a dark ellipse 'shadow' is layered on top - its width is calculated from the phase fraction, and it is positioned on the left side of the moon during waxing phases or the right side during waning phases.
  5. The phase fraction is also used to pick one of eight strings from the phaseNames array, which is drawn as text beneath the moon so the viewer knows exactly which phase they're looking at.
  6. If the browser window is resized, windowResized() resizes the canvas and re-centers the moon's x and y position so the layout still looks correct.

🎓 Concepts You'll Learn

Animation loop (draw)Sine wave oscillation for twinklingDate/time functions (day())Array of objectsConditional logic for phase shadingResponsive canvas (windowResized)

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It's the right place to build data structures (like the stars array) that don't need to be rebuilt every frame - only their drawn appearance changes in draw().

🔬 This loop fills the stars array once at startup. What happens visually if you change random(1, 3) to random(1, 8) so stars vary a lot more in size?

  for (let i = 0; i < 200; i++) { stars.push({ x: random(width), y: random(height), size: random(1, 3), alphaOffset: random(TWO_PI) }); }
function setup() {
  createCanvas(windowWidth, windowHeight);
  for (let i = 0; i < 200; i++) { stars.push({ x: random(width), y: random(height), size: random(1, 3), alphaOffset: random(TWO_PI) }); }
  moonX = width / 2;
  moonY = height / 2;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Star Field Generator for (let i = 0; i < 200; i++) { stars.push({ x: random(width), y: random(height), size: random(1, 3), alphaOffset: random(TWO_PI) }); }

Creates 200 star objects with random positions, sizes, and a random phase offset used for twinkling, and stores them in the stars array.

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, so the sky and moon scale to any screen size.
for (let i = 0; i < 200; i++) { stars.push({ x: random(width), y: random(height), size: random(1, 3), alphaOffset: random(TWO_PI) }); }
Runs 200 times, each time pushing a new star object into the stars array with a random x/y position, a random size between 1 and 3 pixels, and a random alphaOffset (a starting point in a sine wave) so not all stars twinkle in sync.
moonX = width / 2;
Positions the moon horizontally at the center of the canvas.
moonY = height / 2;
Positions the moon vertically at the center of the canvas.

draw()

draw() runs continuously (about 60 times per second by default), which is what makes the stars twinkle smoothly. Since the moon's phase only depends on today's date, it doesn't actually change frame to frame - but recalculating it every draw() call keeps the code simple and would automatically update if the date changed while the page stayed open.

🔬 The 0.1 controls how fast stars twinkle. What happens if you slow it down to 0.02, or speed it up to 0.5?

  for (let star of stars) { fill(255, map(sin(frameCount * 0.1 + star.alphaOffset), -1, 1, 100, 255)); noStroke(); circle(star.x, star.y, star.size); }

🔬 This decides which side the shadow falls on. What happens if you swap the two branches so waxing shadows appear on the right instead of the left?

    if (phaseFraction <= 0.5) { // Waxing phases (shadow on left)
      shadowX = moonX - moonRadius + shadowWidth / 2;
    } else { // Waning phases (shadow on right)
      shadowX = moonX + moonRadius - shadowWidth / 2;
    }
function draw() {
  background(0);
  for (let star of stars) { fill(255, map(sin(frameCount * 0.1 + star.alphaOffset), -1, 1, 100, 255)); noStroke(); circle(star.x, star.y, star.size); }

  let dayOfMonth = day(); // Get current day of month (p5.js function)
  let phaseFraction = (dayOfMonth % 29.53) / 29.53; // ~29.53 days in a synodic month
  let currentPhaseName = phaseNames[floor(phaseFraction * 8)]; // Map fraction to one of 8 phases

  fill(240, 240, 220); // Cream color for moon
  noStroke();
  circle(moonX, moonY, moonDiameter); // Draw full moon

  let shadowWidth = moonDiameter * abs(1 - phaseFraction * 2); // Calculate shadow width (0 to moonDiameter)
  let shadowX;

  if (shadowWidth > 0.1) { // Only draw shadow if it's not effectively zero (Full Moon)
    fill(20, 20, 40); // Dark shadow
    if (phaseFraction <= 0.5) { // Waxing phases (shadow on left)
      shadowX = moonX - moonRadius + shadowWidth / 2;
    } else { // Waning phases (shadow on right)
      shadowX = moonX + moonRadius - shadowWidth / 2;
    }
    ellipse(shadowX, moonY, shadowWidth, moonDiameter); // Draw shadow ellipse
  }

  fill(255);
  textAlign(CENTER, CENTER);
  textSize(16);
  text(currentPhaseName, moonX, moonY + moonRadius + 30); // Display phase name
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Star Twinkle Renderer for (let star of stars) { fill(255, map(sin(frameCount * 0.1 + star.alphaOffset), -1, 1, 100, 255)); noStroke(); circle(star.x, star.y, star.size); }

Loops through every star and draws it with a brightness that oscillates smoothly over time using a sine wave.

calculation Lunar Phase Fraction let phaseFraction = (dayOfMonth % 29.53) / 29.53; // ~29.53 days in a synodic month

Converts today's day-of-month into a fraction from 0 to 1 representing progress through the ~29.53 day lunar cycle.

conditional Shadow Placement Check if (phaseFraction <= 0.5) { // Waxing phases (shadow on left) shadowX = moonX - moonRadius + shadowWidth / 2; } else { // Waning phases (shadow on right) shadowX = moonX + moonRadius - shadowWidth / 2; }

Decides whether the dark shadow ellipse sits to the left (waxing, moon growing) or right (waning, moon shrinking) of the moon's center.

background(0);
Paints the whole canvas black every frame, clearing the previous frame and creating the night sky backdrop.
for (let star of stars) { fill(255, map(sin(frameCount * 0.1 + star.alphaOffset), -1, 1, 100, 255)); noStroke(); circle(star.x, star.y, star.size); }
For each star, calculates a brightness value using sin() (which oscillates between -1 and 1) mapped to an alpha range of 100-255, then draws the star as a small white circle at that brightness.
let dayOfMonth = day();
Calls p5.js's built-in day() function to get today's numeric day of the month (1-31) from the user's system clock.
let phaseFraction = (dayOfMonth % 29.53) / 29.53;
Divides the day of month by the ~29.53-day lunar cycle length to get a repeating fraction between 0 and 1 that represents where we are in the moon's cycle.
let currentPhaseName = phaseNames[floor(phaseFraction * 8)];
Multiplies the fraction by 8 (the number of named phases) and floors it to pick the matching phase name from the phaseNames array.
fill(240, 240, 220);
Sets the fill color to a warm cream tone used for the moon's surface.
circle(moonX, moonY, moonDiameter);
Draws the full moon as a circle - this is the 'base' shape before any shadow is applied.
let shadowWidth = moonDiameter * abs(1 - phaseFraction * 2);
Calculates how wide the dark shadow ellipse should be: it's zero at Full Moon (phaseFraction = 0.5) and grows toward the full diameter near New Moon (phaseFraction near 0 or 1).
if (shadowWidth > 0.1) {
Skips drawing the shadow entirely when it's essentially zero width, so the Full Moon appears perfectly round with no dark sliver.
fill(20, 20, 40);
Sets the fill color to a dark navy used for the shadowed portion of the moon.
shadowX = moonX - moonRadius + shadowWidth / 2;
During waxing phases, positions the shadow ellipse so it covers the left side of the moon, leaving a growing crescent of light on the right.
shadowX = moonX + moonRadius - shadowWidth / 2;
During waning phases, positions the shadow ellipse on the right side instead, so the light appears to shrink from the right.
ellipse(shadowX, moonY, shadowWidth, moonDiameter);
Draws the shadow as an ellipse - narrower than the moon horizontally but the same height, which creates the illusion of a curved lunar terminator.
text(currentPhaseName, moonX, moonY + moonRadius + 30);
Draws the current phase's name as text centered below the moon, offset by the moon's radius plus 30 pixels of padding.

windowResized()

windowResized() is a special p5.js callback that fires automatically on browser resize. It's the standard place to call resizeCanvas() and recompute any layout values (like moonX/moonY) that depend on width and height.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  moonX = width / 2;
  moonY = height / 2;
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
A p5.js built-in event that fires automatically whenever the browser window changes size; here it resizes the canvas to match the new window dimensions.
moonX = width / 2;
Recalculates the moon's horizontal center position using the new canvas width.
moonY = height / 2;
Recalculates the moon's vertical center position using the new canvas height.

📦 Key Variables

stars array

Holds all 200 star objects, each with an x, y, size, and alphaOffset used to animate twinkling.

let stars = [];
phaseNames array

Stores the eight human-readable lunar phase labels in order, indexed by the calculated phase fraction.

let phaseNames = ["New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full Moon", "Waning Gibbous", "Last Quarter", "Waning Crescent"];
moonDiameter number

Sets the pixel diameter of the moon circle and its shadow ellipse.

let moonDiameter = 150;
moonRadius number

Half of moonDiameter, used to position the shadow ellipse relative to the moon's edges.

let moonRadius = moonDiameter / 2;
moonX number

The horizontal pixel position of the moon's center on the canvas.

let moonX;
moonY number

The vertical pixel position of the moon's center on the canvas.

let moonY;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG draw() phase calculation

The phase is derived only from dayOfMonth % 29.53, which has no relationship to an actual known new moon reference date. This means the displayed phase will not match the real moon phase most of the time, despite the sketch's claim of showing 'tonight's lunar phase'.

💡 Calculate days since a known new moon (e.g. Jan 6, 2000) using millis()/Date math, then take that value modulo 29.53 for an astronomically accurate phase.

BUG windowResized()

When the window is resized larger, the stars array is not regenerated, so existing stars remain clustered in the old (smaller) width/height area, leaving new screen space empty of stars.

💡 Either regenerate the stars array in windowResized(), or store star positions as fractions of width/height (e.g. x: random(1) * width at draw time) so they redistribute automatically.

STYLE draw()

Magic numbers like 0.1 (twinkle speed), 29.53 (lunar cycle length), 8 (phase count), and 30 (label offset) appear inline with no named constants, making the code harder to tune and understand.

💡 Extract these into clearly named constants near the top of the file, e.g. const TWINKLE_SPEED = 0.1; const SYNODIC_MONTH = 29.53;

FEATURE draw()

The sketch always shows today's phase with no way to explore other dates or phases interactively.

💡 Add a slider or arrow-key control that lets the user shift a 'days offset' value forward and backward in time, updating phaseFraction so they can preview any phase.

🔄 Code Flow

Code flow showing setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> phasecalculation[phase-calculation] phasecalculation --> shadowconditional[shadow-conditional] draw --> stargenerationloop[star-generation-loop] stargenerationloop --> twinkleloop[twinkle-loop] twinkleloop --> draw windowresized --> draw click setup href "#fn-setup" click draw href "#fn-draw" click stargenerationloop href "#sub-star-generation-loop" click twinkleloop href "#sub-twinkle-loop" click phasecalculation href "#sub-phase-calculation" click shadowconditional href "#sub-shadow-conditional"

❓ Frequently Asked Questions

What visual experience does the AI Moon Phase Display provide?

The sketch features a serene night sky filled with twinkling stars and a prominently displayed moon that accurately represents its current phase, accompanied by descriptive labels.

Is there any user interaction in the AI Moon Phase Display sketch?

The sketch is not interactive; it automatically updates to reflect the current moon phase based on today's date.

What creative coding techniques are showcased in the AI Moon Phase Display?

This sketch demonstrates the use of graphical elements like animations, random star generation, and dynamic calculations for lunar phases to create an educational and meditative visualization.

Preview

AI Moon Phase Display - Tonight's Lunar Phase - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Moon Phase Display - Tonight's Lunar Phase - Code flow showing setup, draw, windowresized
Code Flow Diagram