Quiet Cosmos Studio · Free guide

Make a 3D space film with Claude, with free tools

A free guide from Quiet Cosmos Studio, the studio behind the Quiet Cosmos space channel. This is how we make our own 3D films: the tools, the steps, the prompts, and the rules we learned from our mistakes.

What you'll make

A 26.7-second vertical film (1080 x 1920, 30 frames a second). It asks one question on its first frame and answers it with true numbers. Our example:

"The Moon just stopped. How long until it hits Earth?"

Every number is worked out from official pages, most of them NASA's fact sheets. If one is wrong, the build stops before a single frame is drawn.

The shape we start from (from research, not proven): the question on frame 1, something arriving by 3 s, then a stop every 2.5 to 2.75 s (we use 2.75), 8 to 10 stops, 20 to 30 s in all, the payoff, and the end. No end card.

What it costs

What you need

The tools and their licences

Tool What it does Licence
Claude Code Writes every file, runs every check, fixes what fails Needs a Claude plan (above)
HyperFrames 0.9.2 (github.com/heygen-com/hyperframes) Turns an HTML page into an MP4, one frame at a time, and checks it Apache 2.0
three.js (we use r187) Draws the Moon, Earth and the stars inside the page MIT
GSAP The animation library HyperFrames works with. Our 3D films don't need it, because every frame is drawn from its own time. We use it for flat cards GSAP Standard "No Charge" License: free, commercial use included. Not allowed in no-code animation tools that compete with Webflow
NASA maps: Blue Marble, Black Marble, the cloud map, the CGI Moon Kit Earth by day and by night, its clouds, the Moon Generally not under copyright in the US. Credit NASA, and never suggest NASA made or backs your film
USGS Viking Mars mosaic Mars Public domain. Credit USGS/NASA
Solar System Scope maps (solarsystemscope.com/textures) The Sun and the other planets CC BY 4.0: free even for commercial use, credit required
Kenney sound packs (kenney.nl): Impact, Interface, Sci-fi Ticks and hits CC0: no credit needed
Plus Jakarta Sans The font SIL Open Font License 1.1
FFmpeg Mixes the sound and measures the finished file LGPL 2.1 or later (some builds GPL)

Pin HyperFrames to an exact version. Five releases came out in one day while we worked. We also turn its telemetry off (step 1).

Step by step

1. An empty folder

mkdir moon-film && cd moon-film
export HYPERFRAMES_NO_TELEMETRY=1
claude

Everything the film uses lives in this folder: code, maps, font, sounds. The film never fetches anything from the web.

2. Facts first

No code yet. Paste this into Claude:

We're making a 26.7-second vertical film (1080x1920, 30 fps) in this folder with HyperFrames 0.9.2.
Its question: "The Moon just stopped. How long until it hits Earth?"
Facts first, no code. Use NASA's Moon and Earth fact sheets (nssdc.gsfc.nasa.gov/planetary/factsheet).
Treat it as an ideal fall from rest: both bodies fall toward each other, so use Earth's GM plus the Moon's.
Contact is when the surfaces touch. Work out the time and the closing speed at 8 moments a viewer would
care about ("over 1 km a second", "past halfway") and at contact. Work it two ways, a closed form and a
step-by-step integration, and show they agree to the second. Take each kind of number from one table and
name it, and anything else (like how high GPS satellites fly) from one official page. For each distance,
say from what. Write each number, its page and today's date into SOURCES.md.

We worked our fall three ways, and the build fails if they disagree. Then open one source page and check one number yourself. Two of our rules came from this step. A research note gave Neptune's light time from a different distance than NASA's table (4 h 09 min against 4 h 11 min). And NASA's Voyager page gives the distance from Earth, when our film needed it from the Sun.

3. One file for every number

