Skip to main content

Overview

Gitlantis provides two navigation aids to help you orient yourself in the 3D ocean world: an overhead minimap and a directional compass.

Minimap

The minimap renders a top-down orthographic view of the world, centered on your boat.

Minimap Modes

In standard mode, the minimap appears as a small overlay in the bottom-right corner:
  • Size: 120x120 pixels
  • Position: 10px from bottom-right corner
  • Zoom level: 3.5x
  • Toggle: Press F to switch to fullscreen
const minimapSize = { width: 120, height: 120 };
const minimapPosition = {
  x: size.width - minimapSize.width - 10,
  y: size.height - minimapSize.height - 10,
};
Source: src/browser/hooks/useMinimap/index.ts:18-30

Camera Implementation

The minimap uses a virtual orthographic camera that tracks the boat from above:
const targetPos = boatRef.current.position;
virtualCam.current.position.set(targetPos.x, targetPos.y + 50, targetPos.z);
virtualCam.current.lookAt(targetPos);
virtualCam.current.rotateZ(Math.PI / 2);
The camera is positioned 50 units above the boat and rotated 90° to align north upward. Source: src/browser/hooks/useMinimap/index.ts:36-39

Rendering Pipeline

The minimap uses WebGL scissor testing to render a separate viewport:
gl.autoClear = false;
gl.clearDepth();
gl.setScissorTest(true);

gl.setViewport(
  minimapPosition.x,
  minimapPosition.y,
  minimapSize.width,
  minimapSize.height
);
gl.setScissor(
  minimapPosition.x,
  minimapPosition.y,
  minimapSize.width,
  minimapSize.height
);
gl.render(scene, virtualCam.current);

gl.setScissorTest(false);
gl.setViewport(0, 0, size.width, size.height);
This technique allows rendering the same scene from two different cameras simultaneously. Source: src/browser/hooks/useMinimap/index.ts:42-61

Interactive Minimap

You can click on the minimap to open files and folders:
function onClick(event: MouseEvent) {
  const { clientX, clientY } = event;
  const inside =
    clientX >= minimapPosition.x &&
    clientX <= minimapPosition.x + minimapSize.width &&
    clientY >= minimapPosition.y &&
    clientY <= minimapPosition.y + minimapSize.height;

  if (!inside || !virtualCam.current) return;

  pointer.x = ((clientX - minimapPosition.x) / minimapSize.width) * 2 - 1;
  pointer.y = -((clientY - minimapPosition.y) / minimapSize.height) * 2 + 1;

  raycaster.setFromCamera(pointer, virtualCam.current);
  const intersects = raycaster.intersectObjects(scene.children, true);

  if (intersects.length) {
    const object = intersects[0].object;
    object.userData?.openOnClick();
  }
}
The system:
  1. Checks if the click is within minimap bounds
  2. Converts screen coordinates to normalized device coordinates
  3. Uses raycasting to detect which object was clicked
  4. Calls the object’s openOnClick handler
Source: src/browser/hooks/useMinimap/index.ts:84-104

Visibility Settings

The minimap respects user settings and can be hidden:
if (!virtualCam.current || !boatRef?.current || settings.minimap === "Hide")
  return;
Source: src/browser/hooks/useMinimap/index.ts:33

Compass

The compass displays your current heading using cardinal directions and degrees.

Heading Calculation

The compass extracts the boat’s yaw rotation and converts it to degrees:
const yawRadians = new Euler().setFromQuaternion(
  boatRef.current.quaternion,
  "YXZ"
).y;
let yawDegrees = -(yawRadians * (180 / Math.PI));
yawDegrees = ((yawDegrees % 360) + 360) % 360;
The rotation is normalized to 0-360° and updated every 16ms (60fps). Source: src/browser/hooks/useBoat/compass/index.ts:12-18

Cardinal Directions

The compass supports 8 cardinal directions:
const cardinals = [
  { label: "N", degree: 0 },
  { label: "NE", degree: 45 },
  { label: "E", degree: 90 },
  { label: "SE", degree: 135 },
  { label: "S", degree: 180 },
  { label: "SW", degree: 225 },
  { label: "W", degree: 270 },
  { label: "NW", degree: 315 },
];
Source: src/browser/hooks/useBoat/compass/index.ts:24-33

Scrollable Compass Strip

The compass creates a scrollable strip of cardinal points:
const createScrollableCompass = () => {
  const strip: Array<{ label: string; degree: number; key: string }> = [];
  for (let circle = -2; circle <= 2; circle++) {
    cardinals.forEach((cardinal) => {
      const adjustedDegree = cardinal.degree + circle * 360;
      strip.push({
        ...cardinal,
        degree: adjustedDegree,
        key: `${cardinal.label}-${circle}`,
      });
    });
  }
  return strip.sort((a, b) => a.degree - b.degree);
};
This creates 5 full rotations (-2 to +2 circles) for smooth infinite scrolling. Source: src/browser/hooks/useBoat/compass/index.ts:56-69

Display Format

The compass displays both numeric degrees and the nearest cardinal direction:
const degreesLabel = `${Math.round(currentRotation) ?? 0}° ${
  getClosestCardinal(Math.round(currentRotation)) ?? ""
}`;
Example: 347° NW or 90° ESource: src/browser/hooks/useBoat/compass/index.ts:75-77

Performance Considerations

Both navigation aids are designed for minimal performance impact:
  • Minimap: Uses scissor testing to avoid separate render passes
  • Compass: Updates at 60fps but only recalculates when rotation changes
  • Fade-in: Both components respect the splash screen state and fade in after 1700ms
const [shouldFadeIn, setShouldFadeIn] = useState(
  settings.minimap === "Show" && !showSplashScreen
);

useEffect(() => {
  if (settings.minimap === "Show" && !showSplashScreen) {
    setTimeout(() => setShouldFadeIn(true), 1700);
  }
}, [settings.minimap, showSplashScreen]);
Source: src/browser/components/world/minimap/index.tsx:12-20

Build docs developers (and LLMs) love