render

package
v0.32.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package render holds the visual constant tables shared across screens — body color palette, UI tier colors for status / nodes / trajectory.

v0.7.1+: per-body color lives on bodies.CelestialBody.Color (a hex string in systems/*.json). The hardcoded bodyPalette table here stays as a backward-compat fallback for callers / tests that construct CelestialBody literals without a Color field; v0.8 will drop the table once v0.7.1+ saves are in the wild.

Index

Constants

View Source
const (
	ColorEarthOcean        = lipgloss.Color("#1F5F94") // deeper saturated blue
	ColorEarthLand         = lipgloss.Color("#5C8C4A") // temperate green (default land)
	ColorEarthLandTropical = lipgloss.Color("#3E7A35") // tropical jungle, deeper / saturated
	ColorEarthLandBoreal   = lipgloss.Color("#6B7A56") // boreal taiga, browner / drabber
	ColorEarthDesert       = lipgloss.Color("#C8A06A") // brighter Sahara tan
	ColorEarthIce          = lipgloss.Color("#E5EEF6") // slightly bluer ice
	ColorEarthCloud        = lipgloss.Color("#F2F5FA")
	ColorEarthAtmosphere   = lipgloss.Color("#6FA8D6") // atmospheric limb tint
)

Earth-surface palette. v0.8.5.7 polish pass nudges the colors toward the iconic "blue marble" look — deeper, slightly more saturated ocean; biome-split land (boreal taiga / temperate / tropical jungle) so high-latitude continents read browner and equatorial ones read deeper green; warmer Sahara-tan desert; bluer ice. ColorEarthLand is the temperate default — code shifts to boreal / tropical based on the pixel's latitude when rendering. The existing public palette entry (#5BB3FF) stays the HUD-label face.

View Source
const (
	// Io — sulfur-yellow base, dark patera deposits, fresh-flow orange.
	ColorIoBase   = lipgloss.Color("#E8D940") // sulfurous yellow (matches palette entry)
	ColorIoPatera = lipgloss.Color("#7A4A20") // dark volcanic deposits
	ColorIoFresh  = lipgloss.Color("#E07530") // fresh flow orange

	// Europa — bright water ice base with dark linear cracks (lineae).
	ColorEuropaIce  = lipgloss.Color("#E5DBC6") // pale ice
	ColorEuropaLine = lipgloss.Color("#9A6F4A") // brown linea (cryomagma stains)

	// Ganymede — bright young grooved terrain vs. dark ancient terrain.
	ColorGanymedeBright = lipgloss.Color("#C8B498") // grooved terrain
	ColorGanymedeDark   = lipgloss.Color("#6E5A3E") // ancient cratered terrain
	ColorGanymedeRay    = lipgloss.Color("#E0D2B0") // fresh impact ejecta

	// Callisto — uniformly dark, heavily cratered, with bright rays.
	ColorCallistoBase   = lipgloss.Color("#5C4A36") // dark base
	ColorCallistoCrater = lipgloss.Color("#9A8260") // bright crater rim / ray
)

Galilean-moon palette. Retained as named colors; the per-moon surfaces (Io's paterae, Europa's lineae, Ganymede's regiones, Callisto's craters) are now data-driven from sol.json (ADR 0024 PR4), replacing the Io/Europa/Ganymede/CallistoPixelColor shaders. The shared orthographic projection (projectPixelToLatLon) below outlived them — every texture kind uses it.

View Source
const (
	ColorUranusBase = lipgloss.Color("#A8D8E0") // pale cyan (matches palette)
	ColorUranusBand = lipgloss.Color("#8FC4D0") // very subtle banding
	ColorUranusPole = lipgloss.Color("#C0E0E8") // brighter polar haze (Uranus's pole-on view)

	ColorNeptuneBase  = lipgloss.Color("#3A6FB8") // deep methane blue
	ColorNeptuneBand  = lipgloss.Color("#2A5494") // darker band
	ColorNeptuneCloud = lipgloss.Color("#7AA4D8") // bright cirrus / scooter
	ColorNeptuneSpot  = lipgloss.Color("#1F3A6E") // Great Dark Spot
)

Ice-giant palette. Retained as named colors; the banded disks (and Neptune's Great Dark Spot) are now data-driven from sol.json (ADR 0024 PR4), replacing the Uranus/NeptunePixelColor shaders.

View Source
const (
	ColorJupiterZone = lipgloss.Color("#D7B98C") // bright zone (warm)
	ColorJupiterBelt = lipgloss.Color("#8B6240") // dark belt (warm)
	ColorJupiterPole = lipgloss.Color("#7A6450") // muted polar haze
	ColorJupiterGRS  = lipgloss.Color("#A03A28") // Great Red Spot
)

Jupiter-surface palette. Retained as named colors; the banded cloud texture + Great Red Spot are now data-driven from sol.json (ADR 0024 PR4), replacing the JupiterPixelColor shader.

View Source
const (
	ColorMarsRust   = lipgloss.Color("#B7553A") // base regolith
	ColorMarsDark   = lipgloss.Color("#7A3422") // dark albedo features
	ColorMarsBright = lipgloss.Color("#D9A07A") // bright high-albedo (Arabia)
	ColorMarsIce    = lipgloss.Color("#F0E8E0") // polar caps (CO₂ frost)
)

Mars-surface palette. Retained as named colors; the Mars surface texture (dark/bright albedo features + polar caps) is now data-driven from sol.json (ADR 0024 PR4), replacing the MarsPixelColor shader.

View Source
const (
	ColorMoonHighland = lipgloss.Color("#BFB8AA") // warm grey-tan regolith
	ColorMoonMare     = lipgloss.Color("#4A4A55") // darker basalt for contrast
	ColorMoonRay      = lipgloss.Color("#EFEAE0") // bright ejecta, slightly warm
)

Lunar-surface palette. Retained as named colors for the HUD/label chain; the Moon's surface texture (maria + bright rayed craters) is now data-driven from sol.json (ADR 0024 PR4) rather than the MoonPixelColor Go shader this file used to hold.

View Source
const (
	ColorNavballSky    = lipgloss.Color("#2E74C0") // upper-hemisphere sky (KSP blue)
	ColorNavballGround = lipgloss.Color("#9C6B3F") // lower-hemisphere ground (KSP tan-brown)
	ColorNavballGrid   = lipgloss.Color("#C8C8C8") // structural labels (compass ticks)

	// Limb shading — darker hemisphere tints used on cells at the
	// disk edge (few in-disk dots) so the ball reads as a sphere
	// with depth, not a flat color disk. Roughly 60% brightness of
	// the parent hemisphere color.
	ColorNavballSkyEdge    = lipgloss.Color("#1C4A82") // darker sky for limb cells
	ColorNavballGroundEdge = lipgloss.Color("#5E3F26") // darker ground for limb cells

	// Horizon band — bright pale tan used on cells where sky/ground
	// dot counts are nearly balanced (the cell straddles the
	// equator). KSP draws a crisp horizon line; the brightness here
	// makes it pop against both the blue sky and brown ground.
	ColorNavballHorizon = lipgloss.Color("#E6D2A0") // bright horizon line

	// Grid tints — slightly brighter versions of each hemisphere,
	// used on cells whose dots fall on or near a 30° parallel /
	// meridian. Keeps the grid in-hemisphere (not white) so the
	// disk doesn't wash out — the 357937f bug was a single bright
	// grid color winning ties and turning the whole disk white +
	// flickery. These stay tonally adjacent to their hemisphere.
	ColorNavballSkyGrid    = lipgloss.Color("#5AA0E0") // brighter sky for grid-line cells
	ColorNavballGroundGrid = lipgloss.Color("#C2925A") // brighter ground for grid-line cells

	// Marker colors. Prograde / retrograde mirror KSP's yellow; normal
	// vectors are pink (KSP magenta-ish); radial markers are cyan;
	// target markers are pink-purple to read distinctly against the
	// orbit-frame markers when both render in target mode.
	ColorNavballMarkerPrograde  = lipgloss.Color("#E0D040") // prograde / retrograde yellow
	ColorNavballMarkerNormal    = lipgloss.Color("#D08CC8") // normal+ / normal- pink
	ColorNavballMarkerRadial    = lipgloss.Color("#5CC8D0") // radial+ / radial- cyan
	ColorNavballMarkerTarget    = lipgloss.Color("#C880E8") // target / anti-target purple
	ColorNavballMarkerNoseFront = lipgloss.Color("#FFFFFF") // craft nose, front hemisphere
)

Navball palette. v0.9.6-polish retuned this toward KSP's navball: a saturated sky-blue upper hemisphere over a tan/brown lower hemisphere (KSP uses brown ground, not the classic ADI orange), with a bright pale-tan horizon band so the equator reads as an explicit drawn line rather than just the blue/brown boundary.

View Source
const (
	ColorSaturnRingC = lipgloss.Color("#806E50") // C ring, dim grey-tan
	ColorSaturnRingB = lipgloss.Color("#E8D9A8") // B ring, brightest pale gold
	ColorSaturnRingA = lipgloss.Color("#B89968") // A ring, medium ochre
	ColorSaturnRingF = lipgloss.Color("#F0E0B8") // F ring, thin bright thread
)

Saturn ring palette. v0.8.5.7+ — the C / B / A bands have different brightness (B is brightest, A medium, C dim), and the thin F ring sits just outside A as a bright accent. The Cassini Division between B and A is a visible gap (no band drawn), and the F ring's narrow width is the "thread loose around Saturn" look from Cassini photos.

View Source
const (
	ColorSaturnZone = lipgloss.Color("#E8D9A8") // bright zone (warm pale gold)
	ColorSaturnBelt = lipgloss.Color("#B89968") // dark belt (muted ochre)
	ColorSaturnPole = lipgloss.Color("#9A8458") // muted polar haze, slightly darker
	ColorSaturnSpot = lipgloss.Color("#D9B070") // hexagonal storm + occasional bright ovals
)

Saturn-surface palette. Retained as named colors; the banded cloud texture (incl. the polar-hexagon band) is now data-driven from sol.json (ADR 0024 PR4), replacing the SaturnPixelColor shader.

View Source
const (
	ColorSunCore    = lipgloss.Color("#FFF6C8") // bright yellow-white center
	ColorSunSurface = lipgloss.Color("#FFD050") // saturated solar yellow
	ColorSunLimb    = lipgloss.Color("#E89020") // darker orange-yellow limb
	ColorSunCorona  = lipgloss.Color("#FFE070") // faint corona halo
	ColorSunSpot    = lipgloss.Color("#A06030") // dark sunspot
)

Solar palette. Retained as named colors; the Sun's surface (limb darkening + sunspots) is now data-driven from sol.json via the generic `star` texture kind (ADR 0024 PR4), replacing the SunPixelColor shader. ColorSunCorona is still used by the corona halo renderer.

View Source
const AtmosphereMinHaloPx = 2

AtmosphereMinHaloPx is the minimum pixel gap between the body's rendered edge and the haze ring. The physical atmosphere shell (cutoff + scale-height) is only a couple percent of a planet's radius, so without this floor the haze ring overlaps the body's own outline at every reasonable zoom and is invisible. With the floor the haze always reads as a thin halo just outside the disk.

View Source
const BodyTextureMinRadius = 12

BodyTextureMinRadius is the minimum body pixel radius at which the per-pixel body textures render. Below this, the body falls back to a solid-color disk because (a) the disk has too few cells to read a continent / mare shape, and (b) the textured fill is more expensive per pixel than a plain disk.

View Source
const EarthCenterLonEpoch = -30.0

EarthCenterLonEpoch is the sub-observer longitude at J2000 — the longitude that sat dead-center on the visible hemisphere when Earth was rendered statically (v0.7.6 — v0.8.4). Now the epoch reference for SubObserverLongitudeDeg ("Earth at sim-time-zero looks the same as it did pre-rotation"). v0.8.5+ threads sim-time rotation through the lon0 parameter; this constant is just the epoch offset.

View Source
const EarthTextureMinRadius = BodyTextureMinRadius

EarthTextureMinRadius is the legacy name for BodyTextureMinRadius, preserved for callers / tests that referenced the Earth-specific constant. Drop in v0.8 alongside other v0.7 cleanup.

Variables

View Source
var (
	ColorAlert        = lipgloss.Color("#FF5F5F") // hard errors, peri-below-surface
	ColorWarning      = lipgloss.Color("#FFAF00") // warp clamps, near-collision
	ColorPlannedNode  = lipgloss.Color("#5FD7FF") // maneuver-node markers
	ColorTrajectory   = lipgloss.Color("#FFFFFF") // fallback trajectory preview
	ColorCurrentOrbit = lipgloss.Color("#A8B8C8") // craft's live Keplerian ellipse — pale slate, distinct from any body palette and from maneuver-leg colors
	ColorBodyOrbit    = lipgloss.Color("#6E6E6E") // heliocentric body orbits — dim grey backdrop (KSP-aligned: body orbits are quiet so craft / maneuver layers pop). v0.10.6+
	ColorCraftMarker  = lipgloss.Color("#FFD93D") // craft icon when zoomed out (orbit too small to render) — saturated yellow distinct from Sun gold-white, amber leg, and every body color
	ColorForeignSOI   = lipgloss.Color("#D75FFF") // post-SOI-crossing trajectory segments
	ColorDim          = lipgloss.Color("#5F5F5F") // background / inactive
	ColorFlame        = lipgloss.Color("#FF8C42") // engine-firing flame trail (warm orange — distinct from craftmarker yellow)
	ColorTarget       = lipgloss.Color("#3DDC84") // active TARGET craft + its orbit — vivid green, matches DOCK READY callout, distinct from craft yellow / current-orbit slate / planned-node cyan
	// v0.11.5: RCS puffs paint as a bright-white origin pixel + dim-grey
	// tip in both OrbitView and LaunchView, so a small cold thruster
	// puff reads visually distinct from the hot fuel-coloured main flame.
	// Pre-v0.11.5 puffs used amber `ColorWarning` + orange `ColorFlame`
	// which clashed with the new fuel-type flame palette.
	ColorRCSPuffOrigin = lipgloss.Color("#FFFFFF") // bright white — thruster ignition pixel
	ColorRCSPuffTip    = ColorDim                  // dim grey — fading trail tip
	// ColorCommLink is the CommNet relay sightline (ADR 0027 / C2-7) drawn
	// from the active probe through any relay hops to a ground station —
	// bright teal, drawn as a sparse dotted beam (step 3) so it reads as a
	// signal link distinct from the slate live orbit, amber maneuver legs,
	// and orchid SOI arcs.
	ColorCommLink = lipgloss.Color("#34E2D0")
)

UI tier colors — used for non-body UI elements that need consistent semantic colors across screens. Independent of the body palette so editing one doesn't shift the other.

Far-side rendering convention (v0.10.6+): the orbit-trace draw path renders the far-side arc (depth < 0 relative to the orbit's primary, under the canvas's active basis) at stride*2 stipple in the **same hue** as the near-side arc. Terminals can't do alpha, so per-sample stride flips are the lossless equivalent of KSP's dim-the-back-arc rendering. Callers don't pick a separate "back" colour; the canvas helper (DrawEllipseOffsetFarSideDashed) handles it automatically. ColorBodyOrbit is the one new entry the convention surfaced — body orbits had no dedicated colour before v0.10.6, defaulting to whatever Plot wrote (terminal default).

View Source
var (
	ColorMarkerApoapsis        = lipgloss.Color("#5FC9E8") // sky cyan — apoapsis ▲
	ColorMarkerPeriapsis       = lipgloss.Color("#FFAF00") // amber — periapsis ▼
	ColorMarkerAscendingNode   = lipgloss.Color("#5FD75F") // green — ascending node ◇
	ColorMarkerDescendingNode  = lipgloss.Color("#AF5FFF") // violet — descending node ◆
	ColorMarkerPerilune        = lipgloss.Color("#FFAF00") // amber — perilune ⊕ (the periapsis within a pass SOI; shares the apsis amber)
	ColorMarkerClosestApproach = lipgloss.Color("#FF5FAF") // hot pink — craft-to-craft closest approach ✕
	ColorMarkerSOIEntry        = ColorForeignSOI           // orchid — SOI entry ▷ (shares the foreign-SOI arc hue it brackets, like Perilune shares the apsis amber)
	ColorMarkerSOIExit         = ColorForeignSOI           // orchid — SOI exit ◁
)

Orbital-marker type colours (ADR 0020). The unified marker convention is: one glyph rune per marker type, colour encodes *type*, brightness encodes *state* (see internal/render/markers.go). These are the fixed per-type hues; the Maneuver marker is the one documented exception whose colour is positional (per post-burn leg, ManeuverSegmentColor) rather than drawn from this table. Chosen to stay mutually distinct and distinct from the orbit/leg/body palettes already in play.

View Source
var (
	CameraDirTop    = Vec3{0, 0, +1}
	CameraDirBottom = Vec3{0, 0, -1}
	CameraDirRight  = Vec3{+1, 0, 0}
	CameraDirLeft   = Vec3{-1, 0, 0}
)

CameraDirTop is the world-frame body-to-camera direction for the canvas's "top" view (looking down system Z-axis from +Z). Defined here so the tui orbit screen can hand the texture pipeline a stable camera direction without round-tripping through sim.ViewMode and avoiding an import cycle.

Functions

func AtmosphereHazeColor added in v0.8.4

func AtmosphereHazeColor(b bodies.CelestialBody) lipgloss.Color

AtmosphereHazeColor returns the haze tint for the body. Falls back to ColorFor(b) when Atmosphere.Color is empty so a body that declares an atmosphere without an explicit haze color still gets a reasonable halo.

func AtmosphereOuterMeters added in v0.8.4

func AtmosphereOuterMeters(b bodies.CelestialBody) float64

AtmosphereOuterMeters returns the distance from the body's center at which the haze ring is drawn. The shell sits at (R_body + CutoffAltitude + ScaleHeight) so it's slightly above the drag-cutoff altitude — visually marks "this is where the atmosphere fades to vacuum." Returns 0 when no atmosphere.

func AtmosphereOuterPx added in v0.8.4

func AtmosphereOuterPx(b bodies.CelestialBody, scale float64, bodyPxRadius int) int

AtmosphereOuterPx projects AtmosphereOuterMeters into canvas pixels at the given scale (px/m), then floors the result to bodyPxRadius + AtmosphereMinHaloPx so the ring is always at least a couple pixels outside the body's rendered edge. Returns 0 when haze shouldn't render (no atmosphere or zero scale).

func AtmosphereVisible added in v0.8.4

func AtmosphereVisible(b bodies.CelestialBody, pxRadius int) bool

AtmosphereVisible reports whether the haze ring should render. False for airless bodies. v0.8.4 originally suppressed haze past 20 px body radius; with the AtmosphereMinHaloPx floor the ring stays readable as a thin halo at any zoom, so that suppression is no longer needed.

func BodyHasTexture added in v0.7.3

func BodyHasTexture(b bodies.CelestialBody, pxRadius int) bool

BodyHasTexture reports whether TextureFor would return non-nil. Convenience wrapper for callers that just need the boolean (e.g. "should I suppress the body-identity glyph?"). The sub-observer point doesn't affect the gate, so the boolean form omits it.

func BodyRingBasisWorld added in v0.8.5

func BodyRingBasisWorld(b bodies.CelestialBody) (Vec3, Vec3)

BodyRingBasisWorld returns two orthonormal basis vectors that span the body's equatorial plane (perpendicular to its spin axis), expressed in the world inertial frame. Caller can sample a circular ring as

ringPoint(θ) = R·(ê1·cos(θ) + ê2·sin(θ))

and project each sample through the canvas to draw the ring as an ellipse that correctly foreshortens for the current view. Built directly from the spin-axis vector via Gram-Schmidt against world +X (or +Y when degenerate), so it picks up AxialAzimuth automatically. At AxialTilt = 90° aligned with world +X the convention degenerates and we fall back to (ê1, ê2) = (world +Y, world +Z).

func BodyRings added in v0.5.11

func BodyRings(bodyID string) (innerR, outerR float64, ok bool)

BodyRings returns the inner and outer radii of a body's ring system overall (union of all bands), or ok=false if the body has no renderable rings. Backward-compat wrapper for callers that just need the outer extent (e.g. "should I render rings at the current zoom?"). New code should prefer BodyRingBands for the per-band detail.

func ColorFor

func ColorFor(b bodies.CelestialBody) lipgloss.Color

ColorFor returns the palette color for a body. Resolution order:

  1. theme.json `bodies` override (v0.7.2+) keyed by body ID.
  2. b.Color (per-body JSON field, v0.7.1+) when non-empty.
  3. bodyPalette table (legacy hardcoded source-of-truth).
  4. StellarTint by temperature for stars without an explicit entry.
  5. Per-bodyType default.
  6. ColorTrajectory for unrecognised types.

func EclipseFactor added in v0.9.6

func EclipseFactor(bodyPos, occPos Vec3, bodyR, occR, sunR float64) float64

EclipseFactor returns a global dim factor in [umbraFloor, 1] for a body centered at bodyPos with radius bodyR, whose occluder (its parent / the larger sunward body) is centered at occPos with radius occR, with the Sun of radius sunR at the origin. 1 = fully lit; umbraFloor = deep total umbra; the penumbra band interpolates with a smoothstep. Degenerate inputs (occluder at the Sun, body sunward of the occluder) return 1 — no eclipse — so planets, whose "parent" is the star itself, are never spuriously shadowed.

func GlyphFor added in v0.5.12

func GlyphFor(b bodies.CelestialBody) rune

GlyphFor returns the body-identity Unicode glyph for the given body. Used as a single-cell overlay on top of the body's drawille disk so different body types read distinctly even at small pixel- radius. v0.5.12+.

  • Star → ☉ (sun symbol)
  • Gas giant (radius > 20 000 km) → ◉ (fisheye)
  • Terrestrial planet → ● (filled circle)
  • Moon → ○ (open circle)

Returns 0 (zero rune) when no overlay is appropriate (e.g. system primary already has a ring+dot draw style and shouldn't be double-glyphed).

func LoadTheme added in v0.7.2

func LoadTheme() (*Theme, []LoadWarning, error)

LoadTheme reads the user theme overlay (if present) and applies it to package-level state. UI overrides mutate the Color* vars in place; body overrides land in bodyOverrides for ColorFor to consult. Returns the parsed Theme (nil if no file) plus any LoadWarnings (parse / I/O failures).

Idempotent — calling twice with the same file produces the same final state. Pre-existing UI defaults are restored before applying overrides so a second call with a smaller theme doesn't keep stale overrides from the first.

func ManeuverSegmentColor added in v0.6.1

func ManeuverSegmentColor(n int) lipgloss.Color

ManeuverSegmentColor returns the color for the post-maneuver-N orbit leg. N=0 is the orbit immediately after the first planted burn fires; N=1 the orbit after the second; etc. Wraps around the palette table.

func MarkerColor added in v0.18.0

func MarkerColor(t MarkerType, state MarkerState, base lipgloss.Color) lipgloss.Color

MarkerColor resolves the rendered colour for a marker from its type and state (ADR 0020 decision B). Colour encodes type; state adjusts brightness: MarkerAlarm overrides to ColorAlert (bright red, "what's wrong" beats "what it is"); MarkerCounterfactual dims the type colour; MarkerNominal uses it as-is.

base is consulted only for MarkerManeuver — the one type whose colour is positional (it matches the post-burn leg, ManeuverSegmentColor, an ADR-0006-era design) rather than fixed by type. For every other type base is ignored.

func MarkerGlyph added in v0.18.0

func MarkerGlyph(t MarkerType) rune

MarkerGlyph returns the font-safe geometric glyph rune for a marker type (ADR 0020 decision A). The set is deliberately basic so it renders in virtually any monospace font (honouring the v0.11.4 palette caution against exotic Unicode that shows as tofu). An unknown type falls back to '?' rather than 0, so a mis-wired caller draws something visible instead of silently nothing.

func NavballString(cols, rows int, subLatDeg, subLonDeg float64, markers []NavballMarker) string

NavballString paints a navball into a cols×rows cell grid as a multi-line lipgloss-styled string. The sphere is rendered with a horizon split (sky upper, ground lower) plus a lat/lon grid at 30° spacing, and any front-hemisphere markers are overlaid as glyphs. (subLatDeg, subLonDeg) is the sub-observer point — the (lat, lon) on the sphere that sits at the visible disk centre. For the navball this corresponds to the craft's nose direction expressed in the ball's reference frame.

markers may be nil. Each marker is projected via projectLatLonToPixel; back-hemisphere markers render dimmed and markers that project outside the disk are clamped to the rim (KSP-style edge pinning) rather than skipped, so a marker never blinks on/off at the limb.

v0.9.5: braille sub-pixel rendering — each terminal cell contains a 2×4 grid of braille dots (square in physical screen space, since terminal cells are ≈1×2). For an N-cell-wide × M-cell-tall region the dot grid is 2N×4M, with disk pxRadius = min(N, 2M) so the disk is genuinely circular on screen. Per cell: sample all 8 sub-pixels, build the braille pattern from in-disk dots, and color the cell with the dominant texture (grid wins ties so lines stay visible).

Per-pixel (dx, dy) → (lat, lon) projection reuses projectPixelToLatLon; only the painter loop differs from the body-rendering path.

func Shade added in v0.9.6

func Shade(c lipgloss.Color, f float64) lipgloss.Color

Shade multiplies an "#RRGGBB" lipgloss color's RGB channels by f (clamped to [0,1]) and returns the darkened color. Malformed or non-hex input is returned unchanged — render-path code must never panic, and an un-darkened pixel is a far better failure mode than a crash mid-frame.

func StellarTint

func StellarTint(tempK float64) lipgloss.Color

StellarTint maps a star's effective temperature (Kelvin) to a rough visible-light color. Used for non-Sol stars that don't have a hand-picked entry in bodyPalette. Banding is coarse — five buckets covering M-dwarf through O-class.

func Style

Style returns a lipgloss.Style with the body's color as the foreground. Convenience wrapper for HUD / body-info text — call Style(b).Render("Earth") to print a colored label.

func SubObserverLongitudeDeg added in v0.8.5

func SubObserverLongitudeDeg(b bodies.CelestialBody, simTime time.Time) float64

SubObserverLongitudeDeg is the v0.8.5 entry point — lon at the visible centre for an equator-on view. Retained as a thin wrapper over SubObserverPointDeg(camDir = CameraDirRight) so callers that only care about longitude (tests, simple debug paths) keep working. New view-aware code should call SubObserverPointDeg.

func SubObserverPointDeg added in v0.8.5

func SubObserverPointDeg(b bodies.CelestialBody, simTime time.Time, camDir Vec3, primMerDir Vec3) (subLatDeg, subLonDeg float64)

SubObserverPointDeg returns the (lat, lon) on body b — in body- fixed degrees — that sits at the visible disk center for an observer at the world-frame direction camDir, at simTime.

camDir is the unit vector pointing from the body to the camera in the world inertial frame; the canvas's "top" view passes CameraDirTop, etc. The orbit-flat view passes the active orbit-plane normal.

primMerDir is an optional override for the body's prime meridian direction in the world frame. When non-zero, it supersedes the rotation-phase model — used for tidally-locked moons, where the prime meridian (lon=0) always points at the parent body. Caller passes the unit vector from the moon toward its parent at simTime; the function projects this onto the equatorial plane to get the body x-axis. When primMerDir is the zero vector, the function falls back to the inertial- frame rotation-phase model (correct for free bodies and for tidally-locked bodies whose parent isn't tracked here).

The body's spin axis is modelled as lying in the world X-Z plane (azimuth 0 in the inertial frame): n = (sin(tilt), 0, cos(tilt)). v0.8.5.7+ replaces the v0.8.5 "always equator-on" lon0 formulation with full view-aware geometry. ViewTop on a tilted body now reveals polar regions; Uranus's 97° tilt makes it roll pole-on along its orbit; tidally-locked moons keep the same face pointed at the parent regardless of orbit phase.

func WorldToBodyFixed added in v0.11.0

func WorldToBodyFixed(b bodies.CelestialBody, vWorld Vec3, simTime time.Time) (latDeg, lonDeg float64)

WorldToBodyFixed is the inverse of BodyFixedToWorld: given a unit world-frame vector representing a point on (or above) the body at simTime, recover the body-fixed (lat, lon) of that direction. v0.11.0+. v0.11.2+ (ADR 0003): rewritten alongside BodyFixedToWorld as a projection onto the body-fixed basis at simTime; view- independent.

Caller is responsible for normalising the input (only the direction matters) and for computing altitude separately as |R| − mean radius.

Algebra: BodyFixedToWorld writes v = cosφ·cosλ·bodyX + cosφ·sinλ·bodyY

  • sinφ·n with (bodyX, bodyY, n) orthonormal. So sinφ = v · n cosφ·cosλ = v · bodyX cosφ·sinλ = v · bodyY λ = atan2(v·bodyY, v·bodyX)

Output longitude is wrapped to (-180, 180].

Types

type BodyTexture added in v0.7.3

type BodyTexture func(dx, dy, pxRadius int) lipgloss.Color

BodyTexture returns the per-pixel surface color for a textured body. Implementations live in body-specific files (earth.go, moon.go, ...) and assume the caller has already gated the pixel to inside the disk; for points within 1 px of the limb the projection clamps cleanly. The closure returned by TextureFor bakes the body's sub-observer longitude (lon0) in, so the canvas painter doesn't need to thread sim time through every pixel call.

func TextureFor added in v0.7.3

func TextureFor(b bodies.CelestialBody, pxRadius int, subLatDeg, subLonDeg, screenUpX, screenUpY float64, light *SolarLight) BodyTexture

TextureFor returns the per-pixel texture function for the given body, or nil if the body should render as a solid disk (small radius, unsupported body). The dispatch hook for body-specific surface rendering — Mars caps, Jupiter banding, Saturn cloud bands plug in via additional cases here.

v0.8.5.7+ takes the full sub-observer point (subLatDeg, subLonDeg) and bakes it into the returned closure, so the canvas painter calls a 3-arg BodyTexture without threading view + sim time through each pixel. Callers should compute the sub-observer point via SubObserverPointDeg(b, simTime, camDir) once per body per frame.

v0.9.6+ takes an optional light: when non-nil and the body is not the Sun, every pixel is darkened by light.FactorAt to paint the day/night terminator (+ eclipse dimming). nil disables shading and is the back-compat path (e.g. BodyHasTexture). The Sun is always exempt — it is the light source and keeps its own limb darkening.

v0.11.2+ (ADR 0003): adds (screenUpX, screenUpY) — the canvas-frame unit vector in the direction body-local-north at the sub-observer projects on screen. Caller computes it once per body per frame from BodyRotationAxisWorld, the canvas basis, and camDir; for the pre-v0.11.2 "north is canvas-up" assumption pass (0, 1).

type LoadWarning added in v0.7.2

type LoadWarning struct {
	Path string
	Err  error
}

LoadWarning is the render-package counterpart to bodies.LoadWarning for theme-overlay parse failures. Defined locally so this package doesn't import bodies just to reuse the type.

func (LoadWarning) Error added in v0.7.2

func (w LoadWarning) Error() string

type MarkerState added in v0.18.0

type MarkerState int

MarkerState modifies a marker's brightness/variant to convey what is happening to it (ADR 0020 B): nominal renders at full type colour, a counterfactual (e.g. the no-burn arc of ADR 0019's dual-arc) dims, and an alarm (e.g. a perilune below the surface → impact) overrides to the bright alert red regardless of type.

const (
	MarkerNominal MarkerState = iota
	MarkerCounterfactual
	MarkerAlarm
)

type MarkerType added in v0.18.0

type MarkerType int

MarkerType enumerates the orbital-marker vocabulary shared across the orbit screen (ADR 0020). Every marker renders as a single colored glyph stamped at its projected point via Canvas.SetCellOverlayColored — color encodes the type, brightness/variant encodes the state (MarkerState).

const (
	MarkerApoapsis MarkerType = iota
	MarkerPeriapsis
	MarkerAscendingNode
	MarkerDescendingNode
	MarkerPerilune // periapsis within an upcoming SOI Pass (ADR 0019)
	MarkerClosestApproach
	MarkerManeuver // per-node colour exception — see MarkerColor
	MarkerSOIEntry // SOI Pass arc crosses INTO the pass Body's SOI Ring (ADR 0021 C)
	MarkerSOIExit  // SOI Pass arc crosses back OUT of the SOI Ring
)
type NavballMarker struct {
	LatDeg, LonDeg float64
	Glyph          rune
	Color          lipgloss.Color
}

NavballMarker is a glyph drawn at a fixed (lat, lon) on the navball sphere in the active basis. The painter projects (LatDeg, LonDeg) through the sub-observer point to a cell offset; if the marker lies in the front hemisphere (or in the narrow limb dead zone) it draws Glyph in Color, otherwise the marker is skipped — the ball reads like a solid sphere with no antipodal bleed-through.

v0.9.5+.

type RingBand added in v0.8.5

type RingBand struct {
	InnerR float64        // meters from body center
	OuterR float64        // meters from body center
	Color  lipgloss.Color // band color (passed through to canvas pixel tags)
}

RingBand is one annular band of a body's ring system: an inner and outer radius (meters from body center) plus the band color. v0.8.5.7+ — multi-band ring rendering for Saturn.

func BodyRingBands added in v0.8.5

func BodyRingBands(bodyID string) []RingBand

BodyRingBands returns the per-band ring layout for ringed bodies. Bands are returned in order from innermost to outermost so callers can iterate radially. Saturn's layout: C ring (dim) → B ring (brightest) → Cassini Division (gap, not in list) → A ring (medium) → F ring (thin bright thread). v0.8.5.7+.

Numbers are textbook radii (NASA Cassini fact-sheet); the equatorial-plane projection means callers see them foreshortened per the canvas view direction.

type SolarLight added in v0.9.6

type SolarLight struct {
	SubSolarLatDeg, SubSolarLonDeg float64
	EclipseFactor                  float64
}

SolarLight carries the per-body lighting state the texture wrapper needs. SubSolar{Lat,Lon}Deg is the sub-solar point in the same (lat,lon) frame projectPixelToLatLon produces. EclipseFactor is the Phase-B global dim multiplier (1.0 = no eclipse).

func (*SolarLight) FactorAt added in v0.9.6

func (s *SolarLight) FactorAt(dx, dy, r int, subLatDeg, subLonDeg, screenUpX, screenUpY float64) float64

FactorAt returns the illumination factor for the pixel at (dx,dy) on a disk of radius r, given the camera sub-observer point (subLatDeg, subLonDeg) the shader used to paint it. The result is the smooth day/night term times EclipseFactor, clamped to [umbraFloor, 1].

subLatDeg/subLonDeg MUST be the camera sub-observer values passed to the shader (not the sub-solar ones): projectPixelToLatLon then yields the pixel's (lat,lon) in the same frame as SubSolar*, so the spherical-cosine angular distance below is frame-consistent and the epoch offset cancels.

type Theme added in v0.7.2

type Theme struct {
	UI     map[string]string `json:"ui,omitempty"`
	Bodies map[string]string `json:"bodies,omitempty"`
}

Theme is the on-disk shape of the user's optional theme overlay, loaded from $XDG_CONFIG_HOME/terminal-space-program/theme.json (or ~/.config/... if XDG is unset). Both blocks are optional; missing keys fall through to v0.7.1's per-body Color JSON field and the legacy bodyPalette table.

Keys in UI match the lower-cased name of the corresponding package-level Color* var (without the "Color" prefix). Keys in Bodies match each body's id from systems/*.json.

type Vec3 added in v0.8.5

type Vec3 struct{ X, Y, Z float64 }

Vec3 is the render package's local 3-element inertial-frame vector. Defined here rather than imported from orbital to keep the render package free of physics-package dependencies (render stays a pure leaf). Same convention as orbital.Vec3.

func BodyFixedToWorld added in v0.9.2

func BodyFixedToWorld(b bodies.CelestialBody, latDeg, lonDeg float64, simTime time.Time) Vec3

BodyFixedToWorld converts body-fixed (lat, lon) to a unit world-frame vector that points AT the (lat, lon) cell on body b at simTime. v0.11.2+ (ADR 0003): pure rotation about the body's physical spin axis (BodyRotationAxisWorld), view-independent.

At simTime t the body-fixed basis in world coords is:

bodyX(t) = body lon=0, lat=0 meridian direction in world frame
bodyY(t) = body lon=+90, lat=0  (= cross(n, bodyX))
n       = BodyRotationAxisWorld(b)         (lon=any, lat=+90)

(lat, lon) → world: cosφ·cosλ·bodyX + cosφ·sinλ·bodyY + sinφ·n.

bodyX(t) is the world +X reference (or +Y when degenerate) rotated about n by (rotationPhase(t) − epochOffset). The per-body epochOffset is the offset between the body's lon=0 meridian and the reference direction at rotationEpoch — chosen so iconic features land at the visible disk centre for the canonical reference view.

Latitude in degrees north positive. Longitude in degrees east positive (real-Earth-style). Multiply the returned unit vector by primary radius for surface position.

Pre-v0.11.2 this used a Snyder-at-ViewTop construction that rotated about world +Z instead of the physical spin axis, diverging from BodySpinOmegaWorld by the body's tilt (32.6° for Earth). See ADR 0003 for the full diagnosis and the alternatives considered.

func BodyFrameEast added in v0.11.0

func BodyFrameEast(b bodies.CelestialBody, r Vec3) Vec3

BodyFrameEast returns the local east unit vector in the world inertial frame at the surface point given by `r` (position vector from body centre; magnitude irrelevant — only direction matters).

East is defined as the unit vector ẑ_body × r̂, where ẑ_body is the body's tilted spin axis (`BodyRotationAxisWorld(b)`). This gives a vector tangent to the surface, perpendicular to local-up, pointing in the prograde (eastward) direction of rotation.

Pole-on degenerate guard (per v0.11 Slice 1 grill resolution): when the cross product magnitude shrinks below 1e-9 (caller is within ~57 nrad of the spin axis), falls back to projecting world +X onto the local-horizontal plane (the plane perpendicular to r̂). This is non-degenerate, deterministic, and safe — though world +X is inertial rather than body-fixed, so the fallback rotates relative to the ground at the body's spin rate. Acceptable for the pole- launch case Slice 1 does not optimise.

func BodyRotationAxisWorld added in v0.8.5

func BodyRotationAxisWorld(b bodies.CelestialBody) Vec3

BodyRotationAxisWorld returns the body's spin axis as a unit vector in the world inertial frame, derived from AxialTilt and AxialAzimuth. The axis is

n = (sin(tilt)·cos(azimuth), sin(tilt)·sin(azimuth), cos(tilt))

where tilt is the obliquity to the world Z-axis (orbital-plane normal) and azimuth is the longitude of the axis's projection onto the X-Y plane. With AxialAzimuth = 0 (the default for all bodies populated through v0.8.5.7) this collapses to the X-Z-plane convention `(sin(tilt), 0, cos(tilt))` the earlier v0.8.5.7 work used.

func BodySpinOmegaWorld added in v0.9.2

func BodySpinOmegaWorld(b bodies.CelestialBody) Vec3

BodySpinOmegaWorld returns the body's spin angular-velocity vector in the world frame: ω = (2π/period) · n_hat. Direction matches BodyRotationAxisWorld (tilted, picks up AxialTilt + AxialAzimuth). Returns the zero vector when the body has no rotation period set. v0.9.2+. Used by sim's launchpad spawn (surface co-rotation velocity = ω × r) and landed-craft integration (rotates R about the tilted axis per tick).

v0.11.2+ (ADR 0003): physics.AtmosphereOmega returns the same vector — the renderer, integrator, and drag model all share one rotation convention. Pre-v0.11.2 the drag path used a Z-aligned approximation that diverged from this for tilted bodies.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL