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.
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.
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.
| Role | Typeface | Why |
|---|---|---|
| Headlines & body | Space Grotesk | A 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 / labels | JetBrains Mono | Tabular 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 animatesyPercent: 110 → 0on the span so the text wipes up from behind a hard mask instead of just fading in. - Scroll reveals — every content block has a
.revealclass (opacity 0, translateY 28px at rest). AScrollTrigger.create({start:'top 85%', once:true})per element flips an.is-inclass, 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 withpower2.outeasing, writing the formatted number intotextContenton 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
visibilitychangelistener 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-motionis honored twice: CSS disables.revealtransforms 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.