GodUIGodUI
91

Anatomy of Globe

How one rAF tick drives an idle drift or a dragged rotation off the same angle, and why the canvas gets destroyed instead of just hidden.

Globe isn't a spinning image — it's a single WebGL surface that cobe repaints every frame from one number: phi. Idle or dragged, that's the only thing changing.

One canvas, some markers, a pointer

The whole component is createGlobe(canvas, config) handed a merged config — your markers, colors, and glow over sane defaults — plus pointer handlers that never touch the globe directly. They only record how far the pointer has moved.

Anatomyone canvas · markers · pointer drag
Canvas
cobe WebGL surface — createGlobe(canvas, config)
Markers
config.markers — location + size, glowColor tint
Drag
pointerMovement offsets phi while held
tsx
 
const merged: COBEOptions = {
  ...DEFAULT_CONFIG,
  ...config,
  width: widthRef.current * 2,
  height: widthRef.current * 2,
};
const globe = createGlobe(canvas, merged);
tsx
 
onPointerDown={(e) => {
  pointerInteracting.current = e.clientX - pointerMovement.current;
}}
onPointerMove={(e) => {
  if (pointerInteracting.current !== null) {
    pointerMovement.current = e.clientX - pointerInteracting.current;
  }
}}

The motion: one tick, two sources for phi

Every frame, the requestAnimationFrame loop decides where phi comes from. Nobody's holding the pointer: it drifts by a fixed 0.005. Somebody is: the drift stops and pointerMovement.current / 200 takes over instead — the same phi variable, read from a different place, fed into the exact same globe.update() call.

The motionidle drift vs. pointer-driven phi
Idle
phi += 0.005 every frame, nobody's touching it
Drag
pointerMovement / 200 drives phi instead
Phi
the one angle globe.update() sets, every frame
tsx
 
const tick = () => {
  if (pointerInteracting.current === null && !reduceMotion) {
    phiRef.current += 0.005;
  }
  globe.update({
    phi: phiRef.current + pointerMovement.current / 200,
    width: widthRef.current * 2,
    height: widthRef.current * 2,
  });
  raf = requestAnimationFrame(tick);
};

There's no mode switch, no branch that swaps animation systems — phi is one accumulator, and the pointer's offset is just added on top of it, so letting go resumes the drift from wherever the drag left it.

Why the loop lives outside React

phi, pointer state, and canvas width are all refs, not useState. If they were state, every one of those ~60 updates a second would trigger a React re-render for a value React never actually renders — globe.update() mutates the WebGL buffer directly. Refs let the tick run at full frame rate without ever touching the component tree.

tsx
 
const phiRef = React.useRef(0);
const pointerMovement = React.useRef(0);

Lifecycle: a real teardown, not a pause

Mounting starts the tick and reveals the canvas on the next frame (so it never flashes an unpainted state). Unmounting doesn't just stop rendering — the effect's cleanup cancels the frame and calls globe.destroy(), which frees the WebGL context cobe allocated. Leaving it running after the canvas is gone would leak a context per mount/unmount cycle.

LifecyclerAF tick, then a real teardown

mounted — rAF running

unmounted — context freed

Mount
createGlobe(canvas, config) + rAF loop starts
Idle spin
tick → phi += 0.005 → globe.update(), forever
Destroy
cleanup: cancelAnimationFrame + globe.destroy()
tsx
 
useEffect(() => {
  const globe = createGlobe(canvas, merged);
  let raf = requestAnimationFrame(tick);
  const reveal = requestAnimationFrame(() => {
    canvas.style.opacity = "1";
  });

  return () => {
    cancelAnimationFrame(raf);
    cancelAnimationFrame(reveal);
    globe.destroy();
  };
}, [config]);

Accessibility

The canvas is aria-hidden in spirit — it's a decorative globe, not content, so Globe doesn't force a role or label on you; wrap it with your own if the globe conveys information (say, active regions) rather than atmosphere. prefers-reduced-motion is read once at mount and skips the phi += 0.005 drift entirely — the globe holds still until you drag it.

tsx
 
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// tick(): if (pointerInteracting.current === null && !reduceMotion) phiRef.current += 0.005;

The result

Resultthe real component — drag it

Same tick, same phi, same globe.update() — whether it's drifting on its own or following your cursor.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub