Skip to content

rulkens/skymap

Repository files navigation

skymap

An interactive WebGPU 3D explorer for the SDSS, GLADE, and 2MRS galaxy catalogs — fly through millions of galaxies in your browser.

CI License: MIT Node TypeScript DOI

skymap — orbit, command palette, focus tween, info card

Live demo →   ·   Chrome 113+   ·   Edge 113+   ·   Firefox 141+   ·   Safari 26+

Built in TypeScript with React for the UI. Hover or click any galaxy to see its sky coordinates, redshift, lookback time, and catalog metadata; pin one to compare against another; explore the cosmic-web wedge in 3D with mouse-driven orbit controls.

The code is documented didactically throughout — if you're also looking to learn WebGPU, GPU picking, or the basics of cosmological coordinate math, the source is meant to be read.

Screenshots

Full HUD with all surveys loaded

The full overlay surface — left stack (Navigation cheatsheet, Settings panel, FPS / counts) and the InfoCard pinned to a selected galaxy on the right, with the cosmic-web filament skeleton drawn over the point field.

Data-tier selector

Segmented control at the top of the Settings panel hot-swaps the loaded dataset between three sizes: Small (~300k galaxies — mobile-friendly), Medium (~600k — default for laptops), and Large (~3.5M — full catalog). Tier choice on first paint is driven by viewport width; clicking a button re-fetches the relevant .bin files and re-uploads the GPU vertex buffers in place — no page reload.

Pinned InfoCard for a 2MRS galaxy

Close-up of the right-side InfoCard. Pinning a galaxy reveals lookback time (with an Earth-era reference line), comoving distance + recession velocity, equatorial coordinates in both sexagesimal and decimal form, redshift, apparent magnitude in the survey's native band, plus a thumbnail (DSS via CDS hips2fits for 2MRS / GLADE; SDSS DR18 ImgCutout for SDSS) and an external-catalogue link where one exists.

Local Volume close-up

Local Volume, roughly 10–20 Mpc across. Bright textured quads are Famous-catalog galaxies with hand-curated DESI Legacy thumbnails; the faint blue lattice is the DisPerSE filament overlay.

Supercluster-scale view

Supercluster-scale view, roughly 200–400 Mpc across — thousands of galaxies clustering into a dense core with filaments radiating into surrounding voids.

Close-up zoom

Zooming in: the dot dissolves into a procedural disk impostor (Gaussian bulge + exponential profile, 3D-oriented from catalog axis-ratio + position angle) and then into a real DESI thumbnail once the apparent size crosses the fetch threshold.

Density-correction modes dropdown

Five density-correction modes selectable at runtime (None / Volume-limited / 1/V_max / Schechter LF / Angular re-weight via HEALPix), addressing Malmquist bias and pencil-beam survey-footprint artefacts. See the Density correction section below for the science behind each mode.

Use cases

  • Teaching cosmic large-scale structure — fly through the SDSS wedge and see filaments, voids, and the Sloan Great Wall directly, without needing to spin up a Jupyter notebook or a desktop visualisation suite.
  • Public outreach and general curiosity — a browser-based way to experience the geometry of the local universe: how galaxies cluster, how far apart they really are, and how the Milky Way sits inside the cosmic web.
  • Galaxy-catalog browsing for the GW host-candidate community (potential). Skymap loads GLADE because GLADE was designed for gravitational-wave host-candidate work; the actual probability-volume overlay isn't built yet, but I'm open to exploring it if there's genuine demand from someone in the follow-up community.

Requirements

  • Node 20+

  • A WebGPU-capable browser. WebGPU has been a Baseline web platform feature since January 2026; in practice that means:

    • Chrome / Edge 113+ — desktop (since 2023) and Android 12+ on Qualcomm/ARM GPUs.
    • Firefox 141+ — Windows (since July 2025).
    • Firefox 145+ — macOS on Apple Silicon (Tahoe 26+); Linux & Android still in progress through 2026.
    • Safari 26+ — macOS Tahoe 26, iOS 26, iPadOS 26, visionOS 26.

    Touch UX on phones / tablets is not yet polished — controls assume a mouse — but the WebGPU pipeline itself runs everywhere in the list.

