Solar System Motion

It's how our solar system is moving.

// 01·LIVE PREVIEW
Interactive stage
// 01.B·PARAMETERS
Theme
Target
Speed
1x
Pitch
36°
Angle
48°
Streak Turns
2.4 rev
Streaks
Orbits
Playing
// 02·USAGE
example.tsx
example.tsx
import { SolarSystemMotion } from "@/components/ui/solar-system-motion";

export default function Example() {
  return (
    <div className="w-full h-[500px]">
      <SolarSystemMotion
        colorMode="monochrome"
        speed={1.0}
        pitchDeg={36}
        angleDeg={48}
        showStreaks
      />
    </div>
  );
}
// 03·API REFERENCE
9 props

Prop

Type

// 04·COMPONENT SOURCE
2 files
SolarSystemMotion.tsx
"use client";

import {
  Camera,
  Color,
  Geometry,
  Mesh,
  Polyline,
  Program,
  Renderer,
  Sphere,
  Transform,
  Vec3,
} from "ogl";
import type React from "react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/cn";
import {
  CELESTIAL_PLANETS,
  DEFAULT_VORTEX_CONFIG,
  MOON_CONFIG,
} from "./motion-engine";

const STREAK_VERTEX_SHADER =  `
  precision highp float;

  attribute vec3 position;
  attribute vec3 next;
  attribute vec3 prev;
  attribute vec2 uv;
  attribute float side;

  uniform mat4 modelViewMatrix;
  uniform mat4 projectionMatrix;
  uniform vec2 uResolution;
  uniform float uDPR;
  uniform float uThickness;
  uniform float uMiter;

  varying vec2 vUv;

  vec4 getPosition() {
    mat4 mvp = projectionMatrix * modelViewMatrix;
    vec4 current = mvp * vec4(position, 1.0);
    vec4 nextPos = mvp * vec4(next, 1.0);
    vec4 prevPos = mvp * vec4(prev, 1.0);

    vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);    
    vec2 currentScreen = current.xy / current.w * aspect;
    vec2 nextScreen = nextPos.xy / nextPos.w * aspect;
    vec2 prevScreen = prevPos.xy / prevPos.w * aspect;

    vec2 dir1 = normalize(currentScreen - prevScreen);
    vec2 dir2 = normalize(nextScreen - currentScreen);
    vec2 dir = normalize(dir1 + dir2);

    vec2 normal = vec2(-dir.y, dir.x);
    normal /= mix(1.0, max(0.3, dot(normal, vec2(-dir1.y, dir1.x))), uMiter);
    normal /= aspect;

    float pixelWidthRatio = 1.0 / (uResolution.y / uDPR);
    float pixelWidth = current.w * pixelWidthRatio;
    normal *= pixelWidth * uThickness;
    current.xy -= normal * side;

    return current;
  }

  void main() {
    vUv = uv;
    gl_Position = getPosition();
  }
`;

const STREAK_FRAGMENT_SHADER =  `
  precision highp float;
  uniform vec3 uColor;
  uniform float uAlpha;
  varying vec2 vUv;

  void main() {
    float dist = abs(vUv.x - 0.5) * 2.0;
    float edge = smoothstep(1.0, 0.0, dist);
    float core = pow(1.0 - dist, 2.0);
    float tailFade = pow(max(0.0, 1.0 - vUv.y), 1.35);
    vec3 col = mix(uColor, vec3(1.0, 1.0, 1.0), core * 0.45);
    float alpha = edge * tailFade * uAlpha;
    gl_FragColor = vec4(col, alpha);
  }
`;

const STAR_VERTEX_SHADER =  `
  precision highp float;
  attribute vec3 position;
  attribute float aSize;
  attribute vec2 aTwinkle;
  uniform mat4 modelViewMatrix;
  uniform mat4 projectionMatrix;
  uniform float uTime;
  varying float vAlpha;

  void main() {
    float twinkle = 0.65 + 0.35 * sin(uTime * aTwinkle.x + aTwinkle.y);
    vAlpha = twinkle;
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = aSize * (220.0 / -mvPosition.z);
    gl_Position = projectionMatrix * mvPosition;
  }
`;

const STAR_FRAGMENT_SHADER =  `
  precision highp float;
  varying float vAlpha;

  void main() {
    float dist = length(gl_PointCoord - 0.5);
    if (dist > 0.5) discard;
    float alpha = smoothstep(0.5, 0.08, dist);
    gl_FragColor = vec4(1.0, 1.0, 1.0, alpha * vAlpha);
  }
`;

const SPHERE_VERTEX_SHADER =  `
  precision highp float;
  attribute vec3 position;
  attribute vec3 normal;
  attribute vec2 uv;
  uniform mat4 modelViewMatrix;
  uniform mat4 projectionMatrix;
  uniform mat3 normalMatrix;
  varying vec3 vNormal;
  varying vec3 vPosition;
  varying vec2 vUv;

  void main() {
    vNormal = normalize(normalMatrix * normal);
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    vPosition = mvPosition.xyz;
    vUv = uv;
    gl_Position = projectionMatrix * mvPosition;
  }
`;

const PLANET_FRAGMENT_SHADER =  `
  precision highp float;
  uniform vec3 uColor;
  uniform vec3 uGlowColor;
  varying vec3 vNormal;
  varying vec3 vPosition;

  void main() {
    vec3 normal = normalize(vNormal);
    vec3 lightDir = normalize(vec3(0.5, 0.8, 1.0));
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 viewDir = normalize(-vPosition);
    float fresnel = 1.0 - max(dot(viewDir, normal), 0.0);

    vec3 ambient = uColor * 0.45;
    vec3 diffuse = uColor * (0.55 * diff);
    vec3 halo = uGlowColor * (0.45 * pow(fresnel, 2.0));

    gl_FragColor = vec4(ambient + diffuse + halo, 1.0);
  }
`;

const SUN_FRAGMENT_SHADER =  `
  precision highp float;
  uniform vec3 uCoreColor;
  uniform vec3 uRimColor;
  varying vec3 vNormal;
  varying vec3 vPosition;

  void main() {
    vec3 normal = normalize(vNormal);
    vec3 viewDir = normalize(-vPosition);
    float fresnel = 1.0 - max(dot(viewDir, normal), 0.0);
    vec3 col = mix(uCoreColor, uRimColor, pow(fresnel, 1.5));
    gl_FragColor = vec4(col, 1.0);
  }
`;

const RING_VERTEX_SHADER =  `
  precision highp float;
  attribute vec3 position;
  attribute vec2 uv;
  uniform mat4 modelViewMatrix;
  uniform mat4 projectionMatrix;
  varying vec2 vUv;

  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const SATURN_RING_FRAGMENT_SHADER =  `
  precision highp float;
  uniform vec3 uRingColor;
  varying vec2 vUv;

  void main() {
    float dist = length(vUv - 0.5) * 2.0;
    if (dist < 0.48 || dist > 0.98) discard;
    float band = sin(dist * 60.0) * 0.12 + 0.88;
    float edge = smoothstep(0.48, 0.54, dist) * smoothstep(0.98, 0.90, dist);
    vec3 ringColor = uRingColor * band;
    gl_FragColor = vec4(ringColor, edge * 0.75);
  }
`;

const SIMPLE_LINE_VERTEX_SHADER =  `
  precision highp float;
  attribute vec3 position;
  uniform mat4 modelViewMatrix;
  uniform mat4 projectionMatrix;

  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const SIMPLE_LINE_FRAGMENT_SHADER =  `
  precision highp float;
  uniform vec3 uColor;
  uniform float uAlpha;

  void main() {
    gl_FragColor = vec4(uColor, uAlpha);
  }
`;

function hexToOglColor(hex: string): Color {
  return new Color(hex);
}

export const MONOCHROME_PLANET_PALETTE: Record<
  string,
  { color: string; trailColor: string; glowColor: string }
> = {
  mercury: { color: "#94a3b8", trailColor: "#ffffff", glowColor: "#cbd5e1" },
  venus: { color: "#f8fafc", trailColor: "#ffffff", glowColor: "#ffffff" },
  earth: { color: "#e2e8f0", trailColor: "#ffffff", glowColor: "#f1f5f9" },
  mars: { color: "#cbd5e1", trailColor: "#ffffff", glowColor: "#e2e8f0" },
  jupiter: { color: "#f1f5f9", trailColor: "#ffffff", glowColor: "#ffffff" },
  saturn: { color: "#e2e8f0", trailColor: "#ffffff", glowColor: "#f8fafc" },
  uranus: { color: "#cbd5e1", trailColor: "#ffffff", glowColor: "#e2e8f0" },
  neptune: { color: "#94a3b8", trailColor: "#ffffff", glowColor: "#cbd5e1" },
};

export interface SolarSystemMotionProps {
  speed?: number;
  pitchDeg?: number;
  angleDeg?: number;
  colorMode?: "monochrome" | "color";
  showStreaks?: boolean;
  showOrbits?: boolean;
  streakTurns?: number;
  isPlaying?: boolean;
  visiblePlanets?: string[];
  selectedPlanet?: string;
  className?: string;
}

