astro

v0.1.0

Draw celestial bodies in Typst

educationphysics
Repository GitHub
License MIT
Typst Universe astro
Version 0.1.0
Last update 29 Apr 2026

Introduction

astro is a Typst package for drawing diagrams of celestial bodies. It ships pre-defined surfaces for the Sun, all eight planets, the Moon, and Pluto, and exposes a flexible planet function for building custom bodies. All drawing is done on a CeTZ canvas.

Getting Started

Import astro and cetz, open a canvas, and call any of the pre-defined body functions:

#import "@preview/cetz:0.5.0"
#import "@preview/astro:0.1.0": *

#set page(width: auto, height: auto, fill: rgb("#050510"), margin: 20pt)

#cetz.canvas({
import cetz.draw: *
earth(center: (0, 0))
mars(center: (3, 0))
})

All body functions accept the same keyword arguments as planet (see below).

Pre-defined Bodies

The following convenience functions are available after import "@preview/astro:0.1.0": *. Each wraps planet with a realistic surface texture and a default radius scaled to real relative sizes.

Function Default radius Notes
sun(..args) 4.0 Yellow gradient with sunspot groups
mercury(..args) 0.35 Grey with Caloris Basin and craters
venus(..args) 0.87 Cloud-banded golden atmosphere
earth(..args) 1.0 Blue ocean, continents, polar ice caps
moon(..args) 0.27 Maria, craters; supports all phase values
mars(..args) 0.53 Rust surface, Valles Marineris, polar caps
jupiter(..args) 2.5 Belt/zone banding, Great Red Spot
saturn(..args) 2.0 Full ring system enabled by default
uranus(..args) 1.5 Thin ring enabled by default
neptune(..args) 1.45 Thin ring enabled by default, Great Dark Spot
pluto(..args) 0.5 Tombaugh Regio heart feature

The example below draws all bodies side by side at their default radii:

#import "@preview/cetz:0.5.0"
#import "@preview/astro:0.1.0": *

#set page(width: auto, height: auto, fill: rgb("#050510"))

#cetz.canvas({
import cetz.draw: *
let gap = 3
let x = 0
for (body, fn) in (
("sun", sun), ("mercury", mercury), ("venus", venus),
("earth", earth), ("mars", mars), ("jupiter", jupiter),
("saturn", saturn), ("uranus", uranus), ("neptune", neptune),
("pluto", pluto),
) {
fn(center: (x, 0))
x = x + dr.at(body) + gap
}
})

Default radii and display names are also exported as dictionaries for layout arithmetic:

#import "@preview/astro:0.1.0": dr, dn

// dr.at("earth") == 1.0 (radius in canvas units)
// dn.at("earth") == "Earth"

The planet Function

All pre-defined bodies delegate to planet. You can call it directly for full control or to draw a custom body.

planet(
center: (0, 0), // (x, y) position on the canvas
radius: 1, // radius in canvas units
surface: none, // surface key (string) or none for generic_surface
tilt: 0, // axial tilt in degrees
phase: "full", // illumination phase (see Moon Phases)
rings: false, // full Saturn-style ring system
ring: none, // thin ring at given semi-major axis (number or none)
color: blue, // fallback colour when surface is none
name: "", // label drawn below the body (empty → no label)
)
Parameter Description
center Canvas coordinate (x, y) for the centre of the body.
radius Radius in canvas units. All surface detail and ring geometry scales with this value.
surface Key into the built-in surface map ("sun", "earth", "moon", etc.) or none to use generic_surface(color: color).
tilt Axial tilt applied to the surface and rings, in degrees.
phase Illumination phase. Applies a shadow overlay. See Moon Phases for valid values.
rings When true, draws a full Saturn-style elliptical ring system behind and in front of the body.
ring When set to a number, draws a single thin ring whose semi-major axis equals the value (in the body’s local coordinate space after radius scaling).
color Fill colour used by generic_surface when no named surface is given.
name Text label rendered below the body. Pass "" to suppress the label.

The example below places the Earth and Moon on a dashed orbit line:

#import "@preview/cetz:0.5.0"
#import "@preview/astro:0.1.0": *

#set page(width: auto, height: auto, fill: rgb("#050510"), margin: 20pt)

#cetz.canvas({
import cetz.draw: *
let center-e = (0, 0)
let center-m = (3.5, 3)
let r = cetz.vector.dist(center-e, center-m)
circle(center-e, radius: r, stroke: (dash: "dashed", paint: rgb("#fff")))
earth(center: center-e)
moon(center: center-m)
})

Moon Phases

The phase parameter accepts the following string values. The same shadow overlay works on any body, not just the Moon.

Value Description
"full" No shadow — fully illuminated.
"new" Fully shadowed.
"first crescent" Thin crescent, right side lit.
"last crescent" Thin crescent, left side lit.
"first half" Right half lit (first quarter).
"last half" Left half lit (third quarter).
"waxing gibbous" More than half lit, right side.
"waning gibbous" More than half lit, left side.
"top half" Top half lit.
"bottom half" Bottom half lit.
#import "@preview/cetz:0.5.0"
#import "@preview/astro:0.1.0": *

#set page(width: auto, height: auto, fill: rgb("#050510"), margin: 20pt)

#cetz.canvas({
import cetz.draw: *
let x = 0
let r = dr.at("moon")
for phase in (
"new", "first crescent", "first half", "waxing gibbous",
"full", "waning gibbous", "last half", "last crescent",
) {
moon(center: (x, 0), phase: phase, name: phase)
x = x + 2 * r + 1
}
})

Custom Bodies

Pass surface: none and a color to draw a plain coloured sphere, or supply a custom surface key that maps to a function you register via the surfaces dictionary.

#cetz.canvas({
import cetz.draw: *
// Generic teal body, no label
planet(center: (0, 0), radius: 1.2, color: teal)

// Custom axial tilt on a named body
earth(center: (4, 0), tilt: 23, name: "Earth (tilted)")
})

Solar System Layout

For a more complete scene, the solar_system example arranges all bodies on randomised orbits:

#import "@preview/cetz:0.5.0"
#import "@preview/suiji:0.5.1": *
#import "@preview/astro:0.1.0": *

#set page(width: auto, height: auto, fill: rgb("#050510"), margin: 20pt)

#cetz.canvas({
import cetz.draw: *
let center = (0, 0)
let gap = 3
let x = 0
let rng = gen-rng-f(4)
for (body, fn) in (
("sun", sun), ("mercury", mercury), ("venus", venus),
("earth", earth), ("mars", mars), ("jupiter", jupiter),
("saturn", saturn), ("uranus", uranus), ("neptune", neptune),
("pluto", pluto),
) {
let (rng2, theta) = uniform-f(rng, low: 0.0, high: 2 * calc.pi)
rng = rng2
let a = center.at(0) + x * calc.cos(theta)
let b = center.at(1) + x * calc.sin(theta)
circle((0, 0), radius: x, stroke: (paint: rgb("#fff")))
fn(center: (a, b), name: "")
x = x + dr.at(body) + gap
}
})
Made with Manifesto from Typst Universe