moon-cycle
Maps dates and times to NASA SVS moon phase imagery. 8,760 hourly frames covering a full year, delivered via jsDelivr CDN.
Overview
The NASA Scientific Visualization Studio publishes annual moon phase video sequences — one frame per hour for an entire year, showing the Moon from Earth's perspective with accurate libration, phase illumination, and orientation.
moon-cycle provides a TypeScript API to map any date/time to the correct frame index and generate a CDN URL for that image.
GitHub: github.com/acamarata/moon-cycle
Installation
<span><span style="color: var(--shiki-token-function)">pnpm</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">add</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">moon-cycle</span></span>
<span></span>
API
cycleMonth
Returns an array of frame descriptors for every hour of a given month.
<span><span style="color: var(--shiki-token-function)">cycleMonth</span><span style="color: var(--shiki-color-text)">(options: { year</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> number; month: number }): MoonFrame[]</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">import</span><span style="color: var(--shiki-color-text)"> { cycleMonth</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> cdnUrl } </span><span style="color: var(--shiki-token-keyword)">from</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'moon-cycle'</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">frames</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">cycleMonth</span><span style="color: var(--shiki-color-text)">({ year</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">2025</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> month</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">3</span><span style="color: var(--shiki-color-text)"> })</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">frames</span><span style="color: var(--shiki-color-text)">.</span><span style="color: var(--shiki-token-constant)">length</span><span style="color: var(--shiki-color-text)">) </span><span style="color: var(--shiki-token-comment)">// 744 (31 days × 24 hours)</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">firstFrame</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> frames[</span><span style="color: var(--shiki-token-constant)">0</span><span style="color: var(--shiki-color-text)">]</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">firstFrame</span><span style="color: var(--shiki-color-text)">.date) </span><span style="color: var(--shiki-token-comment)">// 2025-03-01T00:00:00Z</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">firstFrame</span><span style="color: var(--shiki-color-text)">.index) </span><span style="color: var(--shiki-token-comment)">// frame index in the NASA dataset</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-function)">cdnUrl</span><span style="color: var(--shiki-color-text)">(firstFrame)) </span><span style="color: var(--shiki-token-comment)">// jsDelivr CDN URL</span></span>
<span></span>
cycleYear
Returns frame descriptors for every hour of a full year (8,760 frames for non-leap years, 8,784 for leap years).
<span><span style="color: var(--shiki-token-function)">cycleYear</span><span style="color: var(--shiki-color-text)">(options: { year</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> number }): MoonFrame[]</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">import</span><span style="color: var(--shiki-color-text)"> { cycleYear</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> cdnUrl } </span><span style="color: var(--shiki-token-keyword)">from</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'moon-cycle'</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">frames</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">cycleYear</span><span style="color: var(--shiki-color-text)">({ year</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Date</span><span style="color: var(--shiki-color-text)">()</span><span style="color: var(--shiki-token-function)">.getFullYear</span><span style="color: var(--shiki-color-text)">() })</span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">now</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Date</span><span style="color: var(--shiki-color-text)">()</span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">hourIndex</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">Math</span><span style="color: var(--shiki-token-function)">.floor</span><span style="color: var(--shiki-color-text)">((</span><span style="color: var(--shiki-token-constant)">now</span><span style="color: var(--shiki-token-function)">.getTime</span><span style="color: var(--shiki-color-text)">() </span><span style="color: var(--shiki-token-keyword)">-</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Date</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">now</span><span style="color: var(--shiki-token-function)">.getFullYear</span><span style="color: var(--shiki-color-text)">()</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">0</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">1</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-function)">.getTime</span><span style="color: var(--shiki-color-text)">()) </span><span style="color: var(--shiki-token-keyword)">/</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">3_600_000</span><span style="color: var(--shiki-color-text)">)</span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">currentFrame</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> frames[hourIndex]</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">imageUrl</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">cdnUrl</span><span style="color: var(--shiki-color-text)">(currentFrame)</span></span>
<span><span style="color: var(--shiki-token-comment)">// Use in an <img> tag or Next.js <Image> component</span></span>
<span></span>
cdnUrl
Returns the jsDelivr CDN URL for a given frame.
<span><span style="color: var(--shiki-token-function)">cdnUrl</span><span style="color: var(--shiki-color-text)">(frame: MoonFrame): string</span></span>
<span></span>
CDN delivery
The ~438 MB NASA image dataset is hosted on jsDelivr via the moon-cycle-data npm package. jsDelivr serves all npm package files from their global CDN at no cost.
<span><span style="color: var(--shiki-token-comment)">// https://cdn.jsdelivr.net/npm/moon-cycle-data@2025/frames/2025-03-15-14.jpg</span></span>
<span></span>
Images are 512×512 JPEG, approximately 20–40 KB each. They show the Moon against a black background with accurate libration (wobble), phase illumination, and orientation relative to the horizon.
Dataset details
| Property | Value |
|---|---|
| Source | NASA Scientific Visualization Studio |
| Frames per year | 8,760 (non-leap) or 8,784 (leap) |
| Frame interval | 1 hour |
| Resolution | 512×512 |
| Format | JPEG |
| Dataset size | ~438 MB per year |
| Coverage | 2020–2030 (packaged) |
NASA publishes a new annual sequence each year. The moon-cycle-data npm package is updated annually with the new dataset.