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)">&#39;moon-cycle&#39;</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)">&#39;moon-cycle&#39;</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 &lt;img&gt; tag or Next.js &lt;Image&gt; 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

PropertyValue
SourceNASA Scientific Visualization Studio
Frames per year8,760 (non-leap) or 8,784 (leap)
Frame interval1 hour
Resolution512×512
FormatJPEG
Dataset size~438 MB per year
Coverage2020–2030 (packaged)

NASA publishes a new annual sequence each year. The moon-cycle-data npm package is updated annually with the new dataset.