starmap

Guides

Drawing

What the renderer actually puts on screen: the layers, the order they are stacked in, and how each object is constructed out of circles and paths.

Order is the composition

SVG has no z-index. What is written later is drawn on top — so the order of the layers is the composition, and changing it is a visual change.

Each layer is a <g> carrying a data-layer. That attribute is not decoration: it is how an application dims, hides or restyles a whole class of thing without the package knowing anything about it.
Aurelia Vesper Sol Gate
Everything on. This is the default.
{ backdrop: 0, labels: false } — the same drawing with two layers removed, which is the easiest way to see what the others are doing.

The system layers

Bottom to top, exactly as they are emitted:

FieldTypeDescription
defsnot a layerThree radial gradients — the star's halo, its core, and the shading on a planet. Their ids are prefixed, see idPrefix.
backgrounda bare <rect>Fills the view. Three times the viewBox and centred on it, on purpose — the letterboxing story.
backdropdata-layerThe decorative star field. Seeded, so it never flickers between frames.
orbitsdata-layerThe circles planets and gates travel on.
fieldsdata-layerAsteroid belts and clusters, with their rocks.
routesdata-layerLocal routes, each stroked with its own dash pattern.
stardata-kindNot a layer of its own — one group, sitting between the fields and the planets.
planetsdata-layerPlanets, their stations, their labels.
gatesdata-layerLast, so a gate is never hidden behind a planet that drifts over it.

The galaxy layers

Shorter, because at galaxy scale there is less to say:

FieldTypeDescription
backgrounda bare <rect>Same as above.
backdropdata-layerThe star field, sown wider.
lanesdata-layerThe hyperlane network. Highlighted lanes are drawn in the same layer.
systemsdata-layerA dot per system, its faction ring, its halo, its label.
Sol Vega Rigel Altair Lyra
Five systems, five lanes, nothing highlighted. Sol and Vega wear their faction's colour; Rigel, Altair and Lyra have no faction, so their ring falls back to factions.default.

Nothing moves on the galaxy map

renderGalaxy() takes no t. At galaxy scale a planet is smaller than a rounding error, so there is nothing to animate — the systems are where createGalaxy() put them.

Two kinds of size

A system can be 300 world units across, or 30 000. That single fact shapes every number in the renderer, and it splits them into two families:

  • Real sizes stay real.A star's radius, a planet's radius, an orbit — those come from the model and are drawn as they are. A planet twice as big looks twice as big.
  • Everything else is a fraction of the view. Gate glyphs, stroke widths, label sizes, the gaps between things. Sizing those in absolute units would make them fill the screen in a small system and vanish in a large one.

Inside the source that second family goes through a small Scale object — u for spacing (a 200th of the view), k for strokes, font for labels. You never see it from outside, but it is why a drawing holds up at any size.

This is what makes a thumbnail work

Render a system at 64 pixels and the labels are still legible relative to the drawing, because they were never 12 pixels — they were a fraction of the frame.

How each object is built

The star

Three circles stacked: a wide halo on the glow gradient, the disc itself, and a bright core. The halo is several times the disc — that is what reads as brightness rather than as a bigger star.

Its colour comes from theme.stars, keyed by star.class. A class the table does not list falls back to the table's default.

Planets and stations

A planet is a disc filled from theme.planets by its type, with a shading gradient over it so it does not read as a flat dot. Two optional additions:

  • a ring in theme.habitable when planet.habitable is true;
  • a label with its name, when labels is on.

Stations are drawn inside the planet's group, because they travel with it. Each is a small mark coloured from theme.stations by its type.

<g data-layer="planets">
  <g data-kind="planet" data-id="sol-p1">
    <circle .../>                                   the disc
    <circle fill="url(#starmap-shade)" .../>        the shading
    <circle stroke="#4fd8a5" .../>                  habitable ring
    <g data-kind="station" data-id="sol-p1-st0"></g>
    <text>Vesper</text>
  </g>
</g>

Gates

A glyph on its orbit circle, which is dashed to tell it apart from a planet's. A gate whose status is unstable gets a second ring in theme.gateUnstable.

A gate label can name where it leads

Pass systems: indexSystems(galaxy)and the label becomes the destination's name rather than the gate's. The renderer cannot know it otherwise — a system does not carry its neighbours.
import { indexSystems } from '@industrieh/starmap'

renderSystem(system, { systems: indexSystems(galaxy) })
Aurelia Vesper Sol Gate → Vega
The gate at the top right is labelled “Vega”, its destination, instead of “Sol Gate”. Without the index the renderer has no way to know — a system does not carry its neighbours.

Asteroid fields

Two shapes, chosen by the field's discriminant:

  • A belt is an arc, stroked at the width of the ring. It is a path rather than a circle because a belt is usually a partial arc.
  • A cluster is a circle at its own position.

Either way, the rocks you provided are drawn inside it. The renderer never invents any — an empty rocks array draws the outline and nothing else.

A full ring is a special case worth knowing about: a single SVG arc cannot describe a complete circle, because its end point would be its start point and the arc would collapse to nothing. Past 359.5° the path becomes two half-circles instead.

import { arcPath } from '@industrieh/starmap-render'

arcPath(220, 0, 120)
// 'M 220 0 A 220 220 0 0 1 -110 190.53'

arcPath(220, 0, 360)
// 'M 220 0 A 220 220 0 1 1 -220 0 A 220 220 0 1 1 220 0'
//   ↑ two arcs, because one cannot close a circle

Systems and lanes

On the galaxy map, a system is up to four things stacked:

  • a halo, so a dense cluster still reads as points of light;
  • a ring in theme.factions, keyed by faction.id — only when the system has a faction;
  • the dot itself;
  • its name, when labels are on.

A lane is a line between two systems, in theme.lane — or in theme.highlight when it is part of the highlighted path.

The faction ring is dashed until it is highlighted

A highlighted system gets a solid ring in theme.highlight; every other one is dashed, in its faction's colour. Two channels rather than one, so the path is still obvious in greyscale — the same reasoning as the route dash patterns.

The empty cases

The core makes most fields optional, so the renderer meets them constantly. All of these produce a valid document:

FieldTypeDescription
No planetsa systemNo planets layer at all — an empty group is never emitted.
No type on a planetundefinedFalls back to theme.planets.default. Not a bug — the core never invents a value, so every table has a fallback.
No systemsan empty galaxyA fallback viewBox of -100 -100 200 200, so the document is still displayable.
One systema galaxyIts bounding box would have zero width and height, which renders as nothing. Both are clamped to at least 1.
outerLimit of 0a systemFalls back to 100. A drawing that cannot be framed is worse than one framed arbitrarily.
Rigel: a star and nothing else. No planets layer, no fields layer, no gates layer — and still a perfectly valid document.

A name with an ampersand does not break the document

Every name in a galaxy is user data, and it goes through XML escaping before it reaches a string. A system called Ohm & Sons comes out as Ohm &amp; Sons — which is also what stops a name carrying a tag from injecting markup into the page that embeds the drawing.