Welcome to the Beniverse
So I finally did it. After years of my projects being scattered across random GitHub repos and half-finished ideas living only on my laptop, I built myself a proper home on the internet.
I've been meaning to do this for... probably five years? Every few months I'd start a new portfolio site, get frustrated with it, and abandon it. This time I actually finished. Let me tell you about it.
Why a solar system though?
Honestly? I was tired of seeing the same portfolio layouts everywhere. You know the ones - hero section with a big photo of your face, about me paragraph, grid of project cards, contact form at the bottom. Maybe a parallax effect if you're feeling fancy.
They're fine. They work. But they're boring. I wanted something that felt more... me. Something weird. Something that would make people go 'huh, that's different' instead of immediately bouncing.
The solar system thing started as a joke while I was procrastinating on yet another abandoned portfolio attempt. 'What if my portfolio was literally my own universe?' I said to myself. And then I couldn't stop thinking about it. What if each section was a planet? What if they actually orbited? What if clicking on a star did something unexpected?
So here we are. The Beniverse. A solar system where you're the astronaut and my projects are celestial bodies. It's weird and I love it.
The design process (or: how many iterations is too many?)
The first version was a disaster. I went way too hard on the space theme - everything was rotating, pulsing, glowing. It looked like a sci-fi screensaver from 2003. Cool for about 30 seconds, then exhausting.
Version two swung too far the other way. Minimal to the point of boring. Just circles and text. No personality. I showed it to a friend who said 'it looks like a PowerPoint template' and I knew they were right.
Version three is what you're seeing now. I found the balance between 'interesting' and 'actually usable'. The orbits are subtle. The animations are smooth but not distracting. The starfield adds atmosphere without stealing focus. It took me about two weeks of tweaking timing functions and easing curves to get the motion to feel natural instead of mechanical.
The hardest part was figuring out how to make it work on mobile. Orbiting planets are great on a big screen, but on a phone? They just don't fit. I went through probably a dozen different mobile layouts before settling on the current stacked approach. The planets still feel planetary, just... vertically aligned. Sometimes constraints force better solutions.
The tech behind it
Since this is a dev blog, you're probably curious about the stack. Here's what I'm using:
- Backend: Python with Starlette (not FastAPI - I'll explain why in another post)
- Frontend: Vanilla JavaScript. Yes, really. No React, no build step.
- Styling: Plain CSS with CSS custom properties for theming
- Animations: CSS animations for the orbits, requestAnimationFrame for the starfield
- Data: JSON files. No database needed for a portfolio.
I went intentionally minimal. Every dependency is a liability, and I wanted this site to be something I could maintain for years without worrying about npm packages breaking.
The orbit animation technique
Getting the planets to orbit smoothly was trickier than I expected. My first attempt used absolute positioning with JavaScript updating the x and y coordinates on every frame. It worked but felt janky - especially on lower-end devices.
Then I remembered that CSS transforms are GPU-accelerated. So instead of moving elements around the DOM, I used CSS animations with transforms:
@keyframes orbit {
from {
transform: rotate(0deg) translateX(300px) rotate(0deg);
}
to {
transform: rotate(360deg) translateX(300px) rotate(-360deg);
}
}
.planet {
animation: orbit 20s linear infinite;
}
The trick is the double rotation. The first rotate() spins the element around the origin. The translateX() moves it outward. The second rotate() with the negative value keeps the planet upright while it orbits. It's like those carnival rides where you spin in a circle while your car also spins - but controlled and pleasant.
Each planet gets a different animation duration to create varied orbital speeds. The inner "planets" orbit faster, which mirrors actual physics even though this isn't a real solar system. It just feels right.
Building the starfield
The starfield background was fun to implement. I wanted thousands of stars, but I didn't want to create thousands of DOM elements - that would tank performance.
Instead, I use a single HTML5 canvas element and redraw the stars on every frame:
class Starfield {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.stars = this.generateStars(2000);
this.resize();
this.animate();
}
generateStars(count) {
return Array.from({ length: count }, () => ({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
size: Math.random() * 2,
opacity: Math.random() * 0.5 + 0.5,
twinkleSpeed: Math.random() * 0.02
}));
}
animate() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.stars.forEach(star => {
star.opacity += Math.sin(Date.now() * star.twinkleSpeed) * 0.01;
star.opacity = Math.max(0.3, Math.min(1, star.opacity));
this.ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
this.ctx.beginPath();
this.ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
this.ctx.fill();
});
requestAnimationFrame(() => this.animate());
}
}
The twinkling effect comes from varying each star's opacity using a sine wave. Different twinkleSpeed values create that organic, random twinkling you see in a real night sky. It's a simple effect but it makes the background feel alive.
One gotcha: I had to be careful with requestAnimationFrame. If you're not careful, it's easy to create memory leaks or performance issues. Always store your animation frame ID and cancel it when needed:
this.animationId = requestAnimationFrame(() => this.animate());
// Later, when cleaning up:
cancelAnimationFrame(this.animationId);
On choosing minimalism
There's this tendency in web dev to reach for frameworks by default. React for interactivity, Tailwind for styling, TypeScript for safety, bundlers for... bundling. Before you know it, you've got 500MB of node_modules for a site that displays some text and images.
I wanted to prove to myself that I could build something interesting without all that. Not because frameworks are bad - I use them at work and they're great for complex apps. But for a portfolio? It felt like overkill.
Vanilla JavaScript is actually really good now. The DOM APIs we used to need jQuery for are built-in and work great. CSS has variables, grid, flexbox, and powerful animation capabilities. You don't need Sass anymore. You definitely don't need a CSS-in-JS library.
Building without frameworks also means the site loads instantly. No JavaScript bundle to parse. No hydration step. Just HTML, CSS, and a small script file. It's fast in a way that modern web apps just... aren't.
Plus, there's something satisfying about opening the developer tools and actually understanding every line of code running on the page. It's all mine. No black boxes, no magic, no 'well that's just how the framework works'.
Modern vanilla JavaScript patterns
Here's the thing most developers don't realize: vanilla JavaScript has caught up. A lot of the pain points that made jQuery and frameworks necessary just don't exist anymore.
Need to select elements? document.querySelector() and document.querySelectorAll() work exactly like jQuery's $(). They accept CSS selectors, they're fast, and they're built-in:
// jQuery-style
const button = $('.submit-button');
// Modern vanilla JS - exactly as concise
const button = document.querySelector('.submit-button');
Event delegation? That was always possible with vanilla JS, but modern addEventListener makes it clean:
// Handle clicks on dynamically added elements
document.addEventListener('click', (e) => {
if (e.target.matches('.planet')) {
navigateToSection(e.target.dataset.section);
}
});
Making HTTP requests used to mean XMLHttpRequest and a lot of callback hell. Now we have fetch:
async function loadBlogPost(slug) {
try {
const response = await fetch(`/api/blog/${slug}`);
if (!response.ok) throw new Error('Post not found');
const post = await response.json();
return post;
} catch (error) {
console.error('Failed to load post:', error);
showErrorMessage();
}
}
It's clean, it uses promises, it handles errors well. No axios needed.
CSS custom properties changed everything
One of my favorite modern CSS features is custom properties (CSS variables). They make theming trivial:
:root {
--bg-primary: #0a0e27;
--text-primary: #e0e0e0;
--accent: #4a9eff;
--orbit-color: rgba(255, 255, 255, 0.1);
}
[data-theme="light"] {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
--accent: #0066cc;
--orbit-color: rgba(0, 0, 0, 0.1);
}
body {
background: var(--bg-primary);
color: var(--text-primary);
}
Toggle between themes by changing a single attribute on the root element:
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.dataset.theme || 'dark';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.dataset.theme = newTheme;
localStorage.setItem('theme', newTheme);
}
The browser handles all the color transitions automatically if you add transition: all 0.3s ease; to the elements. No JavaScript color calculations, no theme provider context, no style re-computation. It just works.
What's actually here
Click on the planets orbiting around - each one takes you somewhere:
-
About: The stuff you'd expect. Who I am, what I do, how to reach me. The obligatory professional headshot. You know the drill.
-
Projects: Side projects I've built over the years. Some are useful tools I still use daily. Some are games. Some are experiments that went nowhere but taught me something.
-
Resume: My actual work history, for recruiters and hiring managers who ended up here somehow. It's also downloadable as a PDF if you're into that sort of thing.
-
Blog: You're reading it! Mostly dev stuff - Python, JavaScript, tooling, opinions about software. Occasional tangents into game dev and whatever else I'm thinking about.
-
Experiments: Interactive toys and demos I've built into this site. Things that don't fit anywhere else. Some are useful, most are just fun.
The little details
I spent way too much time on details that most people won't notice. But that's kind of the point, right? The details are what make something feel polished instead of slapped together.
Try clicking on some of the stars in the background. You're welcome. Each one shows a random quote - some are actual things I've said, some are thoughts about programming, some are just silly. I wanted the starfield to be more than decoration.
There are keyboard shortcuts for navigation. Press ? to see them. Arrow keys move between sections. Numbers jump directly to planets. Escape closes modals. I'm a terminal person at heart, so keyboard navigation was non-negotiable.
The orbits have different speeds and directions, like a real solar system (well, sort of). The inner planets move faster than the outer ones. I spent an embarrassing amount of time reading about Kepler's laws of planetary motion just to get the speed ratios feeling right, even though nobody will ever notice.
The whole thing is responsive. It works on phones. The planets stack vertically on small screens and the orbits adjust. I'm unreasonably proud of getting that to work without a single media query library or responsive framework.
There's also a dark mode toggle (check the top right) that smoothly transitions between color schemes. The theme preference persists across sessions. Small touch, but it matters.
Oh, and if you inspect the HTML, there are comments scattered throughout. Some are helpful, some are jokes, some are notes to my future self. Consider them easter eggs for developers.
Performance optimization lessons
Building something visually complex taught me a lot about web performance. Here are the things that actually mattered:
1. Use CSS transforms instead of positional properties
Animating top and left triggers layout recalculation on every frame. Animating transform uses the GPU and is buttery smooth:
// Slow - causes layout thrashing
element.style.left = x + 'px';
element.style.top = y + 'px';
// Fast - GPU accelerated
element.style.transform = `translate(${x}px, ${y}px)`;
2. Debounce expensive operations
Window resize events fire constantly while someone's dragging their browser window. Don't recalculate everything on every single event:
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
recalculateLayout();
}, 150);
});
3. Use will-change sparingly
The will-change CSS property tells the browser to optimize for changes to specific properties. But overusing it actually hurts performance because the browser pre-allocates resources:
/* Good - only on elements that will actually animate */
.planet:hover {
will-change: transform;
}
/* Bad - wastes memory on static elements */
* {
will-change: transform;
}
4. Intersection Observer for lazy loading
I use Intersection Observer to only render complex animations when they're actually visible:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
startAnimation(entry.target);
} else {
pauseAnimation(entry.target);
}
});
});
document.querySelectorAll('.animated-section').forEach(section => {
observer.observe(section);
});
This cut CPU usage in half when users have the site open in a background tab.
5. Reduce paint areas
Every time the browser paints, it costs performance. Using transform and opacity for animations is fast because they can be composited without repainting:
/* Fast - composite layer */
.fade-in {
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn {
to { opacity: 1; }
}
/* Slower - triggers repaint */
.fade-in-bad {
visibility: hidden;
animation: showElement 0.3s ease forwards;
}
@keyframes showElement {
to { visibility: visible; }
}
Common vanilla JS gotchas
If you're coming from React or Vue, there are some gotchas with vanilla JavaScript that'll bite you:
Event listeners aren't automatically cleaned up
In React, event listeners get removed when components unmount. In vanilla JS, you're responsible for cleanup:
class NavigationController {
constructor() {
this.handleKeyPress = this.handleKeyPress.bind(this);
document.addEventListener('keydown', this.handleKeyPress);
}
handleKeyPress(e) {
// Handle the event
}
destroy() {
// Clean up when you're done!
document.removeEventListener('keydown', this.handleKeyPress);
}
}
Forgetting to remove listeners is a classic memory leak.
NodeList is not an array
querySelectorAll returns a NodeList, which looks like an array but isn't:
const planets = document.querySelectorAll('.planet');
// This doesn't work
planets.map(p => p.classList.add('active')); // Error!
// Convert to array first
Array.from(planets).map(p => p.classList.add('active')); // Works
// Or use forEach, which NodeList supports
planets.forEach(p => p.classList.add('active')); // Also works
Dynamic content and event delegation
If you add elements after page load, events won't be attached unless you use delegation:
// Won't work for dynamically added buttons
document.querySelectorAll('.button').forEach(btn => {
btn.addEventListener('click', handleClick);
});
// This works for current AND future buttons
document.addEventListener('click', (e) => {
if (e.target.matches('.button')) {
handleClick(e);
}
});
Why having a home on the internet matters
This might sound overly sentimental, but I think everyone who creates things should have their own space on the internet. Not a LinkedIn profile, not a Twitter bio, not a portfolio on someone else's platform. A place that's actually yours.
Social media is rented land. Platforms change their algorithms, their layouts, their entire business models. Your content gets buried, reformatted, or just disappears when the platform dies. Remember GeoCities? MySpace? Google+? Hundreds of thousands of personal sites, just gone.
Having your own site means you control everything. The design, the content, how people experience it. If I want to add a silly easter egg or write a 3000-word essay about something obscure, I can. No character limits, no engagement algorithms, no 'you should post more videos' suggestions.
It's also a forcing function for actually finishing things. Side projects that live only on my laptop tend to stay there forever. But a site? A site demands content. It's a container that needs filling. This blog post exists because I needed something to put in the blog section.
Plus, there's something permanent about it. Ten years from now, this site will still be here (assuming I keep paying for the domain). The design might change, the content will grow, but it's mine in a way that no social media presence ever is.
Lessons for building your own portfolio
If you're thinking about building a portfolio site, here's what I learned:
Start with constraints. I gave myself three rules: no frameworks, no build process, must work offline. Constraints breed creativity. Without them, I would've bikeshedded for months about React vs Vue vs Svelte.
Build the weird version. Everyone has the same portfolio layout. If you have a weird idea that makes you smile, build that instead. The worst case is it doesn't work and you rebuild it normally. The best case is you make something memorable.
Performance matters. Your portfolio is often the first code sample someone sees from you. If it loads slowly or feels janky, that's the first impression you're making. Treat it like production code.
Make it yours. Add easter eggs, jokes, personal touches. A portfolio that looks like it came from a template tells recruiters nothing about you. The weird details are what make it interesting.
Ship it before it's perfect. I could've spent another six months tweaking animations and adding features. But shipping an imperfect thing is infinitely better than perfecting something nobody sees. You can always iterate.
Write about the process. This blog post exists because building the site gave me something to write about. Your portfolio should be a conversation starter, not just a resume in HTML form.
The backend: why Starlette instead of FastAPI
Quick sidebar on the backend choice. I mentioned using Starlette instead of FastAPI. Here's why:
FastAPI is fantastic for APIs. But it's built on top of Starlette, and for a simple site that mostly serves HTML, I didn't need the extra features. FastAPI adds automatic API documentation, request validation with Pydantic, and dependency injection. All great features - for an API.
For a portfolio site? I just need to serve templates and static files. Starlette gives me routing, middleware, and templating with zero magic:
from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.templating import Jinja2Templates
from starlette.staticfiles import StaticFiles
templates = Jinja2Templates(directory="templates")
async def homepage(request):
return templates.TemplateResponse("index.html", {"request": request})
async def blog_post(request):
slug = request.path_params['slug']
post = get_blog_post(slug)
return templates.TemplateResponse("blog_post.html", {
"request": request,
"post": post
})
routes = [
Route('/', homepage),
Route('/blog/{slug}', blog_post),
Mount('/static', StaticFiles(directory='static'), name='static'),
]
app = Starlette(routes=routes)
It's minimal, fast, and does exactly what I need. No more, no less. Plus, the smaller dependency tree means fewer things to update when security patches come out.
On finishing projects
Here's the thing nobody tells you about side projects: finishing them is a skill you have to learn.
I have a graveyard of unfinished projects. A chess engine that never got past move validation. A game engine that could draw sprites but not much else. A task manager that was going to revolutionize productivity (spoiler: it wouldn't have).
They all failed for the same reason: I didn't scope them properly. I'd start with grand visions and lose momentum when reality hit.
This site succeeded because I broke it into tiny pieces:
- Make a circle rotate - done
- Make multiple circles rotate at different speeds - done
- Make them clickable - done
- Make them navigate somewhere - done
- Add content to those pages - done
Each piece was small enough to finish in one sitting. Each gave me a dopamine hit of completion. That's how you build momentum.
The other key: I gave myself permission to ship something imperfect. The first version I deployed had broken CSS on some screen sizes, no dark mode, and placeholder text in half the sections. But it was live. Public. That forced me to actually finish it instead of perpetually tweaking in private.
What's next
I've got ideas for more stuff to add. An interactive terminal experiment. Maybe a game built right into the site. Definitely more blog posts - I've got a backlog of half-written drafts about Python packaging, SQLite, and why I think everyone should build useless projects.
The experiments section is particularly exciting. I want to build weird, interactive things that don't belong anywhere else. Maybe a visualizer for sorting algorithms. Maybe a procedural planet generator. Maybe just a button that does something surprising when you click it.
I'm also thinking about ways to make the space theme more immersive. What if the orbits responded to your mouse? What if planets had gravitational effects on each other? What if you could build your own solar system? These are probably terrible ideas, but I'll try them anyway.
But for now, I'm just happy this thing exists. It's live. It's weird. It's mine.
If you made it this far, thanks for reading. I hope this gave you some ideas for your own projects, or at least convinced you that vanilla JavaScript isn't as painful as you remember.
Thanks for stopping by. Poke around, break things, let me know what you think. My email's on the About page, or you can find me on the usual platforms.
Welcome to the Beniverse.