Quickstart (synthetic data)

npm install
npm run dev

Open http://localhost:5173 — drag to orbit, scroll to zoom. Without real data files present you'll see 100,000 synthetic galaxies distributed in a sphere. Enough to verify the renderer works end-to-end and to play with hover/select before you commit to a multi-megabyte download.

What surveys do I actually need?

The renderer can ingest up to three galaxy catalogs in parallel. Each is just a list of galaxies with positions and brightnesses, but they cover the sky differently:

  • SDSS (Sloan Digital Sky Survey) — a deep photographic + spectroscopic survey from a single 2.5 m telescope in New Mexico, covering roughly the northern third of the sky. Best dense coverage in its footprint; we use a slice of ~500 k galaxies.
  • 2MRS (2MASS Redshift Survey) — a smaller (~45 k), all-sky redshift survey concentrated on the local volume around the Milky Way. Useful for nearby galaxies in any direction.
  • GLADE — a million-galaxy all-sky mega-catalog cross-matched from several surveys. Reaches roughly the same radial depth as SDSS, but covers the full sky — so its main contribution is filling in the celestial regions outside SDSS's northern footprint, while also extending well beyond 2MRS's local volume.

You can run with any one, any two, or all three. The renderer falls back to synthetic data if no .bin files are present.

Loading real data

The renderer fetches /data/sdss.bin, /data/2mrs.bin, and /data/glade.bin at startup, using whichever are present. The pipeline below produces those files from raw catalog downloads.

1. Download the catalogs

Survey Source File / Notes
SDSS SkyServer SQL Run the query below; export as CSV.
2MRS VizieR J/ApJS/199/26 table3.dat, 233-byte fixed-width, 44,599 rows, ~10 MB. Drop into data/raw/2mrs_table3.dat.
GLADE VizieR VII/281 glade2.3.dat, 256-byte fixed-width, 3.26 M rows, ~838 MB. Drop into data/raw/glade2.3.dat.

GLADE alone subsumes 2MPZ and 6dFGS — the GLADE team has already cross-matched and deduplicated 2MPZ + 2MASS XSC + HyperLEDA + GWGC + SDSS-DR12Q, so a single download replaces what would otherwise be three.

SDSS query

Go to the DR18 SQL Search and run:

SELECT TOP 500000
  p.objID, p.ra, p.dec, s.z,
  p.modelMag_u, p.modelMag_g, p.modelMag_r, p.modelMag_i, p.modelMag_z,
  p.expAB_r, p.expPhi_r, p.deVAB_r, p.deVPhi_r, p.fracDeV_r,
  p.petroR50_r, p.petroR90_r
FROM SpecObj AS s
JOIN PhotoObjAll AS p ON s.bestObjID = p.objID
WHERE
  s.class = 'GALAXY'
  AND s.zWarning = 0
  AND s.z BETWEEN 0.001 AND 0.3

Choose CSV as the output format. The columns break down into three groups:

  • Photometry (modelMag_u/g/r/i/z) — brightness in five colour bands; drives the per-galaxy colour ramp.
  • Shape / orientation (expAB_r, expPhi_r, deVAB_r, deVPhi_r, fracDeV_r) — axis ratio and position angle from two profile fits, blended by fracDeV_r. Drives the elliptical billboard mask and 3D disk plane.
  • Size (petroR50_r, petroR90_r) — half-light and 90%-light radii in arcsec; the parser converts them to physical kpc using each galaxy's redshift.

Need more than the 500 k row limit? Use CasJobs instead — same query, no timeout, larger result quotas.

2. Build the binary files

npm run build-all -- \
  --sdss    "data/Skyserver_SQL.csv" \
  --twomrs  data/raw/2mrs_table3.dat \
  --glade   data/raw/glade2.3.dat \
  --out-dir public/data

Omit any --xxx flag you don't have — the merger treats missing inputs as empty and skips writing that output file. So --sdss only is a fine single-survey workflow.

The tool parses each catalog, runs cross-match dedup using priority SDSS > 2MRS > GLADE, then writes v4 binary files to public/data/sdss.bin, 2mrs.bin, glade.bin. Sample run on the full inputs: 500 k SDSS / 41 k 2MRS / 2.1 M GLADE galaxies after dedup, ≈ 32 + 2.6 + 130 MB on disk.

3. (Optional) Enrich with real galaxy orientations

2MRS and GLADE don't ship with shape/orientation columns, so by default those galaxies render with deterministic-but-fake orientations (random per galaxy, stable across reloads). You can fetch real orientation data from external services:

npm run fetch-2mass-xsc    # ~5 minutes; adds PA + axis-ratio for 2MRS galaxies
npm run fetch-hyperleda    # ~1 hour; adds PA + axis-ratio for GLADE galaxies
  • fetch-2mass-xsc queries the 2MASS Extended Source Catalog and writes data/raw/2mass_xsc_pa.csv. Quick — runs in roughly five minutes.
  • fetch-hyperleda queries HyperLEDA at 4 concurrent requests across ~1.5 M PGCs and writes data/raw/hyperleda_pa.csv. Takes roughly 1 hour end-to-end. The script is resumable — interrupt and restart safely.

HyperLEDA orientation cache: download instead of fetching

Running the full HyperLEDA fetch yourself takes an hour and hammers HyperLEDA's servers with ~1.5 M requests. A pre-computed cache is available from the same R2 bucket that serves the .bin catalog files — download it instead:

mkdir -p data/raw
curl -L -o data/raw/hyperleda_pa.csv.gz \
  https://skymap-data.rulkens.com/data/hyperleda_pa.csv.gz
gunzip data/raw/hyperleda_pa.csv.gz

The cache is the output of a completed npm run fetch-hyperleda run, gzipped with -9 for transport. It's updated manually whenever a catalog refresh is synced to R2. If you need the absolute latest HyperLEDA values (e.g. after a new GLADE release), the npm run fetch-hyperleda path above still works — run it, then gzip -k -9 data/raw/hyperleda_pa.csv and follow the npm run sync-r2 steps in CLAUDE.md to push a fresh copy.

Both files are picked up automatically by the next npm run build-all. Both commands are entirely optional; the renderer works without them.

4. Reload

The browser fetches all available files in parallel at startup. Surveys arrive progressively. The settings panel (bottom-left) has per-survey checkboxes for toggling sources on and off.

Per-survey colour indices

Each survey is coloured by its own most-informative photometric pair, since the five magnitude slots in the binary format carry different bands depending on the source. The raw colour difference is normalised to the shader's blue → white → red ramp at upload time, and a per-row K-correction coefficient compensates for redshift band-shifting before the ramp is sampled. Rows whose preferred bands aren't measured render with a fixed mid-ramp tint instead of poisoning the ramp with NaN.

Survey Colour Natural range K per unit z Why this k
SDSS u−g 0.5 .. 2.0 3.0 Calibrated against the SDSS spectroscopic sample.
GLADE B−J 0.5 .. 3.5 1.0 Optical–NIR pair; B redshifts out of band slowly.
2MRS J−K 0.7 .. 1.1 0.0 NIR colours are nearly redshift-invariant in 2MRS's z ≲ 0.1 box.

Famous galaxies (curated atlas)

Optional. The renderer works fine without the famous-galaxies bin — survey galaxies still render and the InfoCard still works. Skip this section entirely if you only want the catalog data. Build it when you want curated names + hand-fetched high-quality thumbnails for the Messier / NGC greatest-hits.

A separate small catalog of well-known galaxies (Messier + NGC greatest-hits) ships alongside the survey data. Entries appear with their curated names in the InfoCard and are searchable via the Cmd+K / Ctrl+K command palette. Their thumbnails are pre-processed transparent WebPs hand-fetched from the DESI Legacy Imaging service, so famous galaxies always render at high quality — even for nearby objects (M31, M33) that survey catalogs filter out as too close.