Make timeline.mjs: the one place every number and word of the film lives. 801 frames at 30 fps.
Stop i lands on frame floor(82.5 * (i + 1) + 0.5): a 2.75 s grid. Between stops the clock runs straight.
At each stop it lands on the true value and rests there for 15 frames. The hit is frame 705 (23.5 s).
Under the clock, "KM TO GO": the gap between the surfaces, rounded down to 1,000 km above 10,000 km,
to 100 km above 1,000 km, then to 10 km. It must never read more than is left. Under the band, a chip
with the last fact that came true. From the hit on, the chip says only what is true then, and the last
line answers the title's own question. Set every line break by hand.
Then make build.mjs. With node:assert, check that the clock shows each true value on its frame and holds
it, never runs backwards, and that each row and chip changes on its frame, not before. Nothing may be on
screen under 0.83 s or need over 12 characters a second. No word may sit alone on a line. Compare against
the values in SOURCES.md typed in by hand, not against the same code. Run it until it passes.

The page reads this file to draw. The build reads it to check. Change a number, and the build tells you everything it broke.

4. The page and the 3D scene

Make index.html and scene.mjs for HyperFrames 0.9.2.
- Root div: data-composition-id="main" data-start="0" data-duration="26.7" data-width="1080"
  data-height="1920" data-fps="30" data-no-timeline. A full-size canvas for three.js.
- A solid black band from the top to y 712 holds the words: the title at y 269, the clock, KM TO GO.
  Nothing 3D passes over it. Must-read text stays inside x 65 to 972 and y 269 to 1248, in capitals
  at least 48 px tall (the clock's at least 86).
- Everything local: three.js r187's three.module.js and three.core.js in lib/, Plus Jakarta Sans as a
  woff2 in fonts/, maps in assets/. Download NASA's Blue Marble, Black Marble and cloud maps and the CGI
  Moon Kit from their official pages, and record each file's page, licence and SHA-256 in SOURCES.md.
  No web address anywhere in the code.
- Draw each frame from its time alone, in an "hf-seek" listener: no Math.random, Date, timers or
  requestAnimationFrame. Stars from a fixed seed (mulberry32), as soft round dots. Register the setup
  as a window.__hf.buildReady promise.
- Start with a full-screen magenta div. Hide it only after the scene and every map are built and the
  page has checked every text state against the safe area. If anything fails, throw, so it stays magenta.
- Camera just behind the Moon's far side. Earth grows above its edge at its true angular size from
  the Moon (assert it on every frame) and turns at its true rate. After the hit, pull back to the whole
  Earth, with something still moving to the last frame.
- Add index.motion.json so HyperFrames' check proves each text element shows by 0.1 s and stays in frame.

Watch it as you go: npx --yes hyperframes@0.9.2 preview opens it in your browser, so you can scrub through it.

Why magenta: one of our renders came out 100% magenta from a missing map, and the render still said it succeeded. A magenta frame in your file means the scene didn't build.

5. Sound

Download Kenney's CC0 packs Impact Sounds, Interface Sounds and Sci-fi Sounds from kenney.nl into
assets/sfx/src. My bed track is assets/sfx/src/bed.mp3. Write make-sound.sh with ffmpeg: one tick on the
exact frame of each stop, each a little higher and heavier than the last; the bed thinning toward the end,
its energy above 110 Hz; silence just before the hit; a low hit at 23.5 s. Place each file with its own
<audio> tag, data-start and data-track-index. Then measure, don't guess: each tick within one frame of its
stop and at least 3 dB over the bed before it; the hit at least 3 dB over the bed in a phone's band
(200 Hz to 6 kHz); loudness -16 to -14 LUFS; true peak no higher than -1.5 dB.
I will listen and decide. You can't hear it.

Pick the bed yourself, and read its own page first (see "Honest limits").

6. Render, with every check

Write render.sh (bash, set -euo pipefail, HYPERFRAMES_NO_TELEMETRY=1). Stop at the first failure:
1. node build.mjs
2. grep scene.mjs and timeline.mjs for Math.random, Date, timers, requestAnimationFrame, fetch or web
   addresses, and fail on any
3. npx --yes hyperframes@0.9.2 lint --json, and fail on any error or warning
4. npx --yes hyperframes@0.9.2 check --json --strict --frame-check --caption-zone "x0=0;y0=.65;x1=1;y1=1;severity=error"
5. npx --yes hyperframes@0.9.2 render -o raw.mp4 --workers 1
6. ffmpeg -i raw.mp4 -map_metadata -1 -c copy "$1"
7. node gates.mjs "$1", plus an earlier render if one is given
Then write gates.mjs to check the file: 1080x1920, 30 fps, 801 frames; no black stretch and nothing frozen
for a second; no magenta pixel at the start, at each stop and at the end; Earth at its true size on the
first stops; every tick on its frame; loudness; the clock and words read back with a text reader at every
stop. Given an earlier render, every frame must match it. Run it and fix what fails.

Render twice: bash render.sh a.mp4, then bash render.sh film.mp4 a.mp4. The second run also proves the film comes out the same every time. The render's own "done" proves nothing. The gates do.

7. Look, then post

The rules that matter

Twelve rules, each one from a mistake we made.

  1. A moving thing's trail is always behind it. Our light moved up with its tail pointing the same way, and it looked like it was going backwards.
  2. Words and numbers sit on their own solid band. Planets slid over our clock and its words.
  3. A counter lands exactly on the true value and never shows more than is left. Rounded to hundreds, our "KM TO GO" read 0 one frame before the hit, with 82 km still to go. Now its step shrinks near the end.
  4. A label says only what is true at that moment. Ours kept saying "Just reached Mars" long after Mars.
  5. The payoff answers the title's own question. Our title asked "how long", and our first answer gave a speed. The speed moved to the chip.
  6. Set line breaks by hand. Our title wrapped and left "SUN." alone on a line.
  7. New text is already showing on its own first frame. A fade that started fully clear left the frame a beat landed on with no words.
  8. Every file lives in the film's folder. An early card of ours loaded its animation library and a font from the web. A film must render the same twice.
  9. Next to a giant, open close on the small body, then pull back. At its true size beside the Sun, Earth was a 7-pixel dot among the stars.
  10. The subject never leaves the frame. A camera turn toward the Sun took Earth out of the picture for 4 seconds. Once it is too small to see, give it a marker ring and keep that ring to the payoff.
  11. A held ending keeps moving. A still ending reads as frozen. After the hit we pull back to something whole that keeps moving.
  12. Mix for a phone speaker. Our first bed sat at 55 Hz, which phone speakers barely play. Keep the bed above about 110 Hz, and every tick and the hit at least 3 dB over it, measured in a phone's band.

The checks before posting

Our render.sh stops at the first failure. In order:

  1. Numbers: build.mjs asserts every value before anything renders.
  2. Code: no random values, clocks, timers or web addresses in the scene.
  3. HyperFrames: lint with 0 errors and 0 warnings, then check --strict: layout, contrast, motion, and no text in the bottom 35% of the frame.
  4. The file: 1080 x 1920, 30 fps, exactly 801 frames, the renderer's tags stripped.
  5. The picture: no black stretch, nothing frozen for a second, the ending still moving, not one magenta pixel at the start, the stops or the end.
  6. Sizes: Earth measured at its true size on the frames that matter.
  7. Sound: every tick within one frame of its stop, a silent gap before the hit, loudness -16 to -14 LUFS, true peak at most -1.5 dB.
  8. Read-back: a text reader reads the clock and the words off the video at every stop. Compare the digits: a reader can misread small unit letters next to big digits.
  9. Twice: a second render matches the first, frame for frame.
  10. You: full-size frames, a phone with the sound off, and your ears.

Honest limits


The full kit (4 film templates, all the rules, the checks) is coming soon. Follow @quietcosmos.studio.

Want an animated explainer for your page? DM us.