Civilization Star Map
Designing an interactive constellation where each star is a node of human knowledge — and making it work on every screen.
The Metaphor
Most portfolio sites arrange content in grids. Cards, lists, categories — legible, functional, forgettable.
What if each topic was a star instead? Not placed on a grid, but orbiting through a shared sky — drifting in and out of view like a living constellation.
That’s the idea behind the Civilization Star Map: mapping knowledge the way ancient civilizations mapped the heavens. Each star is a node. Each orbit is a relationship. The whole system breathes.

From Dots to Orbits
The first version was scattered dots that faded in and out. Felt like a screensaver.
The breakthrough: physics. Real celestial bodies follow orbits. So each star got:
- An elliptical path with unique eccentricity
- An orbital inclination (tilted orbit plane)
- A unique angular velocity — some drift slowly, others sweep through
- Depth — closer stars glow brighter, distant ones fade
The sky went from “random dots” to “something with intent.”
When motion follows physical plausibility — even simplified — our brains stop seeing “animation” and start seeing “behavior.”
The 2.5D Layer
Flat orbits felt predictable. Adding a Z-axis changed everything.
Foreground stars glow, cast box-shadows, respond to hover. Background stars fade to near-invisible, creating parallax. And a subtle Lissajous wobble makes each path slightly irregular — like gravitational tugs from unseen bodies:
const y = rawZ * 20 * Math.sin(star.inclination)
+ Math.sin(theta * 1.3 + star.nodeAngle) * 12 // primary wobble
+ Math.cos(theta * 0.7 + star.nodeAngle * 2) * 5 // secondary wobble
Three lines of math. Remove them and the motion immediately feels sterile. Keep them and most users won’t consciously notice — but the sky feels organic.
Interaction: Stars You Can Touch
Hover Pause
Hover a foreground star and two things happen:
- The entire sky freezes — all motion stops
- A tooltip card appears with the star’s identity
The pause is deliberate. A moment of stillness in a constantly-moving scene. It says: this one matters.

The Pointer-Events Trick
The star map sits behind the hero text. Text must be readable. Stars must be hoverable. These conflict.
The fix is CSS layering:
.hero-content {
pointer-events: none; /* mouse passes through text */
}
.hero-buttons {
pointer-events: auto; /* buttons stay clickable */
}
Your cursor passes through the title and reaches the stars behind it. Only buttons intercept. Feels magical.
One Sky, Every Screen
The hardest part wasn’t the math — it was the phone.
On desktop, stars orbit wide with overflow-visible, extending beyond their column. On a 375px screen, that same behavior creates a horizontal scrollbar.
The solution: one component, responsive container — no conditional rendering, no separate mobile build.
<div class="
absolute inset-0 overflow-hidden
lg:relative lg:overflow-visible lg:min-h-[500px]
">

Tablet — atmospheric background

Mobile — clean, no scrollbar
Mobile: absolute background, overflow-hidden clips at container edge.
Desktop: grid column, overflow-visible lets stars roam free. Page-level overflow-x-hidden catches them at the viewport.
Same animation. Same component. CSS does the rest.
Performance: 60fps with 125 Elements
Setting left and top every frame forces the browser to recalculate layout — 125 elements × 60fps = 7,500 layout passes per second. That’s jank.
The fix: transform: translate(). Transforms skip layout entirely and run on the GPU.
// ❌ Layout reflow every frame
el.style.left = `calc(50% + ${x}%)`;
// ✅ GPU compositing only
el.style.transform = `translate(${tx}px, ${ty}px)`;
Container dimensions are cached via ResizeObserver — no DOM queries inside the animation loop.
Animate with transform. Measure with ResizeObserver. Never read layout properties inside requestAnimationFrame.
What’s Next
- Click a star → enter its constellation (topic page)
- Dynamic data — stars generated from content, not hardcoded
- Constellation lines — connections between related stars
The sky is literally not the limit.
Built with React, TypeScript, and an unreasonable amount of time watching dots move across a screen.