Skip to content

About

Scroll-driven WebGL journey from a single star to the Laniakea Supercluster and back — the Astronamer marketing site. Next.js 16, React Three Fiber, three.js.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Astronamer — marketing site

A scroll-driven WebGL journey from a single star out to the Laniakea Supercluster and back down onto one named star, built to the brief in Astronamer_Website_Design_Prompt.md.

The site is not a page describing the app. It is a compressed run of the app's own universe-to-single-star navigation, in the browser, before you install anything.

npm run dev     # http://localhost:3000
npm run build   # fully static export — no server runtime needed
npm run lint
npx tsc --noEmit

Stack, and why

Choice Why
Next.js 16 (App Router) The whole page prerenders to static HTML. Copy, metadata and the CSS sky are painted before a byte of three.js parses.
React 19 + React Three Fiber 9 Declarative scene graph. The 3D reads as components with props, not as an imperative graph you have to hold in your head.
three.js Everything is a custom ShaderMaterial and a GPU point cloud. No model loader, no physics, no scene format.
GSAP + ScrollTrigger The only scroll library that measures reliably across mobile browser-chrome resize. Free in GSAP 3.13+.
Zustand UI state only. Per-frame state deliberately does not live here — see below.
Tailwind v4 Layout and spacing utilities. Design tokens live in @theme in globals.css; bespoke visuals are hand-written CSS classes prefixed u-.
TypeScript, strict

The site ships no 3D model files, no image assets, and no audio files. Stars, galaxies, nebulae, the certificate face and the ambient audio bed are all generated at runtime. That is not a shortcut — it is why the whole thing loads fast, scales cleanly to any resolution, and cannot break because a file failed to deploy.


The two ideas that make it work

1. The scale ladder

You cannot travel from 8 light-years to 520 million light-years in float32. The depth buffer falls apart long before you get there.

So the camera never really goes anywhere. It stays inside a small, well-conditioned envelope, and the world scales around it. Three "cosmic levels" sit at the same origin:

Level center Renders 1:1 when
Local star field 0 at the hero and every close-up section
Milky Way 1 mid pull-back
Laniakea Supercluster 2 the widest view

Each is scaled by SCALE_STEP^(center − scale) and neighbours cross-fade, so exactly one is at natural size at a time. It reads as a powers-of-ten zoom while every vertex stays within ~1e3 units of the camera.

src/animations/journey.ts · src/scenes/CosmicLevel.tsx

2. Scroll never re-renders React

Two kinds of state, kept strictly apart:

  • frame (src/animations/state.ts) — a plain mutable object. Scroll and pointer listeners write to it; useFrame callbacks read it. Never triggers a render. At 60fps a store update per frame would re-render the entire overlay tree.
  • useAppStore — ordinary Zustand, for things that genuinely change the UI: boot phase, active section, audio on/off, the claimed star name.

Position on the journey is one number: u = sectionIndex + progressWithinSection, so u ∈ [0, 7] across the seven sections. Every camera keyframe, every object's visibility window, and the warp transition are all placed on that one axis.

Because section heights in the DOM set the pacing, copy can grow without retuning a single camera value.


File map

src/
  animations/
    journey.ts             The camera keyframes. Read top to bottom = the whole film.
    state.ts               frame (mutable, per-frame) + useAppStore (React state)
    useJourneyScroll.ts    ScrollTrigger → u.  One trigger per section.
    useViewportFade.ts     Beat copy that fades in and back out
    usePointerParallax.ts  Cursor + device tilt
  scenes/
    UniverseCanvas.tsx     The single persistent <Canvas> + boot/precompile
    Universe.tsx           The whole world in one graph
    CameraRig.tsx          Scroll → damped camera
    CosmicLevel.tsx        One rung of the scale ladder
    StarField.tsx          GPU point cloud — hero field, galaxy disc, halo
    GalaxyCluster.tsx      ~1,600 galaxies in one instanced draw call
    NamedStar.tsx          Clickable star + floating name card
    CertificateCard.tsx    3D card, face drawn to canvas at runtime
    AudioReactiveHalo.tsx  Ring displaced by the narration spectrum
    Constellation.tsx      Hairline figures that trace themselves on
    Atmosphere.tsx         Nebula dust + the warp-streak veil
    Effects.tsx            Tiered post-processing
    visibility.ts          Fade windows on the u axis
    shaders/               All GLSL
  ui/
    SiteShell.tsx          Orchestrates everything
    Section.tsx            Section shell + shared type primitives
    sections/              One file per section of the brief
    LoadingScreen.tsx      "Calibrating telescope…" — real progress
    StaticFallback.tsx     CSS sky, static hero, re-enable-3D control
    Nav / Footer / AudioControl / CometCursor
  lib/
    deviceTier.ts          Device probing + per-tier budgets
    audioEngine.ts         Synthesised ambience, chimes, narration analyser
    useMediaQuery.ts       Reduced motion, via useSyncExternalStore
  content/copy.ts          ALL site copy, in one file
  assets/higgsfield.ts     Generated-asset slot manifest