Run order (only if you want the famous-galaxies atlas):

  1. npm run build-all — produces 2mrs.bin + glade.bin, which the famous build needs for cross-match.
  2. npm run fetch-famous-images — downloads + processes 20 thumbnails (~30 s). Idempotent; pass --force to re-fetch.
  3. npm run build-famous — produces famous.bin + famous_meta.json + famous_xrefs.json.

Adding more galaxies

The seed file is data/famous_galaxies.seed.json. Each entry needs:

Field Type Notes
id string URL-safe lower-case identifier (e.g. m31, ngc-5128)
names string[] One or more names; first is the headline
ra number Right Ascension in degrees, [0, 360)
dec number Declination in degrees, [-90, 90]
distanceMpc number Curated distance in megaparsecs
diameterKpc number Physical isophotal diameter in kpc
type string Hubble morphological type (free-form)
description string 1-3 sentence editorial blurb

After adding an entry, re-run npm run fetch-famous-images && npm run build-famous.

Galaxy thumbnails

When you zoom in close to a galaxy, the renderer fetches its real image and draws it as a textured billboard instead of the usual dot. The textured-quad pass runs after the existing point pass, so the dot stays visible behind the quad as a soft glow.

How it decides which galaxies get textured: the engine computes each galaxy's on-screen apparent size from its real catalog-derived diameter (with a 30 kpc fallback for galaxies whose source catalog doesn't carry size data), and only galaxies whose apparent size exceeds 24 pixels get a thumbnail fetched. Below the threshold the dot is all you get — keeps network traffic bounded to the small handful of galaxies that are actually large on screen.

Image sources:

  • SDSS DR18 ImgCutout is the primary source — high-resolution colour JPEGs covering ~1/3 of the sky (mostly northern).
  • CDS hips2fits is the all-sky fallback for 2MRS/GLADE galaxies outside the SDSS footprint. Lower resolution, monochrome (DSS POSS-II red), but covers the entire celestial sphere and is CORS-safe.

Cache: thumbnails live in a single 2048×2048 RGBA8 GPU texture atlas with 256 slots of 128×128 pixels each. When the atlas is full, the slot whose galaxy was least recently visible is evicted (LRU). A priority fetch queue runs at most 4 concurrent downloads, picking the largest-on-screen pending galaxies first so the most visually important thumbnails arrive first.

Visual polish: each quad uses a radial alpha falloff so the JPEG-square outline fades into a soft galaxy-like blob rather than showing as a hard rectangle against dark space.

Toggle: the Settings panel has a "Galaxy thumbnails" checkbox (default on). Switch it off if you'd rather see the raw point cloud without network traffic, or to compare the dot field with and without textures.

Procedural galaxy disks

Between the dot field (small, screen-aligned billboard) and the real thumbnail (large, downloaded JPEG), there's a middle band where a galaxy is visibly large enough that the dot looks too sparse but the thumbnail-fetch network round-trip would feel laggy. The renderer fills that band with a third pass: a procedural 3D-oriented disk impostor that runs entirely on the GPU, no network, no atlas.

How it looks: a soft elliptical disk with a brighter Gaussian bulge in the middle and an exponential falloff outward. Hue comes from the same colour-index ramp the points pass uses, so a galaxy's procedural disk matches its companion point's colour exactly.

Geometry: each disk is a 3D quad fixed in world space, oriented by the galaxy's catalog axis ratio (b/a → inclination via cos i) and position angle (east of north). Foreshortening falls out of the perspective projection naturally — orbit the camera and the projected ellipse shape changes accordingly. See disks.wgsl for the basis- construction derivation; the procedural pass reuses that math verbatim so the textured-thumbnail pass and the procedural pass agree at the crossfade boundary.

Crossfade band: apparent size 8 → 14 px. Below 8 px the dot is fully bright and no disk renders. Inside the band a t² (3 − 2t) smoothstep ramps the disk in while the dot fades out by the complementary curve 1 − t² (3 − 2t); the two curves sum to exactly 1.0 across the band so the per-galaxy HDR contribution stays constant through the transition (no double-bright donut). Above 14 px the procedural disk is at full alpha; above 24 px the textured thumbnail overlays it with higher fidelity.

