webdevpuneet.com

Three.js Scroll Moon Phases — Orbiting-Light Terminator

Three.js Scroll Moon Phases Cycle · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<section class="mnp-stage" id="mnpStage">
  <div class="mnp-intro"><p>Scroll ↓ to pass a lunar month</p></div>
  <canvas id="mnpCanvas"></canvas>
  <div class="mnp-hud" id="mnpPhase">NEW MOON</div>
</section>
<section class="mnp-bottom"><p>A whole month in one scroll.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Phases from real illumination geometry — one DirectionalLight orbiting in the camera–moon plane, zero masks
Terminator curve correct at every angle because Lambert shading computes it, not an artist
26 seeded cosine-bowl craters with raised rims, displaced into the sphere and lit via computed normals
Craters throw their longest shadows exactly at the terminator, where the eye is drawn
Earthshine ambient keeps the night side faintly visible — "the old moon in the new moon's arms"
Libration wobble (±0.04 rad on clock sines) shifts crater lighting across long scrolls
Sun intensity rises toward full, echoing real full-moon contrast washout
Eight-phase HUD labels indexed from the single scrubbed angle, flipping at exact midpoints

About this UI Snippet

How to Build Scroll-Driven Moon Phases With Three.js and GSAP

Screenshot of the Three.js Scroll Moon Phases Cycle snippet rendered live

The Three.js Scroll Moon Phases Cycle snippet renders a single cratered moon and takes it through an entire synodic month in one scroll — new moon, waxing crescent, first quarter, gibbous, full, and back — by doing what the solar system does: moving the light, not masking the moon. GSAP's ScrollTrigger scrubs one sun angle from 0 to 2π; every phase, the terminator's curve, and the HUD label fall out of that geometry.

Phases are lighting, not masks

Most moon-phase widgets overlay crescent-shaped masks on a circle — and the terminator (the day/night boundary) never quite curves correctly through the intermediate phases. This snippet has no masks at all: one DirectionalLight orbits in the camera–moon plane, positioned at (sin(a)·60, 8, −cos(a)·60), so at a = 0 the light is behind the moon (new), at π behind the camera (full), and every angle between produces the exact elliptical terminator that real Lambert shading dictates. Waxing gibbous looks right because it *is* right — the same reason the planet approach's day/night line works during its corkscrew.

Craters that perform at the terminator

The moon is a 160-segment sphere with 26 craters pushed into its vertices at seeded directions — each a smooth cosine bowl with a gaussian-raised rim — plus continuous multi-octave regolith noise (a function of surface direction, never vertex index, so normals stay smooth) and five soft-edged dark maria patches baked as vertex colors. Craters exist for one payoff: the terminator. Along the day/night line, light rakes the surface nearly parallel, so every bowl throws its longest shadows exactly where the eye is drawn — the phenomenon that makes real first-quarter moons more dramatic than full ones. As scroll sweeps the terminator across the disc, each crater lights up, performs its shadow play, and fades — depth generated by displacement geometry, computeVertexNormals(), and per-fragment MeshStandardMaterial shading, no normal maps.

Earthshine keeps the dark side alive

A real new moon is not black — the night side glows faintly with sunlight reflected off Earth ("the old moon in the new moon's arms"). A dim blue-grey AmbientLight reproduces this: the dark portion stays barely legible against the starfield at every phase, which both looks correct and keeps the scene readable at new moon when a naive implementation would show nothing. Sun intensity also rises slightly toward full, echoing how a full moon washes out contrast.

Libration, the connoisseur's detail

The real moon rocks a few degrees over a month (libration), letting us see 59% of its surface over time. The snippet's moon wobbles ±0.04 radians on slow clock sines plus a tiny scroll-linked drift — imperceptible as rotation, but it shifts which crater rims catch the terminator, keeping long scrolls from feeling like a static photograph under a moving lamp. The camera drifts on a slow ellipse rather than orbiting: this is a portrait sitting, and the phase angle — not camera position — is the story.

Eight labels from one angle