Performance

src/lib/deviceTier.ts probes the device once on the client — reduced-motion preference, WebGL availability, unmasked GPU string, core count, device memory, save-data, viewport — and picks a tier. Every scene reads its counts from TIER_BUDGET rather than hard-coding.

static low mid high
Local stars — 3,500 7,000 14,000
Galaxy stars — 9,000 32,000 85,000
Cluster galaxies — 220 700 1,600
Bloom — ✓ ✓ ✓
Depth of field — — — ✓
Chromatic aberration — — ✓ ✓
MSAA — — — ✓
Max DPR 1 1.25 1.6 2

static skips WebGL entirely and renders the CSS starfield. It is selected for prefers-reduced-motion, no WebGL context, or a software rasteriser.

Shader precompilation. Before the loading screen lifts, BootSequence forces every level visible, hands the whole graph to gl.compileAsync, then steps u through all twelve keyframes so each level renders at least once and the driver uploads its buffers. Compiling a shader when a section first scrolls into view is exactly the stutter this site cannot afford.

AdaptiveDpr also drops resolution at runtime if the frame budget slips, with a floor of 0.8x (performance={{ min: 0.8 }}) — unbounded it degrades hardest exactly at the galaxy beat, which reads as the page suddenly going pixelated.

Counts are lower than they look like they should be on purpose. Once every star carries a real magnitude and a sub-pixel alpha fade, brightness distribution sells the field, not raw density — and every extra point is an additive, depth-write-free fragment on the heaviest frame in the site.


Tuning the camera

Scrubbing a 10,000px page by hand to check one keyframe is miserable, so dev builds expose a console handle (stripped from production):

__astronamer.goto(1.625)   // jump to any point on the journey axis, 0–7
__astronamer.u             // where you are now
__astronamer.triggers()    // what each section trigger measured
__astronamer.refresh()     // re-measure after a layout change

Useful positions: 0 hero · 1.375 local field · 1.625 Milky Way · 1.875 supercluster · 2.1 mid-warp · 2.45 certificate · 4 narration halo · 7 bookend.

__astronamerScene gives you { gl, scene, camera, advance } for inspecting what the camera is actually looking at.

Responsive behaviour

  • Layout. Single column below lg. The naming panel drops under its copy; tier cards, arcade cabinets and testimonials all stack.
  • Camera framing. Keyframes carry a lateral offset that pushes the 3D subject to one side so DOM copy can own the other half. Below 900px that offset is multiplied by 0 and subjects re-centre, because the layout has stacked and there is no side to clear.
  • Parallax. Cursor on desktop; device tilt on mobile. iOS gates DeviceOrientation behind a gesture, so permission is requested on the first pointerdown, never on load.
  • Comet cursor is off for coarse pointers.
  • Section heights are set in svh, not vh, so mobile browser chrome collapsing does not resize the journey mid-scroll.
  • Sound control sits above the safe-area inset.
  • Verified with no horizontal overflow at 375, 768 and 1440.

Accessibility

  • Every fact the canvas renders also exists as real text in the DOM. The canvas itself is aria-hidden — it is decorative duplication, not content.
  • prefers-reduced-motion drops to the static path. A visible Enable 3D universe control lets anyone opt back in — reduced motion should mean "off by default", not "unavailable".
  • Narration ships captions plus a full transcript, both available whether or not audio is playing.
  • Audio is muted by default with an explicit unmute. Nothing touches the AudioContext until the visitor presses it.
  • Skip link, one h1, ordered headings, labelled controls, visible focus rings, aria-live on the caption and the loading status.

Assets

Nothing is required to run. Two drop zones, both with their own README:

  • public/assets/higgsfield/ — every slot the site expects, declared in src/assets/higgsfield.ts with exact filename, aspect, duration, the component that consumes it, and prompt direction. Drop a file in and it appears; every consumer falls back to procedural art when it is missing.
  • public/audio/ — only the narration sample. Ambience and chimes are synthesised.

Before launch

  • Replace testimonials, the registry counter, ratings and press logos in src/content/copy.ts — all marked PLACEHOLDER.
  • Confirm the pricing tiers match the real product.
  • Point SITE_URL in src/app/layout.tsx and DOWNLOAD_URL in src/app/page.tsx at production.
  • Real App Store / Play Store links in copy.ts.
  • Drop og-hero.jpg — social cards are currently 404ing.
  • Record and drop public/audio/narration-sirius.mp3, then update the caption cue points.
  • Have legal read naming.disclosure and footer.disclosure. Keep them honest. The IAU does not sell star names and no registry entry changes that. Being the star registry that says so plainly is the position this entire site is built on — do not let it get softened into the same copy every competitor runs.

About

Scroll-driven WebGL journey from a single star to the Laniakea Supercluster and back — the Astronamer marketing site. Next.js 16, React Three Fiber, three.js.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages