An interactive WebGPU 3D explorer for the SDSS, GLADE, and 2MRS galaxy catalogs — fly through millions of galaxies in your browser.
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.
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.
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.
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, 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, roughly 200–400 Mpc across — thousands of galaxies clustering into a dense core with filaments radiating into surrounding voids.
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.
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.
- 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.
-
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.
npm install
npm run devOpen 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.
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.
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.
| 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.
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.3Choose 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 byfracDeV_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.
npm run build-all -- \
--sdss "data/Skyserver_SQL.csv" \
--twomrs data/raw/2mrs_table3.dat \
--glade data/raw/glade2.3.dat \
--out-dir public/dataOmit 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.
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 galaxiesfetch-2mass-xscqueries the 2MASS Extended Source Catalog and writesdata/raw/2mass_xsc_pa.csv. Quick — runs in roughly five minutes.fetch-hyperledaqueries HyperLEDA at 4 concurrent requests across ~1.5 M PGCs and writesdata/raw/hyperleda_pa.csv. Takes roughly 1 hour end-to-end. The script is resumable — interrupt and restart safely.
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.gzThe 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.
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.
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. |
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):
npm run build-all— produces2mrs.bin+glade.bin, which the famous build needs for cross-match.npm run fetch-famous-images— downloads + processes 20 thumbnails (~30 s). Idempotent; pass--forceto re-fetch.npm run build-famous— producesfamous.bin+famous_meta.json+famous_xrefs.json.
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.
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.
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.
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):
- Install DisPerSE following its upstream instructions; ensure
delaunay_3D,mse, andskelconvare on$PATH. - Run
npm run build-allfirst so the.bincatalogues exist. - 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.
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]viaclamp((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.
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.
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.tsis 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.
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).
npm testCurrently 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.
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.
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.
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.
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.
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.enabledin the meantime.
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.
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.
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.
- 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.
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.