The HUD phase name indexes a nine-entry array (new appears at both ends) by round(a / 2π × 8), so labels flip exactly at the midpoints between canonical phases. Reverse scrolling wanes the moon back through the month with the terminator sweeping the opposite direction — free, as always in this series, because everything derives from the one scrubbed angle, the same purity as the gear train's crank. Pair it with the seasons tree for a matched set of natural-cycle scrolls.

Step by step

How to Use

  1. 1
    Load the three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js in that order before the snippet JS.
  2. 2
    Paste HTML, CSS, and JSA dark new moon hangs among stars, its night side faintly visible in blue-grey earthshine — the HUD reads NEW MOON.
  3. 3
    Scroll to waxA crescent opens on one limb and grows — watch craters light up and throw long shadows as the terminator crosses them.
  4. 4
    Pass full moonAt half scroll the light sits behind the camera and the disc floods flat and bright, phase label reading FULL MOON.
  5. 5
    Wane back to newThe terminator sweeps in from the opposite limb through gibbous, last quarter, and crescent — one complete synodic month.
  6. 6
    Make it yoursChange crater count/depth, earthshine color, or replace the HUD labels with dates for a real lunar-calendar tie-in.

Real-world uses

Common Use Cases

Astronomy apps and observatory sites
A geometrically honest phase display expert visitors won't fault — pair with the black hole approach for a space-science set.
Wellness, sleep, and cycle-tracking brands
Lunar-cycle products get their metaphor at full fidelity, calmer than any particle effect — a natural hero above a pricing section.
Teaching why phases happen
The scene IS the explanation: students scrub the sun around the moon and watch phases emerge from geometry, the misconception-killer diagram made interactive.
Night-mode and theme-switch storytelling
Sync a page's dark-mode transition to the waxing moon, landing full dark theme at full moon.
Poetry, tarot, and editorial pages
Eight phase labels map to eight stanzas or cards; the wane-in-reverse reading becomes part of the piece.
Werewolf and night-cycle game promos
Games with lunar mechanics can preview their cycle honestly — the full-moon beat lands exactly at 50% scroll.

Got questions?

Frequently Asked Questions

Masks approximate; geometry computes. With a DirectionalLight orbiting at angle a — position (sin(a)·60, 8, −cos(a)·60) — the terminator is the exact great-circle boundary Lambert shading produces, which projects as the correct half-ellipse at every intermediate phase. Crescent masks notoriously get gibbous phases wrong (straight or wrongly-curved terminators); orbital lighting cannot, because it is the same geometry the real moon obeys.

Vertex displacement: 26 seeded directions each define a crater; every sphere vertex within a crater's angular radius is pushed inward on a cosine bowl profile (deepest at center), with a gaussian-profile rim raised just outside the bowl so there is no hard step and no ring artifacts. On top of that, continuous multi-octave trig noise — evaluated from the surface direction, never the vertex index, which would shred the normals into streaks — adds fine regolith roughness, and five soft-edged maria patches darken the vertex colors. computeVertexNormals() afterward makes the dents shade correctly under the per-fragment MeshStandardMaterial. Real geometry means crater shadows lengthen at the terminator automatically — the detail normal maps fake and displacement gets free.

Earthshine — sunlight reflected off Earth dimly illuminating the lunar night side, visible in reality as "the old moon in the new moon's arms" during crescents. A low-intensity blue-grey AmbientLight (0x2a3448 at 0.55) reproduces it, keeping the dark limb legible against the starfield at new moon, where a physically naive scene would render an invisible black disc.

Libration: the real moon's slight rocking that exposes 59% of its surface across a month. Here ±0.04-radian sines on two axes (plus a 0.02-radian scroll drift) are imperceptible as rotation but continuously change which crater rims catch light — especially near the terminator — so a slow scroll never feels like a photo under a moving lamp. It is the idle-life convention of this series applied astronomically.

Yes. Export via the JSX, Vue, Angular, or Tailwind buttons. Build the displaced sphere (a few hundred thousand vertex operations — do it once in the mount effect, never per render), starfield, and ScrollTrigger against a canvas ref. Update the phase label through a ref. On cleanup kill the ScrollTrigger, dispose moon and star geometries/materials, and call renderer.dispose().