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.
↓Scroll to step through it
One canvas, some markers, a pointer
Globe is a single WebGL surface that
cobe repaints every frame from one
number: phi. Idle or dragged, phi is the only thing that changes.
The whole component is createGlobe(canvas, config) handed a merged config
(your markers, colors, and glow over the defaults), plus pointer
handlers that never touch the globe directly. They only record how far the
pointer has moved.
One tick, two sources for phi
Every frame, the requestAnimationFrame loop decides where phi comes
from. When nobody holds the pointer, it drifts by a fixed 0.005. During a
drag, the drift stops and pointerMovement.current / 200 takes over. It is
the same phi variable, read from a different place, fed into the same
globe.update() call.
No branch swaps animation systems. phi is one accumulator and the
pointer's offset is added on top of it, so letting go resumes the drift
from wherever the drag left it.
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 renders, because globe.update() mutates
the WebGL buffer directly. Refs let the tick run at full frame rate without
touching the component tree.
const phiRef = React.useRef(0);
const pointerMovement = React.useRef(0);Lifecycle: a real teardown
Mounting starts the tick and reveals the canvas on the next frame (so it
never flashes an unpainted state). On unmount, 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.
The result
Same tick, same phi, same globe.update(), whether it's drifting on its
own or following your cursor.
- Canvas
- cobe WebGL surface: createGlobe(canvas, config)
- Markers
- config.markers: location + size, glowColor tint
- Drag
- pointerMovement offsets phi while held
const merged: COBEOptions = {...DEFAULT_CONFIG,...config,width: widthRef.current * 2,height: widthRef.current * 2,};const globe = createGlobe(canvas, merged);onPointerDown={(e) => {pointerInteracting.current = e.clientX - pointerMovement.current;}}onPointerMove={(e) => {if (pointerInteracting.current !== null) { pointerMovement.current = e.clientX - pointerInteracting.current;}}}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.
- Canvas
- cobe WebGL surface: createGlobe(canvas, config)
- Markers
- config.markers: location + size, glowColor tint
- Drag
- pointerMovement offsets phi while held
const merged: COBEOptions = {...DEFAULT_CONFIG,...config,width: widthRef.current * 2,height: widthRef.current * 2,};const globe = createGlobe(canvas, merged);onPointerDown={(e) => {pointerInteracting.current = e.clientX - pointerMovement.current;}}onPointerMove={(e) => {if (pointerInteracting.current !== null) { pointerMovement.current = e.clientX - pointerInteracting.current;}}}One canvas, some markers, a pointer
Globe is a single WebGL surface that
cobe repaints every frame from one
number: phi. Idle or dragged, phi is the only thing that changes.
The whole component is createGlobe(canvas, config) handed a merged config
(your markers, colors, and glow over the defaults), plus pointer
handlers that never touch the globe directly. They only record how far the
pointer has moved.
- 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
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);};One tick, two sources for phi
Every frame, the requestAnimationFrame loop decides where phi comes
from. When nobody holds the pointer, it drifts by a fixed 0.005. During a
drag, the drift stops and pointerMovement.current / 200 takes over. It is
the same phi variable, read from a different place, fed into the same
globe.update() call.
No branch swaps animation systems. phi is one accumulator and the
pointer's offset is added on top of it, so letting go resumes the drift
from wherever the drag left it.
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 renders, because globe.update() mutates
the WebGL buffer directly. Refs let the tick run at full frame rate without
touching the component tree.
const phiRef = React.useRef(0);
const pointerMovement = React.useRef(0);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()
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]);Lifecycle: a real teardown
Mounting starts the tick and reveals the canvas on the next frame (so it
never flashes an unpainted state). On unmount, 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.
The result
Same tick, same phi, same globe.update(), whether it's drifting on its
own or following your cursor.
Accessibility
The globe is decorative, so Globe doesn't force a role or label on
you. Wrap it with your own if the globe conveys information (say, active
regions). prefers-reduced-motion is read once at mount and skips the
phi += 0.005 drift entirely. The globe holds still until you drag it.
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// tick(): if (pointerInteracting.current === null && !reduceMotion) phiRef.current += 0.005;Motion Score
WebGL framecobe auto-rotation render loop