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.
- 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;
}
}}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.
- 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);
};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.
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.
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]);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.
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// tick(): if (pointerInteracting.current === null && !reduceMotion) phiRef.current += 0.005;The result
Same tick, same phi, same globe.update() — whether it's drifting on its
own or following your cursor.