Component library
Things I've built and kept
The parts I keep reaching for, with the knobs left on. Pick one, turn them, and the markup below updates to match. The palette selector repaints the canvas with the real tokens from another project — that's the whole point of the fixed ten-token shape. The States tab forces every state at once via .is-* classes; drop those halves when you copy the CSS and keep the plain pseudo-classes.
Globe
Built for the Split Lip Adventures map. cobe draws the sphere on a canvas; everything interesting is what sits on top of it — real DOM buttons, projected into the renderer's own coordinate space so they track their cities as it turns. Grab it and spin it.
Shipped on splitlipadventures.com/map ↗// cobe centres a location at phi = 3π/2 − lng. Re-deriving that is what
// lets a DOM button sit exactly on its city as the canvas turns.
function project(lat, lng, phi, theta, radius) {
const latR = (lat * Math.PI) / 180;
const a = (lng * Math.PI) / 180 + phi - (3 * Math.PI) / 2;
const x = Math.cos(latR) * Math.sin(a);
const y = Math.sin(latR);
const z = Math.cos(latR) * Math.cos(a);
// tilt by the same theta as the camera, or the markers drift off the sphere
const y2 = y * Math.cos(theta) - z * Math.sin(theta);
const z2 = y * Math.sin(theta) + z * Math.cos(theta);
return { x: x * radius, y: -y2 * radius, front: z2 > 0.12 };
}
// One rAF loop drives cobe AND the overlay. Imperative style writes, so a
// spinning globe costs zero React renders.
const frame = () => {
if (!dragging && performance.now() >= resumeAt) phi += 0.0025;
globe.update({ phi, width: w * 2, height: w * 2 });
locations.forEach((l, i) => {
const p = project(l.lat, l.lng, phi, THETA, r * 0.97);
const el = markerRefs.current[i];
el.style.transform = `translate(${r + p.x}px, ${r + p.y}px) translate(-50%,-50%)`;
el.style.opacity = p.front ? "1" : "0"; // hide the far side
el.style.pointerEvents = p.front ? "auto" : "none";
});
raf = requestAnimationFrame(frame);
};- The palette selector repaints the globe too — cobe takes linear [r,g,b] triplets rather than CSS variables, so the tokens get converted by hand. It's the one component the ten-token shape can't reach for free.
- Marker positions are written straight to style.transform inside the frame loop. Putting them in state would mean sixty React renders a second to move six buttons.
- z > 0.12 rather than z > 0 — a marker exactly on the horizon reads as floating beside the globe rather than on it, so it's hidden slightly early.
- Auto-spin pauses while you hold it and resumes five seconds after you let go, so grabbing it doesn't feel like fighting the animation.
- Samples above ~30k costs frames and buys nothing. The slider goes there anyway so you can see where it stops helping.
- Colour and geometry are baked in at createGlobe, so a knob change rebuilds the instance — fine for a deliberate action, never per frame.