Why three passes (point + procedural + textured) rather than two: fetching a thumbnail for every galaxy that grows past a few pixels would slam the SDSS/CDS endpoints and the atlas LRU. The procedural pass lets the renderer present "this is a galaxy with a bulge and a tilt" all the way down to ~8 px without touching the network. The textured pass kicks in only for the relatively small set of galaxies that the user has zoomed close enough on to make pixel-level texture detail worthwhile.

Performance: one extra draw call per frame, with instances emitted only for galaxies inside the band (see maybeEmitProceduralDisk in thumbnailSubsystem.ts). The shader is ~50 lines of WGSL: two exp per fragment plus the colour-ramp lookup. The fragment cost is dominated by the radial brightness profile, not by anything per-galaxy.

Cosmic-web filaments

Galaxies aren't randomly scattered in space — they cluster along a fractal-looking network of filaments and walls separating large underdense voids. Skymap can render that network directly as a faint blue lattice overlaid on the point field.

What you see: thin lines tracing the topological ridges of the galaxy density field. Switch the overlay on via the Filaments toggle in the Settings panel; intensity slider next to the toggle dims the overlay if it competes with the underlying point colours under tone-mapping.

How the skeleton is built: the filament file is computed offline by DisPerSE (Sousbie 2011), an astrophysics topology pipeline that extracts the persistent ridges of the Delaunay-tessellation density field. The default build runs delaunay_3D → mse → skelconv with a 5σ persistence cut and 2 smoothing passes, against the 2MRS + GLADE subset of the catalogue. SDSS is excluded by default because its wedge footprint dominates the density field at the survey edges and DisPerSE locks onto those boundaries instead of the actual cosmic web (an SDSS-only diagnostic build is available via --sources sdss and confirms this empirically).

Building locally (skip if you don't want filaments — the renderer treats filaments.bin as optional and silently no-ops the toggle if the file isn't present):

  1. Install DisPerSE following its upstream instructions; ensure delaunay_3D, mse, and skelconv are on $PATH.
  2. Run npm run build-all first so the .bin catalogues exist.
  3. Run npm run build-filaments. Output: public/data/filaments.bin. Takes a few minutes on a 2MRS+GLADE input.

