Guides
Theming
Every colour the renderer reads, where it comes from, and how to replace one without restating the other twenty.
Why colour lives here
The core carries no color and no display label. Mapping planet.type to an appearance is a decision of the project that displays it — and a colour written into the model stops being usable the day that project needs a light theme, a colourblind mode, or a fixed brand palette.
No colour in the core, and no world fact in the theme. That line is why the two packages are separate.
So the theme is a parameter. It enters at the call, and the same universe can be drawn three different ways on the same page.
How a theme merges
There are two layers, exactly like the core's GalaxyInput and Galaxy: ThemeInput is what you write and every field is optional; Theme is what the drawing functions read and everything is filled in.
Tables merge key by key, so overriding one colour keeps the rest:
renderSystem(system, {
theme: { background: '#000', planets: { terran: '#7ee787' } },
})
// background → '#000' yours
// planets.terran → '#7ee787' yours
// planets.gas → '#d8a06a' still the default
// planets.default → '#8b8377' still the default{ planets: { terran: '#f472b6' } }. Vesper is pink; Aurelia, the star, the belt and the gate are all still the default.A RouteStyle is replaced whole
{ routes: { mining: { … } } } replaces the whole mining style, because a dash pattern without its width is not a style. Give all three fields.Setting a field to undefined does not erase it
{ background: undefined } falls back to the default rather than leaving a hole in the drawing. That is deliberate — a spread would otherwise let an optional variable blank out a colour and produce a transparent document.The flat colours
Thirteen single values. Every one of them is a string, passed straight into SVG.
| Field | Type | Description |
|---|---|---|
background | stringdefault #070b16 | Fills the whole view. |
backdropStar | stringdefault #cdd9ff | The decorative star field behind everything. |
label | stringdefault #dbe4f5 | Text colour. |
font | stringdefault ui-sans-serif, system-ui, sans-serif | A font stack, written onto the label groups. Not a colour, but it belongs with them. |
orbit | stringdefault #2b3d63 | Planet orbit circles. |
gateOrbit | stringdefault #43356e | Gate orbit circles, which are dashed. |
lane | stringdefault #39507a | Hyperlanes, on the galaxy map. |
highlight | stringdefault #9a7bff | Systems and lanes on the path passed as highlight. |
habitable | stringdefault #4fd8a5 | The ring around a planet whose habitable is true. |
gate | stringdefault #9a7bff | The gate body. |
gateUnstable | stringdefault #ff6b81 | The ring around a gate whose status is unstable. |
field | stringdefault #6b5a2a | Outline of an asteroid belt or cluster. |
rock | stringdefault #a89468 | The rocks inside a field. |
The keyed tables
Four colour tables, each keyed by a value from the model. Here they are with their defaults:
stars: { // by star.class
O: '#9bb0ff', B: '#aabfff', A: '#cad7ff', F: '#f8f7ff',
G: '#ffe9a8', K: '#ffc16f', M: '#ff8f5a',
default: '#ffe9a8',
}
planets: { // by planet.type
lava: '#e2542c', rocky: '#a08d7d', desert: '#d9a45b',
barren: '#8b8377', terran: '#4fae6a', ocean: '#2f8fd8',
toxic: '#9ac93b', gas: '#d8a06a', 'ice-giant': '#6fb6d9',
frozen: '#cfe4f0',
default: '#8b8377',
}
stations: { // by station.type
trade: '#61d6c4', mining: '#c9a227', military: '#ff6b81',
research: '#7fb3ff', shipyard: '#d0a6ff',
default: '#9fb4d8',
}
factions: { // by faction.id — drawn as the ring around a system
neutral: '#8e9bb3', consortium: '#4ea3ff', covenant: '#ff5c72',
syndicate: '#c78bff', pilgrims: '#4fd8a5',
default: '#8e9bb3',
}Every table needs a default
This is the part people trip on. The core's vocabularies are open unions: a project can invent type: 'shattered' and the model accepts it. A colour table therefore cannot be exhaustive.
import { pickColor, resolveTheme } from '@industrieh/starmap-render'
const theme = resolveTheme({ planets: { terran: '#7ee787' } })
pickColor(theme.planets, 'terran') // '#7ee787'
pickColor(theme.planets, 'shattered') // '#8b8377' — the default
pickColor(theme.planets, undefined) // '#8b8377' — the default toodefault is required by the type, so a table you build by hand cannot forget it. That is what guarantees a drawing is never missing a colour.
Route styles
Local routes are the one table whose values are objects rather than strings — a route needs a stroke, a dash pattern and a width.
routes: { // by localRoute.type
orbital: { stroke: '#5f7fa8', dash: '6 8', width: 1.1 },
shortcut: { stroke: '#43607f', dash: '2 10', width: 0.9 },
docking: { stroke: '#61d6c4', dash: '3 4', width: 0.8 },
'jump-lane': { stroke: '#9a7bff', dash: '10 6', width: 1.4 },
mining: { stroke: '#c9a227', dash: '4 6', width: 0.9 },
default: { stroke: '#5f7fa8', dash: '6 8', width: 1.1 },
}The dash patterns are an accessibility decision
Writing a light theme
The default theme is dark. Nothing in the renderer assumes that — a light theme is the same object with different values.
import type { ThemeInput } from '@industrieh/starmap-render'
export const LIGHT: ThemeInput = {
background: '#f8fafc',
backdropStar: '#94a3b8',
label: '#0f172a',
orbit: '#cbd5e1',
gateOrbit: '#ddd6fe',
lane: '#94a3b8',
highlight: '#7c3aed',
habitable: '#059669',
gate: '#7c3aed',
gateUnstable: '#e11d48',
field: '#d6c9a0',
rock: '#a8a29e',
// The tables merge, so only the colours that need to change are listed.
planets: { frozen: '#7dd3fc', terran: '#16a34a' },
}
renderSystem(system, { theme: LIGHT })Check the backdrop when you go light
backdropStar is the one that catches people out. Left at its default it is a pale blue, which all but disappears on a pale background — and you get a drawing that looks oddly empty for no obvious reason.backdropStar forgotten. The star field is still there, and you can barely see it.backdropStar: '#94a3b8', and the sky comes back.Resolving once
resolveTheme() is cheap but not free — it builds five merged tables. If you render every frame, resolve once outside the loop.
import { resolveTheme } from '@industrieh/starmap-render'
// Once.
const theme = resolveTheme(LIGHT)
// Then per frame — a resolved Theme is a valid ThemeInput, so it just passes through.
function frame(t: number) {
container.innerHTML = renderSystemContent(system, { t, theme })
}DEFAULT_THEMEis exported, so you can read a value or spread from it.pickColor(table, key)andpickRouteStyle(table, key)are exported too — useful when you draw your own overlay and want it to match.