Build Guide

How Aphelion was designed and built.

A specific, no-fluff walkthrough of the concept, the art direction behind the imagery, the real Three.js orbit scene under the hood, the GSAP techniques driving the motion, and how a build like this ships as a static site.

10 MIN READ PLAIN HTML / CSS / JS THREE.JS + GSAP NO BUILD STEP

Concept & direction

Aphelion is a fictional Series B orbital-infrastructure startup — the brief called for something adjacent to SpaceX, Planet Labs and Varda: real mission-control seriousness, not a sci-fi movie poster. The name itself is an orbital-mechanics term (the point in an orbit farthest from the sun), which set the tone before a single pixel was drawn: precise, technical, confident, no exclamation points.

The visual language leans on three ideas borrowed from real flight-ops software rather than sci-fi concept art: monospace data readouts (lat/long, velocity, mission clock — all ticking in real time, computed client-side from performance.now()), corner-bracket HUD framing on hero and imagery (a technique lifted from camera viewfinders and targeting overlays), and a strictly two-accent lighting model — cyan and violet only — so every glow, rim light and border reads as one coherent lighting rig rather than a decorative rainbow.

The single hardest constraint was restraint: it would have been easy to over-animate a space site into a video-game loading screen. Every animation here is tied to either scroll position or a real orbital calculation — nothing spins just to spin.

Palette & type

Colors are defined once as CSS custom properties in css/style.css and used everywhere — no hard-coded hex outside that block, which is what makes the two-accent rule actually enforceable across 400+ lines of CSS.

Void#050608
Panel#0a0d16
Cyan#3fe4ff
Violet#9b6bff
Amber (status only)#ffb454
Text mid#a4adc2

Amber appears in exactly one place — the pulsing "live" status dot — because real HUDs almost always reserve a warm color for status/alert states, and using it anywhere else would dilute that signal.

RoleTypefaceWhy
Headlines & bodySpace GroteskA geometric grotesque with just enough character to feel designed, not corporate-default; "Space" in the name was a happy coincidence, not the reason it was picked.
HUD / data / labelsJetBrains MonoTabular figures keep the ticking lat/long/velocity readouts from jittering in width as digits change; wide letter-spacing on labels mimics real telemetry UI.

Generated imagery

The 3D globe is real code (see below) — it was never going to be a generated image. Everything else — hardware, environments, the mission-control interior — was generated with Nano Banana Pro via the Higgsfield MCP, art-directed toward photographic product-render logic (real lighting setups, not "space background with stars" clip-art) and the same cyan/violet rim-light pair used everywhere else on the site, so generated photography and CSS/WebGL elements read as one lighting universe.

Each of the six images — the hero satellite, the Earth night side, the mission-control interior, the cubesat formation, the ground station, and the hardware macro — was generated from a carefully art-directed brief specifying the lighting rig, materials, composition, and mood, iterated until it sat inside the site's two-accent lighting universe. The full prompt library is proprietary and available to clients on request.

All six were generated at 2K and re-encoded to JPEG (quality ~78, capped at 1800px wide) with sips to keep the asset folder under 2MB total — there's no reason a marketing page needs 7MB PNGs when the layout never displays an image above ~900px wide.

The orbit scene

This is the one non-negotiable technical centerpiece, built in js/globe.js with plain Three.js (no bundler, loaded from a CDN script tag) against a full-viewport <canvas id="orbit-canvas"> that sits position: fixed behind the entire page (z-index: 0), so it acts as a living background for every section, not just the hero.

Earth core

Instead of a texture-mapped photographic Earth (which would have fought the HUD aesthetic), the globe is a SphereGeometry wrapped in a procedurally drawn canvas texture: a 2D canvas context draws 24 meridians and 12 parallels as a cyan grid, plus ~46 soft violet ellipse "landmass" blobs at random positions, then that canvas is handed to Three.js as a CanvasTexture. It reads as mission-control cartography rather than a stock globe.

Atmosphere fresnel shell