export function SolarSystemMotion({
  speed = 1.0,
  pitchDeg = DEFAULT_VORTEX_CONFIG.pitchDeg,
  angleDeg = DEFAULT_VORTEX_CONFIG.angleDeg,
  colorMode = "monochrome",
  showStreaks = true,
  showOrbits = false,
  streakTurns = 2.4,
  isPlaying = true,
  visiblePlanets,
  selectedPlanet = "earth",
  className = "",
}: SolarSystemMotionProps) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  const isPlayingRef = useRef(isPlaying);
  useEffect(() => {
    isPlayingRef.current = isPlaying;
  }, [isPlaying]);

  const speedRef = useRef(speed);
  useEffect(() => {
    speedRef.current = speed;
  }, [speed]);

  const colorModeRef = useRef(colorMode);
  useEffect(() => {
    colorModeRef.current = colorMode;
  }, [colorMode]);

  const cameraPitchRef = useRef(pitchDeg);
  useEffect(() => {
    cameraPitchRef.current = pitchDeg;
  }, [pitchDeg]);

  const trajectoryAngleRef = useRef(angleDeg);
  useEffect(() => {
    trajectoryAngleRef.current = angleDeg;
  }, [angleDeg]);

  const showStreaksRef = useRef(showStreaks);
  useEffect(() => {
    showStreaksRef.current = showStreaks;
  }, [showStreaks]);

  const showOrbitsRef = useRef(showOrbits);
  useEffect(() => {
    showOrbitsRef.current = showOrbits;
  }, [showOrbits]);

  const streakTurnsRef = useRef(streakTurns);
  useEffect(() => {
    streakTurnsRef.current = streakTurns;
  }, [streakTurns]);

  const visiblePlanetIdsRef = useRef<Set<string>>(
    new Set(visiblePlanets || CELESTIAL_PLANETS.map((p) => p.id)),
  );
  useEffect(() => {
    if (visiblePlanets) {
      visiblePlanetIdsRef.current = new Set(visiblePlanets);
    }
  }, [visiblePlanets]);

  const selectedPlanetIdRef = useRef(selectedPlanet);
  useEffect(() => {
    selectedPlanetIdRef.current = selectedPlanet;
  }, [selectedPlanet]);

  const cameraYawRef = useRef(0);
  const zoomRef = useRef(1.0);
  const isDraggingRef = useRef(false);
  const lastMousePosRef = useRef({ x: 0, y: 0 });

  const handlePointerDown = (e: React.PointerEvent<HTMLCanvasElement>) => {
    isDraggingRef.current = true;
    lastMousePosRef.current = { x: e.clientX, y: e.clientY };
    (e.target as HTMLElement).setPointerCapture(e.pointerId);
  };

  const handlePointerMove = (e: React.PointerEvent<HTMLCanvasElement>) => {
    if (!isDraggingRef.current) return;
    const dx = e.clientX - lastMousePosRef.current.x;
    const dy = e.clientY - lastMousePosRef.current.y;
    lastMousePosRef.current = { x: e.clientX, y: e.clientY };

    cameraYawRef.current = (cameraYawRef.current + dx * 0.35) % 360;
    cameraPitchRef.current = Math.max(
      0,
      Math.min(85, cameraPitchRef.current - dy * 0.3),
    );
  };

  const handlePointerUp = (e: React.PointerEvent<HTMLCanvasElement>) => {
    isDraggingRef.current = false;
    try {
      (e.target as HTMLElement).releasePointerCapture(e.pointerId);
    } catch {}
  };

  const handleWheel = (e: React.WheelEvent<HTMLCanvasElement>) => {
    const zoomDelta = e.deltaY * -0.0012;
    zoomRef.current = Math.max(0.4, Math.min(2.5, zoomRef.current + zoomDelta));
  };

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const renderer = new Renderer({
      canvas,
      dpr: Math.min(window.devicePixelRatio || 1, 2),
      alpha: true,
      antialias: true,
      preserveDrawingBuffer: true,
      powerPreference: "high-performance",
    });
    const gl = renderer.gl;
    gl.clearColor(0.0392, 0.0392, 0.0392, 1.0);

    const scene = new Transform();
    const camera = new Camera(gl, { fov: 42, near: 0.1, far: 2000 });

    const starCount = 380;
    const starPositions = new Float32Array(starCount * 3);
    const starSizes = new Float32Array(starCount);
    const starTwinkle = new Float32Array(starCount * 2);

    for (let i = 0; i < starCount; i++) {
      const u = Math.random();
      const v = Math.random();
      const theta = u * 2.0 * Math.PI;
      const phi = Math.acos(2.0 * v - 1.0);
      const r = 60 + Math.random() * 80;

      starPositions[i * 3 + 0] = r * Math.sin(phi) * Math.cos(theta);
      starPositions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
      starPositions[i * 3 + 2] = r * Math.cos(phi);

      starSizes[i] = Math.random() > 0.85 ? 2.2 : 1.2;
      starTwinkle[i * 2 + 0] = 0.8 + Math.random() * 2.5;
      starTwinkle[i * 2 + 1] = Math.random() * Math.PI * 2;
    }

    const starGeom = new Geometry(gl, {
      position: { size: 3, data: starPositions },
      aSize: { size: 1, data: starSizes },
      aTwinkle: { size: 2, data: starTwinkle },
    });

    const starProg = new Program(gl, {
      vertex: STAR_VERTEX_SHADER,
      fragment: STAR_FRAGMENT_SHADER,
      uniforms: {
        uTime: { value: 0 },
      },
      transparent: true,
      depthWrite: false,
    });
    starProg.setBlendFunc(gl.SRC_ALPHA, gl.ONE);

    const starMesh = new Mesh(gl, {
      mode: gl.POINTS,
      geometry: starGeom,
      program: starProg,
    });
    starMesh.setParent(scene);

    const SAMPLES = 140;
    const allPolylines: Polyline[] = [];

    const sunStreakPtsCore: Vec3[] = [];
    const sunStreakPtsAura: Vec3[] = [];
    const sunStreakPtsCenter: Vec3[] = [];
    for (let i = 0; i < SAMPLES; i++) {
      sunStreakPtsCore.push(new Vec3(i * 0.05, 0, 0));
      sunStreakPtsAura.push(new Vec3(i * 0.05, 0, 0));
      sunStreakPtsCenter.push(new Vec3(i * 0.05, 0, 0));
    }

    const sunStreakAura = new Polyline(gl, {
      points: sunStreakPtsAura,
      vertex: STREAK_VERTEX_SHADER,
      fragment: STREAK_FRAGMENT_SHADER,
      uniforms: {
        uThickness: { value: 16.0 },
        uColor: {
          value: new Color(
            colorModeRef.current === "monochrome" ? "#ffffff" : "#f59e0b",
          ),
        },
        uAlpha: { value: colorModeRef.current === "monochrome" ? 0.25 : 0.4 },
      },
    });
    sunStreakAura.program.transparent = true;
    sunStreakAura.program.depthWrite = false;
    sunStreakAura.program.setBlendFunc(gl.SRC_ALPHA, gl.ONE);
    sunStreakAura.mesh.setParent(scene);

    const sunStreakCore = new Polyline(gl, {
      points: sunStreakPtsCore,
      vertex: STREAK_VERTEX_SHADER,
      fragment: STREAK_FRAGMENT_SHADER,
      uniforms: {
        uThickness: { value: 6.0 },
        uColor: {
          value: new Color(
            colorModeRef.current === "monochrome" ? "#f8fafc" : "#fbbf24",
          ),
        },
        uAlpha: { value: colorModeRef.current === "monochrome" ? 0.75 : 0.9 },
      },
    });
    sunStreakCore.program.transparent = true;
    sunStreakCore.program.depthWrite = false;
    sunStreakCore.program.setBlendFunc(gl.SRC_ALPHA, gl.ONE);
    sunStreakCore.mesh.setParent(scene);

    const sunStreakCenter = new Polyline(gl, {
      points: sunStreakPtsCenter,
      vertex: STREAK_VERTEX_SHADER,
      fragment: STREAK_FRAGMENT_SHADER,
      uniforms: {
        uThickness: { value: 2.2 },
        uColor: { value: new Color("#ffffff") },
        uAlpha: { value: 1.0 },
      },
    });
    sunStreakCenter.program.transparent = true;
    sunStreakCenter.program.depthWrite = false;
    sunStreakCenter.program.setBlendFunc(gl.SRC_ALPHA, gl.ONE);
    sunStreakCenter.mesh.setParent(scene);

    allPolylines.push(sunStreakAura, sunStreakCore, sunStreakCenter);

    const planetPolylines = CELESTIAL_PLANETS.map((planet) => {
      const pts: Vec3[] = [];
      for (let i = 0; i < SAMPLES; i++) {
        pts.push(new Vec3(i * 0.05, 0, 0));
      }

      const initTrailColor =
        colorModeRef.current === "monochrome"
          ? MONOCHROME_PLANET_PALETTE[planet.id]?.trailColor || "#ffffff"
          : planet.trailColor;

      const pLine = new Polyline(gl, {
        points: pts,
        vertex: STREAK_VERTEX_SHADER,
        fragment: STREAK_FRAGMENT_SHADER,
        uniforms: {
          uThickness: {
            value:
              planet.id === "jupiter" || planet.id === "saturn" ? 4.2 : 3.0,
          },
          uColor: { value: hexToOglColor(initTrailColor) },
          uAlpha: { value: 0.9 },
        },
      });
      pLine.program.transparent = true;
      pLine.program.depthWrite = false;
      pLine.program.setBlendFunc(gl.SRC_ALPHA, gl.ONE);
      pLine.mesh.setParent(scene);
      allPolylines.push(pLine);
      return pLine;
    });

    const moonPts: Vec3[] = [];
    for (let i = 0; i < SAMPLES; i++) {
      moonPts.push(new Vec3(i * 0.05, 0, 0));
    }
    const moonPolyline = new Polyline(gl, {
      points: moonPts,
      vertex: STREAK_VERTEX_SHADER,
      fragment: STREAK_FRAGMENT_SHADER,
      uniforms: {
        uThickness: { value: 2.0 },
        uColor: {
          value: hexToOglColor(
            colorModeRef.current === "monochrome"
              ? "#cbd5e1"
              : MOON_CONFIG.trailColor,
          ),
        },
        uAlpha: { value: 0.8 },
      },
    });
    moonPolyline.program.transparent = true;
    moonPolyline.program.depthWrite = false;
    moonPolyline.program.setBlendFunc(gl.SRC_ALPHA, gl.ONE);
    moonPolyline.mesh.setParent(scene);
    allPolylines.push(moonPolyline);

    const unitSphereGeom = new Sphere(gl, {
      radius: 1,
      widthSegments: 20,
      heightSegments: 14,
    });

    const sunProg = new Program(gl, {
      vertex: SPHERE_VERTEX_SHADER,
      fragment: SUN_FRAGMENT_SHADER,
      uniforms: {
        uCoreColor: {
          value: new Color(
            colorModeRef.current === "monochrome"
              ? [1.0, 1.0, 1.0]
              : [1.0, 0.98, 0.92],
          ),
        },
        uRimColor: {
          value: new Color(
            colorModeRef.current === "monochrome"
              ? [0.82, 0.85, 0.9]
              : [0.98, 0.72, 0.12],
          ),
        },
      },
    });
    const sunMesh = new Mesh(gl, {
      geometry: unitSphereGeom,
      program: sunProg,
    });
    sunMesh.scale.set(1.4, 1.4, 1.4);
    sunMesh.setParent(scene);

    const planetMeshes = CELESTIAL_PLANETS.map((planet) => {
      const initColor =
        colorModeRef.current === "monochrome"
          ? MONOCHROME_PLANET_PALETTE[planet.id]?.color || "#e2e8f0"
          : planet.color;
      const initGlow =
        colorModeRef.current === "monochrome"
          ? MONOCHROME_PLANET_PALETTE[planet.id]?.glowColor || "#ffffff"
          : planet.trailColor;

      const pProg = new Program(gl, {
        vertex: SPHERE_VERTEX_SHADER,
        fragment: PLANET_FRAGMENT_SHADER,
        uniforms: {
          uColor: { value: hexToOglColor(initColor) },
          uGlowColor: { value: hexToOglColor(initGlow) },
        },
      });
      const pMesh = new Mesh(gl, { geometry: unitSphereGeom, program: pProg });
      const scale = Math.max(0.24, planet.sizePx * 0.065);
      pMesh.scale.set(scale, scale, scale);
      pMesh.setParent(scene);
      return pMesh;
    });

    const saturnRingGeom = new Geometry(gl, {
      position: {
        size: 3,
        data: new Float32Array([
          -1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0,
        ]),
      },
      normal: {
        size: 3,
        data: new Float32Array([
          0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
        ]),
      },
      uv: {
        size: 2,
        data: new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]),
      },
    });
    const saturnRingProg = new Program(gl, {
      vertex: RING_VERTEX_SHADER,
      fragment: SATURN_RING_FRAGMENT_SHADER,
      uniforms: {
        uRingColor: {
          value: new Color(
            colorModeRef.current === "monochrome"
              ? [0.92, 0.94, 0.96]
              : [0.96, 0.88, 0.62],
          ),
        },
      },
      transparent: true,
      depthWrite: false,
      cullFace: false,
    });
    const saturnRingMesh = new Mesh(gl, {
      geometry: saturnRingGeom,
      program: saturnRingProg,
    });
    saturnRingMesh.scale.set(2.4, 2.4, 1.0);
    saturnRingMesh.rotation.x = Math.PI * 0.35;
    const saturnIdx = CELESTIAL_PLANETS.findIndex((p) => p.id === "saturn");
    if (saturnIdx >= 0) {
      saturnRingMesh.setParent(planetMeshes[saturnIdx]);
    }

    const moonProg = new Program(gl, {
      vertex: SPHERE_VERTEX_SHADER,
      fragment: PLANET_FRAGMENT_SHADER,
      uniforms: {
        uColor: {
          value: hexToOglColor(
            colorModeRef.current === "monochrome"
              ? "#f8fafc"
              : MOON_CONFIG.color,
          ),
        },
        uGlowColor: { value: hexToOglColor("#ffffff") },
      },
    });
    const moonMesh = new Mesh(gl, {
      geometry: unitSphereGeom,
      program: moonProg,
    });
    moonMesh.scale.set(0.14, 0.14, 0.14);
    moonMesh.setParent(scene);

    const ORBIT_SEGS = 64;
    const orbitCirclePositions = new Float32Array(ORBIT_SEGS * 3);
    for (let i = 0; i < ORBIT_SEGS; i++) {
      const th = (i / ORBIT_SEGS) * Math.PI * 2;
      orbitCirclePositions[i * 3 + 0] = Math.cos(th);
      orbitCirclePositions[i * 3 + 1] = Math.sin(th);
      orbitCirclePositions[i * 3 + 2] = 0;
    }
    const orbitGeom = new Geometry(gl, {
      position: { size: 3, data: orbitCirclePositions },
    });
    const orbitProg = new Program(gl, {
      vertex: SIMPLE_LINE_VERTEX_SHADER,
      fragment: SIMPLE_LINE_FRAGMENT_SHADER,
      uniforms: {
        uColor: { value: new Color("#94a3b8") },
        uAlpha: { value: 0.16 },
      },
      transparent: true,
      depthWrite: false,
    });

    const orbitGroup = new Transform();
    orbitGroup.setParent(scene);

    const orbitMeshes = CELESTIAL_PLANETS.map((planet) => {
      const oMesh = new Mesh(gl, {
        mode: gl.LINE_LOOP,
        geometry: orbitGeom,
        program: orbitProg,
      });
      const r3D = planet.radiusAU * 0.045;
      oMesh.scale.set(r3D, r3D, 1.0);
      oMesh.setParent(orbitGroup);
      return oMesh;
    });

    const targetRingProg = new Program(gl, {
      vertex: SIMPLE_LINE_VERTEX_SHADER,
      fragment: SIMPLE_LINE_FRAGMENT_SHADER,
      uniforms: {
        uColor: {
          value: new Color(
            colorModeRef.current === "monochrome" ? "#ffffff" : "#38bdf8",
          ),
        },
        uAlpha: { value: 0.9 },
      },
      transparent: true,
      depthWrite: false,
    });
    const targetRingMesh = new Mesh(gl, {
      mode: gl.LINE_LOOP,
      geometry: orbitGeom,
      program: targetRingProg,
    });
    targetRingMesh.setParent(scene);

    const handleResize = () => {
      if (!canvas) return;
      const width = Math.max(
        300,
        canvas.parentElement?.clientWidth || canvas.clientWidth || 800,
      );
      const height = Math.max(
        240,
        canvas.parentElement?.clientHeight ||
          canvas.clientHeight ||
          Math.round((width * 9) / 16),
      );
      renderer.setSize(width, height);
      camera.perspective({ aspect: width / height });
      for (const p of allPolylines) {
        p.resize();
      }
    };

    handleResize();
    const resizeObserver = new ResizeObserver(handleResize);
    if (canvas.parentElement) {
      resizeObserver.observe(canvas.parentElement);
    }
    window.addEventListener("resize", handleResize);

    let animId: number;
    let lastTime = performance.now();
    let activeColorMode = colorModeRef.current;
    let simTime = 2.0;

    const loop = (timestamp: number) => {
      const dt = Math.min((timestamp - lastTime) / 1000, 0.1);
      lastTime = timestamp;

      if (activeColorMode !== colorModeRef.current) {
        activeColorMode = colorModeRef.current;
        const isMono = activeColorMode === "monochrome";

        if (isMono) {
          sunProg.uniforms.uCoreColor.value.set(1.0, 1.0, 1.0);
          sunProg.uniforms.uRimColor.value.set(0.82, 0.85, 0.9);
          sunStreakAura.program.uniforms.uColor.value.set("#ffffff");
          sunStreakAura.program.uniforms.uAlpha.value = 0.25;
          sunStreakCore.program.uniforms.uColor.value.set("#f8fafc");
          sunStreakCore.program.uniforms.uAlpha.value = 0.75;
          sunStreakCenter.program.uniforms.uColor.value.set("#ffffff");
          sunStreakCenter.program.uniforms.uAlpha.value = 1.0;
          saturnRingProg.uniforms.uRingColor.value.set(0.92, 0.94, 0.96);
          targetRingProg.uniforms.uColor.value.set("#ffffff");

          CELESTIAL_PLANETS.forEach((planet, idx) => {
            const mono = MONOCHROME_PLANET_PALETTE[planet.id] || {
              color: "#e2e8f0",
              trailColor: "#ffffff",
              glowColor: "#ffffff",
            };
            planetMeshes[idx].program.uniforms.uColor.value.set(mono.color);
            planetMeshes[idx].program.uniforms.uGlowColor.value.set(
              mono.glowColor,
            );
            planetPolylines[idx].program.uniforms.uColor.value.set(
              mono.trailColor,
            );
          });

          moonMesh.program.uniforms.uColor.value.set("#f8fafc");
          moonMesh.program.uniforms.uGlowColor.value.set("#ffffff");
          moonPolyline.program.uniforms.uColor.value.set("#cbd5e1");
        } else {
          sunProg.uniforms.uCoreColor.value.set(1.0, 0.98, 0.92);
          sunProg.uniforms.uRimColor.value.set(0.98, 0.72, 0.12);
          sunStreakAura.program.uniforms.uColor.value.set("#f59e0b");
          sunStreakAura.program.uniforms.uAlpha.value = 0.4;
          sunStreakCore.program.uniforms.uColor.value.set("#fbbf24");
          sunStreakCore.program.uniforms.uAlpha.value = 0.9;
          sunStreakCenter.program.uniforms.uColor.value.set("#ffffff");
          sunStreakCenter.program.uniforms.uAlpha.value = 1.0;
          saturnRingProg.uniforms.uRingColor.value.set(0.96, 0.88, 0.62);
          targetRingProg.uniforms.uColor.value.set("#38bdf8");

          CELESTIAL_PLANETS.forEach((planet, idx) => {
            planetMeshes[idx].program.uniforms.uColor.value.set(planet.color);
            planetMeshes[idx].program.uniforms.uGlowColor.value.set(
              planet.trailColor,
            );
            planetPolylines[idx].program.uniforms.uColor.value.set(
              planet.trailColor,
            );
          });

          moonMesh.program.uniforms.uColor.value.set(MOON_CONFIG.color);
          moonMesh.program.uniforms.uGlowColor.value.set("#ffffff");
          moonPolyline.program.uniforms.uColor.value.set(
            MOON_CONFIG.trailColor,
          );
        }
      }

      if (isPlayingRef.current) {
        simTime += dt * speedRef.current * 1.5;
      }
      const curTime = simTime;

      starProg.uniforms.uTime.value = curTime;

      const angleRad = (trajectoryAngleRef.current * Math.PI) / 180;
      const dirX = Math.cos(angleRad);
      const dirY = Math.sin(angleRad);
      const uX = -Math.sin(angleRad);
      const uY = Math.cos(angleRad);

      const L = 28.0;
      const speedAU = 4.2;
      const sunX = L * 0.28 * dirX;
      const sunY = L * 0.28 * dirY;

      sunMesh.position.set(sunX, sunY, 0);

      const maxHistoryTime = streakTurnsRef.current * 2.2;

      if (showStreaksRef.current) {
        sunStreakAura.mesh.visible = true;
        sunStreakCore.mesh.visible = true;
        sunStreakCenter.mesh.visible = true;

        for (let i = 0; i < SAMPLES; i++) {
          const tau = (i / (SAMPLES - 1)) * maxHistoryTime;
          const beamDist = tau * speedAU;
          const hx = sunX - dirX * beamDist;
          const hy = sunY - dirY * beamDist;

          sunStreakAura.points[i].set(hx, hy, 0);
          sunStreakCore.points[i].set(hx, hy, 0);
          sunStreakCenter.points[i].set(hx, hy, 0);
        }
        sunStreakAura.updateGeometry();
        sunStreakCore.updateGeometry();
        sunStreakCenter.updateGeometry();
      } else {
        sunStreakAura.mesh.visible = false;
        sunStreakCore.mesh.visible = false;
        sunStreakCenter.mesh.visible = false;
      }

      orbitGroup.visible = showOrbitsRef.current;
      if (showOrbitsRef.current) {
        orbitGroup.position.set(sunX, sunY, 0);
        orbitGroup.rotation.z = angleRad;
      }

      const earthPos = new Vec3(0, 0, 0);
      targetRingMesh.visible = false;

      CELESTIAL_PLANETS.forEach((planet, idx) => {
        const pMesh = planetMeshes[idx];
        const pLine = planetPolylines[idx];
        const oMesh = orbitMeshes[idx];
        const r3D = planet.radiusAU * 0.045;
        const omega = (2 * Math.PI) / planet.orbitalPeriodYr;

        const isPlanetVisible = visiblePlanetIdsRef.current.has(planet.id);

        const thetaNow = omega * curTime + planet.initialPhase;
        const px = sunX + uX * r3D * Math.cos(thetaNow);
        const py = sunY + uY * r3D * Math.cos(thetaNow);
        const pz = r3D * Math.sin(thetaNow);
        pMesh.position.set(px, py, pz);

        pMesh.visible = isPlanetVisible;
        if (oMesh) {
          oMesh.visible = isPlanetVisible;
        }

        if (planet.id === "earth") {
          earthPos.set(px, py, pz);
        }

        if (isPlanetVisible && planet.id === selectedPlanetIdRef.current) {
          const pulse = 1.0 + 0.15 * Math.sin(curTime * 5.0);
          const ringR = Math.max(0.5, pMesh.scale.x * 1.8 * pulse);
          targetRingMesh.visible = true;
          targetRingMesh.position.set(px, py, pz);
          targetRingMesh.scale.set(ringR, ringR, 1.0);
          targetRingMesh.rotation.z = curTime * 1.5;
        }

        if (showStreaksRef.current && isPlanetVisible) {
          pLine.mesh.visible = true;
          for (let i = 0; i < SAMPLES; i++) {
            const tau = (i / (SAMPLES - 1)) * maxHistoryTime;
            const histT = curTime - tau;
            const beamDist = tau * speedAU;
            const histCenterX = sunX - dirX * beamDist;
            const histCenterY = sunY - dirY * beamDist;

            const histTheta = omega * histT + planet.initialPhase;
            const hx = histCenterX + uX * r3D * Math.cos(histTheta);
            const hy = histCenterY + uY * r3D * Math.cos(histTheta);
            const hz = r3D * Math.sin(histTheta);

            pLine.points[i].set(hx, hy, hz);
          }
          pLine.updateGeometry();
        } else {
          pLine.mesh.visible = false;
        }
      });

      const isEarthVisible = visiblePlanetIdsRef.current.has("earth");
      const moonOmega = (2 * Math.PI) / MOON_CONFIG.orbitalPeriodYr;
      const moonTheta = moonOmega * curTime + 0.4;
      const moonR3D = MOON_CONFIG.radiusAU * 0.045;
      const mx = earthPos.x + uX * moonR3D * Math.cos(moonTheta);
      const my = earthPos.y + uY * moonR3D * Math.cos(moonTheta);
      const mz = earthPos.z + moonR3D * Math.sin(moonTheta);
      moonMesh.position.set(mx, my, mz);
      moonMesh.visible = isEarthVisible;

      if (showStreaksRef.current && isEarthVisible) {
        moonPolyline.mesh.visible = true;
        const moonMaxT = 1.2;
        for (let i = 0; i < SAMPLES; i++) {
          const tau = (i / (SAMPLES - 1)) * moonMaxT;
          const histT = curTime - tau;
          const beamDist = tau * speedAU;
          const histCenterX = sunX - dirX * beamDist;
          const histCenterY = sunY - dirY * beamDist;

          const earthOmega = (2 * Math.PI) / 1.85;
          const eTheta = earthOmega * histT + 4.9;
          const eR3D = 76 * 0.045;
          const ex = histCenterX + uX * eR3D * Math.cos(eTheta);
          const ey = histCenterY + uY * eR3D * Math.cos(eTheta);
          const ez = eR3D * Math.sin(eTheta);

          const mTheta = moonOmega * histT + 0.4;
          const mhx = ex + uX * moonR3D * Math.cos(mTheta);
          const mhy = ey + uY * moonR3D * Math.cos(mTheta);
          const mhz = ez + moonR3D * Math.sin(mTheta);

          moonPolyline.points[i].set(mhx, mhy, mhz);
        }
        moonPolyline.updateGeometry();
      } else {
        moonPolyline.mesh.visible = false;
      }

      const targetX = sunX - 5.5 * dirX;
      const targetY = sunY - 5.5 * dirY + 2.6;
      const targetZ = 0;

      const pitchRad = (cameraPitchRef.current * Math.PI) / 180;
      const yawRad = (cameraYawRef.current * Math.PI) / 180;
      const dist = 38.0 / zoomRef.current;

      camera.position.x =
        targetX + dist * Math.sin(yawRad) * Math.cos(pitchRad);
      camera.position.y = targetY - dist * Math.sin(pitchRad);
      camera.position.z =
        targetZ + dist * Math.cos(yawRad) * Math.cos(pitchRad);
      camera.lookAt(new Vec3(targetX, targetY, targetZ));

      renderer.render({ scene, camera });

      animId = requestAnimationFrame(loop);
    };

    animId = requestAnimationFrame(loop);

    return () => {
      cancelAnimationFrame(animId);
      window.removeEventListener("resize", handleResize);
      resizeObserver.disconnect();
    };
  }, []);

  return (
    <div
      className={cn(
        "relative w-full h-full min-h-[380px] overflow-hidden select-none bg-neutral-950",
        className,
      )}
    >
      <canvas
        ref={canvasRef}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        onPointerCancel={handlePointerUp}
        onWheel={handleWheel}
        className="w-full h-full block cursor-grab active:cursor-grabbing"
        style={{ touchAction: "none" }}
      />
    </div>
  );
}

export default SolarSystemMotion;
IHSOR Discrete Fourier Transform 124-Harmonics Epicycle Synthesis Wordmark
@ 2024-2026 ihsor.dev