← DEVLOG
인터랙티브2025.12.066 min read

행성 포지 — 나만의 행성을 만드는 시뮬레이터

크기·색상·링·위성을 직접 조합해 행성을 만들고 저장하는 커스터마이저 — Canvas 렌더링과 localStorage 영속화

canvascustomizationlocalstorageinteractive

시작 — 내 행성을 만들고 싶다

태양계 행성들은 저마다 개성이 있습니다. 토성의 거대한 고리, 목성의 소용돌이 대적반, 해왕성의 짙은 파란빛. 그 개성들을 조합해 완전히 새로운 행성을 만드는 도구가 있다면 어떨까 생각했습니다.

행성 포지는 슬라이더와 색상 피커만으로 나만의 행성을 디자인하고, 그 설정을 저장해두는 커스터마이저입니다.


행성 파라미터 설계

행성을 구성하는 파라미터는 크게 세 그룹입니다.

interface PlanetSettings {
  // 기본
  size: number; // 6 ~ 100 (반지름 px 기준)
  color: string; // 표면 기본색
  highlight: string; // 하이라이트 포인트 색상
  glowColor: string; // 외곽 발광 색상
  // 링
  hasRing: boolean;
  ringColor: string;
  ringOpacity: number; // 0 ~ 1
  ringRotateX: number; // -80 ~ -20 (도)
  ringRotateZ: number; // -30 ~ 30 (도)
  // 위성
  moons: MoonSettings[];
}

링의 기울기는 rotateXrotateZ 두 축으로 제어합니다. 토성처럼 거의 수평으로 놓인 링, 천왕성처럼 세로로 선 링 모두 표현 가능합니다.


Canvas 구체 렌더링 — radial gradient

행성의 구체감은 createRadialGradient로 만듭니다. 핵심은 하이라이트 포인트를 중심에서 오프셋하는 것입니다.

const grad = ctx.createRadialGradient(
  cx - r * 0.3,
  cy - r * 0.3,
  0, // 하이라이트 중심 (좌상단 오프셋)
  cx,
  cy,
  r, // 행성 중심, 반지름
);
grad.addColorStop(0, highlight); // 밝은 하이라이트 포인트
grad.addColorStop(0.5, color); // 기본 표면색
grad.addColorStop(1, darken(color, 0.5)); // 경계 어두운 음영

외곽 glow는 별도 radialGradient로 반지름 1.8배까지 뻗어나가는 레이어를 먼저 그린 뒤, 그 위에 구체를 덮습니다.


링 시스템 — CSS perspective 타원

링은 CSS transform으로 표현합니다. rotateX로 타원형 원근감을 주고, rotateZ로 기울임을 적용합니다.

// 링 div 스타일
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)`,
};

링이 행성 앞뒤를 가로지르는 효과는 z-index 레이어를 행성 앞/뒤 두 개로 분리하여 구현했습니다. 앞쪽 반원은 행성 위에, 뒤쪽 반원은 행성 아래 렌더링됩니다.


위성 궤도 애니메이션

각 위성은 공전 반지름, 속도, 크기, 색상을 개별 설정합니다. 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;
  // 뒤쪽 위성은 행성 렌더 전, 앞쪽 위성은 행성 렌더 후 그리기
  drawMoon(ctx, mx, my, moon.size, moon.color, angle);
});

ORBIT_Y_RATIO를 0.4로 설정해 궤도를 타원형으로 보이게 합니다. 위성이 행성 뒤로 사라졌다 나타나는 효과는 angle % (Math.PI * 2) > Math.PI 조건으로 앞/뒤를 판단해 그리기 순서를 제어합니다.


localStorage 영속화

설정은 통합 스토리지(storageSet)로 저장합니다. 새로고침해도 마지막으로 만든 행성이 그대로 살아있습니다.

// 저장: 슬라이더 변경 시마다 debounce 300ms 후 저장
const saveSettings = useMemo(() => debounce((s: PlanetSettings) => storageSet('planet-forge', s), 300), []);

// 복원: 초기 상태 직접 주입
const [settings, setSettings] = useState<PlanetSettings>(() => storageGet('planet-forge', DEFAULT_PLANET_SETTINGS));

리셋 버튼을 누르면 storageRemove('planet-forge')로 저장된 설정을 지우고 기본값으로 돌아갑니다.


마치며

슬라이더 하나를 움직일 때 행성이 즉시 반응하는 것 — 그 즉각성이 이 도구의 핵심이라고 생각합니다. 저장 버튼을 따로 누르지 않아도 자동으로 영속화되기 때문에 사용자는 설정 관리가 아니라 행성 디자인 자체에만 집중할 수 있습니다.

한 번 만든 행성이 다음번에 들어와도 그 자리에 있다는 것 — 작지만 그 연속성이 꽤 중요하다고 느꼈습니다.

이 포스트와 연결된 콘텐츠

직접 체험하기