A second, slightly larger sphere uses a custom ShaderMaterial rendered with side: THREE.BackSide. The fragment shader computes a fresnel term — pow(1.05 - dot(normal, viewDir), 2.6) — so the glow is near-zero facing the camera and intensifies toward the silhouette edge, then mixes cyan and violet by world-space y so the rim light itself has a gradient, matching the two-point-light setup used in every generated photo.

Orbital rings and satellites

Three ring "planes" are built from THREE.EllipseCurve sampled into 128 points, rendered as LineLoop, then each ring group is rotated on X and Z to fake orbital inclination — this is the same trick real orbit-visualizer tools use instead of true 3D ellipse math, and it's indistinguishable at this scale. Small satellite groups (a box "bus" plus two plane "solar panels" and a point light) walk along their ring every frame with angle += delta * speed, get transformed by the ring's tilt via Vector3.applyEuler, and call lookAt(0,0,0) so they stay nose-in toward Earth — real orbital motion, not a looping animation clip.

Scroll and mouse coupling

Two inputs drive the camera every frame: window.scrollY (normalized 0–1 against document height) dollies the camera back and rotates the whole orbit group up to 0.6π, and pointermove feeds a lerped parallax offset into camera X position and a slight Earth tilt. Both are eased with simple value += (target - value) * 0.04–0.06 exponential smoothing rather than GSAP tweens, since this runs inside the render loop itself and needs to stay cheap at 60fps.

GSAP & motion

Three distinct GSAP techniques do almost everything outside the canvas:

  • Hero line reveal — each headline line is wrapped in .line (overflow:hidden) containing an inline <span>; GSAP animates yPercent: 110 → 0 on the span so the text wipes up from behind a hard mask instead of just fading in.
  • Scroll reveals — every content block has a .reveal class (opacity 0, translateY 28px at rest). A ScrollTrigger.create({start:'top 85%', once:true}) per element flips an .is-in class, and the actual transition lives in CSS, not inline GSAP tweens — keeps the JS declarative and the transitions easy to retune from CSS alone.
  • Count-up stats — elements tagged data-count="182400" animate a plain object's value from 0 to the target with power2.out easing, writing the formatted number into textContent on every tick, gated behind its own ScrollTrigger so numbers only start counting once visible.

HUD corner brackets are small inline SVGs with a hand-drawn path; GSAP sets strokeDasharray and animates strokeDashoffset from 60 to 0 on load, so the corners draw themselves in rather than appearing instantly — a small detail, but it's the difference between "HUD-themed" and actually feeling like an instrument booting up.

Responsive & performance

The layout is CSS Grid/Flexbox with fluid type via clamp() throughout, checked at 375px and at desktop widths. The nav collapses to a slide-in panel under 900px with a CSS-only hamburger (three spans rotated into an X via aria-expanded) toggled by ~15 lines of JS.

The 3D scene is the one part that actually changes behavior on small screens, not just layout:

  • Device pixel ratio is capped at 1.5 under 760px and 2 above it, instead of the raw (often 3×) DPR phones report.
  • Star count drops from 2200 to 900 points; satellite count per ring drops from 2–3 to 1–2; sphere segment counts drop from 64 to 40.
  • A visibilitychange listener halts the render loop entirely when the tab isn't visible, and delta time is clamped to 50ms so returning from a background tab can't cause the orbit to "jump."
  • prefers-reduced-motion is honored twice: CSS disables .reveal transforms outright, and the orbit scene slows satellite speed by 65% rather than freezing the whole canvas (freezing a WebGL canvas mid-render can look broken; a slow, ambient orbit reads as intentional).

Deployment

Aphelion has zero build step — it's static HTML/CSS/JS with two CDN script tags (Three.js, GSAP + ScrollTrigger) and a Google Fonts <link>. Shipping it is a drag-and-drop: push the aphelion/ folder to a Git repo and connect it to Netlify with no build command and aphelion/ as the publish directory (or drag the folder straight into Netlify's deploy UI for a one-off). Because every asset path is relative (assets/…, css/…, js/…), the same folder also works unmodified from any static host — GitHub Pages, Cloudflare Pages, or a plain S3 bucket — without touching a single path.

← Back to the Aphelion site