← DEVLOG
Interactive2025.12.066 min read

Planet Forge — A Simulator for Building Your Own Planet

A planet customizer where you combine size, color, rings, and moons to build and save your own world — Canvas rendering with localStorage persistence

canvascustomizationlocalstorageinteractive

The Idea — A Planet That Is Entirely Yours

Every planet in our solar system has a personality. Saturn's enormous rings, Jupiter's Great Red Spot, Neptune's deep cobalt blue. I wanted to build a tool that lets you combine those traits any way you like — and create something entirely new.

Planet Forge is a customizer. Sliders and color pickers only. Your planet, your design.


Parameter Design

The planet's configuration is split into three groups.

interface PlanetSettings {
  // Base
  size: number; // 6 ~ 100 (radius in px)
  color: string; // surface base color
  highlight: string; // specular highlight color
  glowColor: string; // outer glow color
  // Ring
  hasRing: boolean;
  ringColor: string;
  ringOpacity: number; // 0 ~ 1
  ringRotateX: number; // -80 ~ -20 (degrees)
  ringRotateZ: number; // -30 ~ 30 (degrees)
  // Moons
  moons: MoonSettings[];
}

Ring tilt is controlled by two axes — rotateX and rotateZ. This covers everything from Saturn's nearly horizontal rings to Uranus-style vertical ones.


Canvas Sphere Rendering — Radial Gradient

The spherical look is created with createRadialGradient. The key is offsetting the highlight center away from the sphere center.

const grad = ctx.createRadialGradient(
  cx - r * 0.3,
  cy - r * 0.3,
  0, // highlight point (upper-left offset)
  cx,
  cy,
  r, // sphere center, radius
);
grad.addColorStop(0, highlight); // bright highlight
grad.addColorStop(0.5, color); // base surface color
grad.addColorStop(1, darken(color, 0.5)); // dark shadow at edge

The outer glow is a separate radialGradient layer extending to 1.8× the planet radius, drawn first, then the sphere is rendered on top.


Ring System — CSS Perspective Ellipse

Rings are rendered in CSS using transform. rotateX creates the elliptical perspective, and rotateZ applies the tilt angle.

const ringStyle: CSSProperties = {
  width: `${size * RING_W_RATIO}px`,
  height: `${size * RING_H_RATIO}px`,
  borderRadius: '50%',
  border: `${ringThickness}px solid ${ringColor}`,
  opacity: ringOpacity,
  transform: `rotateX(${ringRotateX}deg) rotateZ(${ringRotateZ}deg)`,
};

The effect of the ring passing in front of and behind the planet is achieved by splitting the ring into two z-index layers — front semicircle on top of the planet, back semicircle beneath it.


Moon Orbit Animation

Each moon has its own orbital radius, speed, size, and color, calculated per frame in polar coordinates on the Canvas.

moons.forEach((moon, i) => {
  const angle = moonAngles[i] + elapsed * moon.speed;
  const mx = cx + Math.cos(angle) * moon.orbitR;
  const my = cy + Math.sin(angle) * moon.orbitR * ORBIT_Y_RATIO;
  // Moons behind planet: draw before planet; moons in front: draw after
  drawMoon(ctx, mx, my, moon.size, moon.color, angle);
});

ORBIT_Y_RATIO is set to 0.4 to make orbits appear elliptical. The pass-behind effect is controlled by draw order: angle % (Math.PI * 2) > Math.PI determines which side of the planet the moon is on.


localStorage Persistence

Settings are saved through the unified storage utility. Your planet survives page refreshes.

// Save: debounced 300ms after each slider change
const saveSettings = useMemo(() => debounce((s: PlanetSettings) => storageSet('planet-forge', s), 300), []);

// Restore: inject directly as initial state
const [settings, setSettings] = useState<PlanetSettings>(() => storageGet('planet-forge', DEFAULT_PLANET_SETTINGS));

The reset button calls storageRemove('planet-forge') and reverts to defaults.


Closing Thoughts

The core of this tool is immediacy — move a slider and the planet responds instantly. No save button required; persistence happens automatically in the background.

That continuity matters more than it might seem. Coming back to find your planet exactly as you left it makes the thing feel less like a demo and more like a place.

Content related to this post

Try it yourself