npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-cartoon-planet

v1.1.2

Published

Animated cartoon globe for React — zoom from orbit to ground level

Readme

react-cartoon-planet

Animated cartoon globe for React — zoom from orbit to ground level, switch visual styles, drop markers, and drive the camera from your own UI.

react-cartoon-planet demo — click to play

Install

npm install react-cartoon-planet three
# or
pnpm add react-cartoon-planet three

Peer dependencies (you install these):

  • react (18 or 19)
  • react-dom
  • three (>=0.160) — WebGL scene, globe mesh, markers, custom render modes

three is a peer so your app and the globe share one Three.js instance — that's what makes controller.getThree() objects, instanceof checks, and the re-exported THREE all line up. Construct your own meshes from the package's re-exported THREE (see Direct Three.js access).

React Native / Expo? The optional react-cartoon-planet/native entry point renders through expo-gl instead of a DOM canvas and adds react-native + expo-gl as (optional) peers — see React Native / Expo.

Dependencies (installed automatically with the package):

  • earcut — polygon triangulation for continent geometry

Import the bundled stylesheet once in your app:

import "react-cartoon-planet/style.css";

Quick start

Mount the globe, wire a ref for programmatic control, and compose the built-in HUD / sidebar controls as children:

import { useRef, useState } from "react";
import "react-cartoon-planet/style.css";
import {
  AltitudeDisplay,
  BUILTIN_RENDER_MODES,
  CartoonPlanet,
  EARTH_MAP,
  FpsDisplay,
  HintDisplay,
  MarkerLabelsDisplay,
  MarkerManagerControl,
  MOON_MAP,
  OutlineStyleControl,
  PlanetMapControl,
  PlacingToastDisplay,
  QuickJumpControl,
  RenderModeControl,
  ScaleBarDisplay,
  StartLevelControl,
  START_VIEWS,
  SURFACE_RENDER_MODE,
} from "react-cartoon-planet";
import type { CartoonPlanetController, GlobeState } from "react-cartoon-planet";

export function GlobeDemo() {
  const controllerRef = useRef<CartoonPlanetController | null>(null);
  const [planetState, setPlanetState] = useState<GlobeState | null>(null);

  return (
    <div style={{ width: "100%", height: "100vh" }}>
      <header>
        <button
          onClick={() => controllerRef.current?.flyTo(-74.006, 40.7128, 1_500)}
        >
          Fly to NYC
        </button>
        <button
          onClick={() =>
            controllerRef.current?.flyTo(
              START_VIEWS.globe.lng,
              START_VIEWS.globe.lat,
              START_VIEWS.globe.alt_m,
              { duration: 900 },
            )
          }
        >
          Reset view
        </button>
      </header>

      <CartoonPlanet
        ref={controllerRef}
        maps={[EARTH_MAP, MOON_MAP]}
        renderModes={BUILTIN_RENDER_MODES}
        initialState={{
          map: EARTH_MAP,
          renderMode: SURFACE_RENDER_MODE,
          startView: "globe",
        }}
        onStateChange={setPlanetState}
      >
        <FpsDisplay />
        <AltitudeDisplay />
        <ScaleBarDisplay />
        <MarkerLabelsDisplay />
        <PlacingToastDisplay />
        <HintDisplay />
        <StartLevelControl />
        <PlanetMapControl />
        <RenderModeControl />
        <OutlineStyleControl />
        <QuickJumpControl />
        <MarkerManagerControl />
      </CartoonPlanet>

      <footer>
        lng {planetState?.hud.focusLng?.toFixed(2)}° · alt{" "}
        {planetState?.hud.scaleLabel}
      </footer>
    </div>
  );
}

Give the canvas room to breathe — the globe fills its container (width / height: 100% on a sized parent works well) and tracks container resizes automatically via ResizeObserver, so collapsing surrounding panels never leaves the canvas stretched.

Starting camera (initialCamera)

startView snaps the camera to a named preset ("globe" / "ground") on first mount. To open on an exact spot instead — e.g. the user's current location, or a region rather than the default globe view — pass initialCamera in initialState. It overrides startView for the initial framing only; later flyTo / setStartView calls work as usual.

<CartoonPlanet
  initialState={{
    map: EARTH_MAP,
    renderMode: SURFACE_RENDER_MODE,
    initialCamera: { lng: 15, lat: 50, alt_m: 6_000_000 }, // open centred on Europe
  }}
/>

React Native / Expo

The globe also runs on React Native through a dedicated entry point that renders into an expo-gl context instead of a DOM canvas. The scene engine, markers, render modes, initialCamera, and the full controller API are identical — only the host view and input wiring differ.

npx expo install expo-gl
# plus a gesture source for pan/zoom/tap, e.g. react-native-gesture-handler

Native adds react-native and expo-gl as optional peers alongside three.

import { PixelRatio, View } from "react-native";
import * as THREE from "three";
import {
  CartoonPlanetNative,
  EARTH_MAP,
  type CartoonPlanetController,
  type GlobeInteractionTarget,
} from "react-cartoon-planet/native";

// You build the renderer so the Expo GL context + canvas shim stay under your control.
const createGlRenderer = (gl, width, height) =>
  new THREE.WebGLRenderer({ context: gl, antialias: true /* + a canvas shim */ });

<CartoonPlanetNative
  createGlRenderer={createGlRenderer}
  pixelRatio={Math.min(PixelRatio.get(), 2)}
  maps={[EARTH_MAP]}
  initialState={{
    map: EARTH_MAP,
    initialCamera: { lng: 15, lat: 50, alt_m: 6_000_000 },
  }}
  onReady={(c: CartoonPlanetController) => {
    /* same controller API: flyTo, setMarkers, … */
  }}
  onInteractionReady={(target: GlobeInteractionTarget) => {
    // RN has no DOM pointer events — forward your gesture handler's events:
    // target.dispatch({ type: "pointerdown", clientX, clientY, pointerId: 1 })
    // types: "pointerdown" | "pointermove" | "pointerup" | "wheel" | "click"
  }}
/>;

How native differs from the web <CartoonPlanet>:

  • createGlRenderer(gl, width, height) (required) — you construct the THREE.WebGLRenderer against the Expo GL context, keeping the canvas shim and pixel ratio yours.
  • onInteractionReady(target) — pan / zoom / tap are dispatched manually via target.dispatch(...); wire it to your gesture library.
  • No composable UI children / ui prop — HUD and sidebar panels are web-only. Lay native controls out as ordinary RN views over the globe.
  • dayNight, clouds, bloom default to false on native (they default to true on web), so the base globe stays cheap on mobile GPUs.
  • The default render mode is SURFACE_NATIVE_RENDER_MODE — a lighter solid mode tuned for mobile — which is also exported for explicit use.

Render modes

Four built-in styles ship out of the box. Switch them in the sidebar (RenderModeControl) or via controller.setRenderMode("Cyber").

| Solid | Dots | | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | Solid render mode | Dots render mode |

| Hybrid | Cyber | | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | Hybrid render mode | Cyber render mode |

Cyber mode in motion:

Cyber render mode in motion — click to play

Presets: SURFACE_RENDER_MODE (Solid), DOTS_RENDER_MODE, HYBRID_RENDER_MODE, CYBERPUNK_RENDER_MODE, or the full BUILTIN_RENDER_MODES array.

Day/night cycle, clouds & city lights

Render modes can opt in to a day/night cycle via getDayNight() { return true; } (the built-in Solid mode does). When active, a soft terminator shadow drifts slowly around the globe (full cycle ≈ 3.5 min), and the active map can enable two extra layers:

  • clouds: true — a procedural, slowly drifting cloud sphere (on for EARTH_MAP)
  • nightLights: true — warm city lights scattered over land, visible only on the night side (on for EARTH_MAP)

All three layers fade out as you descend toward ground level, so they never obstruct the close-up view — markers stay fully readable day and night.

Like bloom, both are runtime-toggleable props (no scene rebuild — flip them freely from your UI):

<CartoonPlanet dayNight={showDayNight} clouds={showClouds} bloom={showBloom} />

Both default to true; the mode's getDayNight() and the map's clouds / nightLights flags still decide what each toggle can show.

Plug in your own layer generators

clouds and nightLights are composable the same way render modes are: pass a GlobeLayerBuilder function instead of true and the globe uses your layer in place of the built-in one. The builder receives { map, continents, pixelRatio, sunDir } and returns a Three.js Object3D (unit sphere — surface ≈ radius 1.0, built-in clouds sit at 1.018). Attach userData.update = ({ alt, time, sunDir }) => … for per-frame animation; the object is disposed automatically when the map or mode changes.

import { THREE, buildCloudLayer } from "react-cartoon-planet";
import type { GlobeLayerBuilder, PlanetMapDefinition } from "react-cartoon-planet";

const myClouds: GlobeLayerBuilder = ({ sunDir }) => {
  const mesh = new THREE.Mesh(
    new THREE.SphereGeometry(1.02, 64, 48),
    new THREE.MeshBasicMaterial({ map: myCloudTexture(), transparent: true, depthWrite: false }),
  );
  mesh.renderOrder = 3; // clouds slot: above the surface, below the terminator
  mesh.userData.update = ({ alt }) => {
    mesh.rotation.y += 0.0002;
    mesh.material.opacity = THREE.MathUtils.clamp((alt - 60_000) / 700_000, 0, 0.9);
  };
  return mesh;
};

const MY_MAP: PlanetMapDefinition = { ...EARTH_MAP, name: "Mine", clouds: myClouds };

The built-in builders (buildCloudLayer, buildCityLights, buildTerminator) are exported too, so you can wrap or extend them. The demo's Vapor map plugs in a custom neon streak-cloud generator this way. (The atmosphere halo and starfield aren't pluggable yet — drop replacements into the scene via onSceneReady if you need to restyle those.)

Bloom

Pass the bloom prop for an UnrealBloomPass post-processing glow — neon-heavy modes like Cyber pop hard:

<CartoonPlanet bloom />                                  // defaults
<CartoonPlanet bloom={{ strength: 0.8, radius: 0.6, threshold: 0.1 }} />

It's toggleable at runtime (just flip the prop), and rendering falls back to the plain renderer when off. Color output is identical in both paths (the composer ends with an OutputPass).

Coastlines

Continent borders render as screen-space vector lines (not a baked texture stroke), so they stay a crisp, constant thickness from orbit all the way to ground level instead of ballooning as you zoom in. Toggle bold vs. thin via OutlineStyleControl, controller.setFatOutlines(boolean), or initialState.fatOutlines:

  • off (default) — thin 1px lines: crisp and cheap to render.
  • on — bold screen-space "fat" lines (~2.5px), DPR-independent (won't look hairline on HiDPI); a little heavier since each segment is a shaded quad.

Markers

Markers are screen-constant pins anchored to the surface — readable from orbit and at ~5 m ground level alike. Supply them via initialState.markers or the controller's marker methods. Two sample sets ship: DEFAULT_MARKERS (cities) and WARSAW_LANDMARK_MARKERS (three imaginary Warsaw landmarks ~2 m apart, for the clustering demo).

  • Clustering — at altitude, markers that would overlap on screen merge into a count badge (e.g. "3 landmarks"). Clicking a cluster flies down to an altitude that frames and separates its members.
  • Ground level — keep zooming (down to ~5 m) to see individuals at their true coordinates.
  • PlacementstartPlacing() (or MarkerManagerControl) → click the globe to drop a marker exactly where the cursor lands. The built-in editor previews it live on the map (size, color, label, shape); Save finalizes, Cancel discards.
  • LinkssetLinksEnabled(true) draws arcs between markers; toggle in the UI with LinksDisplay.
  • Hover — markers grow smoothly under the pointer and the cursor switches to a pointer; subscribe with the onMarkerHover prop (fires with the marker, then null on leave).
  • Click hookonMarkerClick={(marker) => …} fires before the default fly-to (clusters included — check marker.isCluster). Return false to suppress the default and handle the click entirely yourself (e.g. open your own popover).

Planet maps

  • EarthEARTH_MAP (bundled GeoJSON, blue ocean / green land)
  • MoonMOON_MAP (bundled maria data)

Pass your own PlanetMapDefinition with a GeoJSON URL, or pre-parsed continents to skip the fetch. Use flattenGeoJsonToContinents if you already have GeoJSON in memory.

Map data sources

Bundled land/maria geometry comes from third-party datasets. Full attribution, download URLs, and processing notes:

Geospatial data sources (mvp/data/geospatial/source.md)

| File | Origin | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | earth-land.geojson | Natural Earth ne_110m_landsource GeoJSON (Public Domain) | | moon-maria.geojson | LROC Global Mare boundaries — converted from the official shapefile ZIP with shpjs |

Bundled GeoJSON is simplified for globe-scale rendering with scripts/slim-geojson.mjs (coordinates rounded to 4 decimals, sub-pixel ring points dropped) — the moon dataset shrinks from 18 MB to ~6 MB with no visible difference at the globe's 4096px texture resolution.

Controller API

Attach a ref (useRef<CartoonPlanetController>()) or use onReady to get the controller.

| Method | Description | | ------------------------------------------- | ---------------------------------------- | | flyTo(lng, lat, altitudeMeters, options?) | Animated camera move (default 1800 ms) | | flyToAltitude(altitudeMeters, options?) | Zoom in/out at current heading | | flyToMarker(id) | Fly to a marker by id | | rotateBy(lngDelta, latDelta, options?) | Nudge heading (default 600 ms) | | rotateTo(lng, lat, options?) | Absolute heading at current altitude | | startAutoRotate / stopAutoRotate | Continuous spin | | getView() | Current { lng, lat, altitudeMeters } | | setRenderMode(mode) | Switch visual style (name or definition) | | setPlanetMap(map) | Switch planet map | | setStartView("globe" \| "ground") | Orbit vs near-ground preset | | setMarkers / addMarker / removeMarker | Marker CRUD | | startPlacing / cancelPlacing | Click-to-place marker mode | | setLinksEnabled(boolean) | Toggle marker link lines | | setFatOutlines(boolean) | Bold vs thin (1px) vector coastlines | | getThree() | Live Three.js objects (see below) | | getState() / subscribe(listener) | Reactive globe state |

onStateChange on <CartoonPlanet> is the React-friendly alternative to subscribe.

Composable UI

Sidebar panels and HUD widgets are optional React children — include only what you need:

| Component | Role | | ------------------------ | ------------------------------ | | FpsDisplay | Frame rate | | AltitudeDisplay | Current altitude | | ScaleBarDisplay | Map scale bar | | MarkerLabelsDisplay | Screen-space marker labels | | PlacingToastDisplay | “Click globe to place” hint | | HintDisplay | Interaction hints | | StartLevelControl | Globe / ground start level | | PlanetMapControl | Earth / Moon (or custom maps) | | RenderModeControl | Style picker | | OutlineStyleControl | Bold (fat) coastlines toggle | | QuickJumpControl | Preset locations | | MarkerManagerControl | Add / remove markers | | LinksDisplay | Marker link-lines toggle | | CartoonPlanetDefaultUi | All of the above in one bundle |

If you pass children, the legacy ui={{ … }} prop is ignored. Without children, every built-in panel stays off unless you opt in via ui.

Custom render modes

Implement GlobeRenderModeDefinition with a renderFunction that receives continent geometry and returns a three.js Group. Continent outlines are already triangulated (via earcut) in config.continents — your mode typically textures or stylizes that mesh.

Register modes in the renderModes prop or call setRenderMode with a new definition at runtime.

import * as THREE from "three";
import type { GlobeRenderModeDefinition } from "react-cartoon-planet";

const MY_MODE: GlobeRenderModeDefinition = {
  name: "MyStyle",
  renderFunction(config) {
    const group = new THREE.Group();
    // build from config.continents, config.map, config.outlinePx …
    return group;
  },
};

Direct Three.js access

Need to drop your own meshes into the scene, raycast, add post-processing, or run a custom animation loop? The globe exposes its live Three.js objects via the onSceneReady prop (fires on mount) or controller.getThree() (returns null until mounted).

import { CartoonPlanet, THREE } from "react-cartoon-planet";
import type { CartoonPlanetThree } from "react-cartoon-planet";

<CartoonPlanet
  onSceneReady={(three: CartoonPlanetThree) => {
    // Build from the package's THREE so it's the SAME instance the globe runs on.
    const ring = new THREE.Mesh(
      new THREE.TorusGeometry(1.25, 0.012, 12, 96),
      new THREE.MeshBasicMaterial({ color: "#ff2eea" }),
    );
    ring.rotation.x = Math.PI / 2;
    three.scene.add(ring); // add to `three.planet` instead to move with the globe
  }}
/>;

three contains:

| Field | Type | Notes | | ------------------ | ------------------------- | --------------------------------------------- | | scene | THREE.Scene | Root scene | | camera | THREE.PerspectiveCamera | Globe camera | | renderer | THREE.WebGLRenderer | The WebGL renderer | | controls | GlobeControls | Orbit/zoom controls (radius, theta, …) | | planet | THREE.Group | Surface + markers; child of scene | | surfaceGroup | THREE.Group | Textured surface + coastline lines | | markerRoot | THREE.Group | Marker meshes | | getMarkerGroup() | THREE.Group \| null | Current marker group (rebuilds on clustering) |

The render loop is persistent — anything you add draws every frame. The globe lives on a unit sphere (surface ≈ radius 1.0; one scene unit ≈ Earth's radius). Always construct objects from the re-exported THREE, not your own import * as THREE from "three", so you share the globe's single instance.

Props

| Prop | Type | Notes | | --------------------- | ---------------------------------------- | ---------------------------------------------- | | maps | PlanetMapDefinition[] | Defaults to Earth + Moon | | renderModes | GlobeRenderModeDefinition[] | Defaults to all four built-ins | | initialState | CartoonPlanetInitialState | Map, mode, start view / initialCamera, markers | | bloom | boolean \| CartoonPlanetBloomOptions | Post-processing glow; runtime-toggleable | | dayNight | boolean | Terminator + city lights; runtime-toggleable | | clouds | boolean | Cloud layer; runtime-toggleable | | onStateChange | (state: GlobeState) => void | HUD, fps, active map/mode | | onReady | (controller) => void | Fires when engine is ready | | onSceneReady | (three) => void | Live Three.js objects on mount | | onMarkerClick | (marker) => boolean \| void | Before default fly-to; false suppresses it | | onMarkerHover | (marker \| null) => void | Pointer enters / leaves a marker | | className / style | — | Root container | | children | React nodes | Composable UI (see above) |

Demo app included

The demo-app folder is a Vite + React playground that mirrors the quick start above — toolbar buttons call flyTo, rotateBy, and a scripted intro across all render modes, and the "Warsaw landmarks" button drills into the ground-level marker cluster. It also exercises the newer APIs: a Layers group toggles the dayNight, clouds, and bloom props at runtime, clicking any marker opens an info card via onMarkerClick (without suppressing the default fly-to), the Vapor map plugs a custom neon streak-cloud GlobeLayerBuilder into an Earth-shaped synthwave planet, and onSceneReady exposes the live scene as window.__three for devtools tinkering.

cd demo-app && pnpm install && pnpm dev

Links

License

MIT © radomski.dev