CLI flags: --cut N (persistence sigma, default 5), --smooth N (skelconv smoothing passes, default 2), --sources csv (subset of sdss,2mrs,glade, default 2mrs,glade), --output path (write elsewhere so diagnostic builds don't clobber the canonical file).

One file across all tiers. Unlike the per-tier galaxy .bins, the filament skeleton is shared between the small / medium / large dataset tiers — the cosmic web extends well beyond the points the small tier is rendering, and showing the structure even where the point sample is decimated is more informative than tier-matched filaments would be.

Brightness controls

Real catalogue galaxies span ~10 magnitudes of apparent brightness — the brightest entries are roughly 10⁴× brighter than the faintest — so drawing every galaxy as an identical dot would throw away most of the visual information. Three controls in the renderer decide how that range is displayed on screen:

  • Catalogue magnitude → per-galaxy alpha (automatic, vertex stage) — every galaxy's apparent magnitude is mapped to an intensity in [0.05, 1.0] via clamp((22 − magnitude) / 8, 0.05, 1.0) (points.wgsl). A magnitude-14 nearby spiral therefore renders with ~20× the alpha of a magnitude-22 background galaxy. The 0.05 floor keeps the faintest detections barely visible rather than fully transparent — a hard zero would leave confusing gaps where survey rows are sparse.
  • Global brightness slider (0.2 – 3.0, default 1.0) — uniform per-galaxy intensity multiplier, exposed in the settings panel. Lets you scale the whole sky up or down without re-uploading point data.
  • Camera-distance depth fade (toggle, default on) — fragment-stage alpha gate that multiplies by 1 / (1 + (camDist / FALLOFF_HALF)²), taming the additive-overlap glow at the geometric origin where every sightline through Earth stacks hundreds of billboards on top of each other.

Not the same thing as density correction

The next section ("Density correction (Malmquist bias)") describes a conceptually separate concern: compensating for the fact that flux-limited surveys systematically over-represent nearby galaxies (faint ones are only detectable when close). Density-correction modes do multiply into the same final per-pixel alpha as the brightness controls above, but the purpose is to correct what the catalogue under- or over-samples, not to tweak how an individual galaxy looks. Treat them as orthogonal: the brightness slider is a display preference; density correction is a scientific correction for selection bias.

Tone-mapping (covered in Render pipeline below) is a third orthogonal concern again — it operates on the accumulated HDR output of the entire frame, not on individual galaxies.

Density correction (Malmquist bias)

Flux-limited surveys over-represent nearby galaxies because faint ones are only detectable when close. Skymap offers four user-selectable correction modes via the settings panel:

  • None — raw catalogue, apparent over-density visible near origin.
  • Volume-limited (recommended) — show only galaxies brighter than a tunable absolute-magnitude threshold M_lim. Default M_lim = −19, matching SDSS's spectroscopic completeness near 750 Mpc. Honest: shows uniformly-detectable subsample.
  • 1/V_max alpha — keep all data, but dim each galaxy by its inverse maximum-detection volume. Schmidt 1968 weighting, applied as alpha rather than discard.
  • Schechter LF — modulate per-distance alpha by the inverse of the expected number density predicted by each survey's Schechter luminosity function. Most aggressive correction; visually flattens the local cluster into the cosmic web.

A separate angular-isotropy axis (orthogonal to the four modes above) addresses GLADE's deep pencil-beam artefacts:

  • GLADE isotropic build — when tools/buildAllBins.ts is run with --glade-isotropic, the parser drops GLADE rows whose only parent catalogue is SDSS-DR12 (which is footprint-restricted, ~1/3 of sky). Removes the radial "jet" structures that come from deep SDSS-only entries dominating outside their footprint.
  • HEALPix angular re-weighting (optional, runtime toggle) — bin the sky into HEALPix cells and modulate per-galaxy alpha by the ratio of median angular density to local angular density. Visually uniform direction-by-direction independent of which surveys contributed.

The flux-limit table (src/data/surveyFluxLimits.ts) hard-codes m_lim and (M*, α, φ*) per survey based on:

  • SDSS: Blanton et al. 2003 r-band LF; m_r ≤ 17.77 spec completeness.
  • 2MRS: Huchra et al. 2012 catalogue; K_s ≤ 11.75; Kochanek et al. 2001 K-band LF.
  • GLADE: B-band parent samples (HyperLEDA, GWGC); Norberg et al. 2002 b_J Schechter as the closest proxy.

Coordinate system

We use a right-handed equatorial Cartesian frame with distances in megaparsecs (Mpc):

  • +x → (RA = 0°, Dec = 0°) — vernal equinox direction
  • +y → (RA = 90°, Dec = 0°)
  • +z → Dec = +90° — celestial north pole

Distance from redshift uses Hubble's law: d = cz/H₀ with H₀ = 70 km/s/Mpc. This is the linear approximation — only accurate for z ≪ 1 but fine to a few percent for the SDSS spectroscopic galaxy sample (most z < 0.3).

Tests

npm test

Currently 707 tests across 95 files. Unit tests cover the pure modules: coordinate conversion (forward and inverse), the binary point-cloud format, the orbit camera, parsers, the derived-physics helpers, the data-tier subsampler, and the cloud-loader hot-swap path. The rendering pipeline and React UI are not unit-tested — they're verified visually in the browser.

Render pipeline

Visible draw passes (points, quads, disks) render into a rgba16float HDR offscreen target instead of straight to the swap chain. At the end of every frame, a fullscreen tone-map pass compresses the accumulated linear-light values into the swap chain's displayable range. Five curves are runtime-selectable from the SettingsPanel (Linear baseline, Reinhard-extended, Asinh / Lupton stretch, Gamma 2.0, ACES filmic); switching is a single 4-byte uniform write per frame, no pipeline rebuild. The pick renderer is on a separate r32uint integer target and is not tone-mapped. See docs/superpowers/plans/2026-05-04-hdr-tonemap.md for the full rationale and curve descriptions.

Architecture

src/
  @types/             Top-level type declarations (PointCloud, EngineHandle,
                      Tier, …)
  components/         React UI shell
    common/Panel/       Shared glass-card chrome reused by Navigation, Stats,
                        and the SettingsPanel outer frame
    App/                Root component + canvas mount + state plumbing
    SettingsPanel/      Tier selector, sliders, toggles, density modes
    InfoCard/           FullCard / CompactCard / Thumbnail
    NavigationPanel/    Static cheatsheet
    StatsPanel/         FPS + galaxy-count rollup
    StatusBar/          Engine lifecycle text
    ScaleBar/           Bottom-right Mpc scale legend
    CommandPalette/     Cmd-K famous-galaxy search
  data/               Static data definitions: sources enum, colour-index spec,
                      binary point-cloud format, tier-target table
  services/
    camera/           OrbitCamera, OrbitControls, focus tweens
    engine/           Top-level engine orchestrator, autoLod, cloud loader
                      (tier-aware, abortable hot-swap)
    gpu/              Renderers, texture atlas, image queue/fetcher, WGSL shaders
    input/            SpaceMouse + raw input → camera deltas
  styles/             global.css — design tokens (color, surface, type, spacing,
                      radius, motion) + page reset, loaded once at boot
  utils/              Pure helpers (math, format, random, initialTier) —
                      heavily tested

tools/
  buildAllBins.ts     Pipeline: parse raw catalogs → cross-match → emit per-tier
                      .bin variants (small / medium / large)
  buildFilaments.ts   Pipeline: read .bin catalogues → write DisPerSE TSV →
                      run delaunay_3D / mse / skelconv → encode FILA v1
  subsampleByAbsMag.ts  Tier subsampler (brightest-N by absolute magnitude)
  parsers/            SDSS CSV, 2MRS fixed-width, GLADE fixed-width parsers
  crossMatch.ts       Dedup logic across surveys
  fetch2massXsc.ts    Optional 2MASS XSC orientation enrichment
  fetchHyperLeda.ts   Optional HyperLEDA orientation enrichment
  fetchFamousImages.ts  DESI Legacy thumbnail processor for the Famous atlas
  buildFamous.ts        Famous-catalog binary + cross-ref encoder

data/raw/             Catalog source files + their VizieR ReadMes
tests/                Vitest suite, mirrors src/ tree

The split between the engine (in services/engine/) and the React tree is the core architectural choice: WebGPU and the per-frame loop are inherently imperative, so they live in a long-running engine that the React UI subscribes to via callbacks. React owns the DOM and the UI-relevant state slices (status, hovered, selected, scale); the engine owns everything that updates 60× per second.

Render scheduling: render-on-demand

The engine doesn't run a continuous render loop — frame() fires only when something has changed. Every event handler that mutates render-affecting state (mouse drag, wheel zoom, settings change, camera tween, image-queue completion, …) calls scheduler.requestRender(), which schedules exactly one rAF. Inside the frame body, after the GPU work is submitted, the tail re-schedules only when motion is in flight: autoRotate, an active camera tween, deflected SpaceMouse axes, pending thumbnail fetches, or recent thumbnail load-fade. Otherwise the loop pauses.

Idle CPU is effectively zero — no GPU encoding, no per-galaxy thumbnail-priority loop, no uniform writes.

The scheduler abstraction lives in src/services/engine/renderScheduler.ts and is unit-tested independently of WebGPU.

Browser binary format (SKMP v4)

Little-endian, 16-byte header (magic = "SKMP", version = 4, count, reserved) followed by count × 64 bytes per point:

offset  size  field
──────  ────  ─────
0       8     objID            (uint64)
8       12    xyz              (3 × float32, Mpc)
20      20    magU/G/R/I/Z     (5 × float32)
40      4     axisRatio        (float32, b/a in [0,1] or NaN)
44      4     positionAngleDeg (float32, PA in [0,180) or NaN)
48      4     diameterKpc      (float32, physical diameter or NaN)
52      12    padding          (zeroed; keeps records 16-byte aligned)

Old v1/v2/v3 files are no longer accepted — re-run npm run build-all to upgrade.

Roadmap

These are deliberately not in this version:

  • Comoving distance via ΛCDM integration — currently linear Hubble's law.
  • Spatial chunking + LOD for ≥10M points. The current architecture maxes out around 1–5M points before frame rate degrades. SDSS's full photometric catalog (~1B objects) needs an octree-based renderer.
  • Galactic-coordinate orientation (currently equatorial-aligned).
  • Picking on the photometric scale — same blocker as above.
  • Touch / mobile UX — WebGPU itself now runs in Safari 26+ (iOS 26 / iPadOS 26) and Chrome on Android 12+, but the orbit / pan / zoom controls are designed for a mouse and wheel. A proper touch-gesture layer (pinch-zoom, two-finger orbit) hasn't been built yet.
  • Firefox on Linux — still tracking through 2026; works in Nightly behind dom.webgpu.enabled in the meantime.

A note on AI-assisted development

CLAUDE.md at the repo root is onboarding guidance for AI coding assistants (Claude Code in particular). It's not load-bearing for the build or runtime — humans don't need to read it, and removing it wouldn't change anything that ships. It's there because parts of this project were developed with AI assistance and that context is useful for future AI-assisted edits.

How to cite

If you use Skymap in a publication, talk, or derived work, please cite it via the metadata in CITATION.cff — GitHub renders a "Cite this repository" button in the sidebar that exposes both BibTeX and APA forms automatically. Once a tagged release is minted on Zenodo, the DOI in CITATION.cff will resolve to a versioned archive.

The catalog data shown by the renderer (SDSS, 2MRS, GLADE, HyperLEDA, 2MASS XSC, Wikipedia, DESI Legacy) carries its own citation requirements — see ATTRIBUTIONS.md for the full list of papers each catalog asks be cited.

License

Skymap's source code is released under the MIT License — see LICENSE for the full text. Catalog data, imagery, and external service usage carry their own citation and licensing requirements (CC-BY-SA, public-domain, publication-citation, etc.) — see ATTRIBUTIONS.md for the full enumeration.

Camera focus

  • Focus button on a pinned galaxy's InfoCard pivots the camera onto that galaxy with a 600 ms ease-out tween. Yaw and pitch are preserved so you don't lose your orientation.
  • Home button (bottom-left, next to the Settings panel) returns the camera to its initial framing — origin target, default distance and pitch.
  • Keyboard shortcuts:
    • f — focus on the currently-pinned galaxy (no-op if nothing is pinned).
    • h — return to the home / Earth view.
    • Esc — clear the pinned selection.

Tweens are interrupted by mouse drag or wheel — manual orbit controls always take precedence over an in-progress focus.

SpaceMouse 6DOF input (optional)

If you have a 3Dconnexion SpaceMouse (Compact, Wireless, Pro, Enterprise, or the older Logitech-branded SpaceNavigator), Skymap can read its 6 axes directly via WebHID for a much smoother free-flight feel than mouse drag.

  • Open the Settings panel (bottom-left) and click Connect SpaceMouse. The browser prompts you to pick the device; pick yours and grant access.
  • Once paired, the permission persists across reloads — Skymap will silently re-acquire the device on every subsequent visit (no second prompt).
  • Adjust the Sensitivity slider to taste. The response curve is cubic, so small puck deflections give very fine motion and full deflections give fast camera moves regardless of slider position.

Axis mapping:

Puck motion Camera effect
Push left / right Pan target sideways
Push forward / back Pan target up / down
Pull up / push down Zoom (exponential, scale-invariant)
Tilt forward / back Pitch
Turn left / right Yaw
Twist Ignored (orbit camera has no roll)

Browser support: Chromium-only (Chrome, Edge, Brave, Opera). Firefox and Safari don't implement WebHID and the entire SpaceMouse section of the settings panel is hidden on those browsers — the rest of the app works exactly as before.