A five-tile bento section for a network, hosting or logistics product. The large tile holds a rotating globe made of about 4,600 dots, with the continents picked out, amber city markers and arcs that fly from one location to the next before a ring lands. The other tiles show a request counter that keeps climbing, a median response time with a bar strip that shifts on every ping, a flat dot map with a feed of the latest pings and an uptime strip. Everything is drawn in code with no map files or images. On phones the tiles stack and the globe fills the first one.
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useInView, useReducedMotion } from "framer-motion";
/** Edge locations that light up. Add or remove rows freely. */
const cities = [
{ name: "Lisbon", lat: 38.7, lon: -9.1 },
{ name: "Frankfurt", lat: 50.1, lon: 8.7 },
{ name: "Reykjavik", lat: 64.1, lon: -21.9 },
{ name: "Lagos", lat: 6.5, lon: 3.4 },
{ name: "Cape Town", lat: -33.9, lon: 18.4 },
{ name: "Dubai", lat: 25.2, lon: 55.3 },
{ name: "Mumbai", lat: 19.1, lon: 72.9 },
{ name: "Singapore", lat: 1.4, lon: 103.8 },
{ name: "Tokyo", lat: 35.7, lon: 139.7 },
{ name: "Sydney", lat: -33.9, lon: 151.2 },
{ name: "Denver", lat: 39.7, lon: -105 },
{ name: "Toronto", lat: 43.7, lon: -79.4 },
{ name: "Bogota", lat: 4.7, lon: -74.1 },
{ name: "Sao Paulo", lat: -23.5, lon: -46.6 },
];
const shares = [
{ region: "Europe and Africa", share: 41 },
{ region: "Americas", share: 34 },
{ region: "Asia Pacific", share: 25 },
];
/** Globe settings: radians per second, viewing tilt in radians, number of dots on the sphere. */
const SPIN = 0.24;
const TILT = 0.42;
const DOTS = 4600;
/** Milliseconds between pings, and how long each ring lives. */
const PING_EVERY = 620;
const PING_LIFE = 1900;
const REQUESTS_AT_LOAD = 48210377;
const REQUESTS_PER_SECOND = 1840;
const DOT_COLOUR = "#5eead4";
const PING_COLOUR = "#fcd34d";
/**
* A rough outline of the continents. One row for every 6 degrees of latitude, starting at 87 north,
* each row a list of [west, east] longitudes that are land.
*/
const LAND: [number, number][][] = [
[],
[[-90, -65], [-60, -20], [95, 105]],
[[-120, -80], [-58, -20], [55, 65], [88, 112], [140, 150]],
[[-165, -141], [-135, -95], [-82, -65], [-52, -24], [15, 30], [32, 180]],
[[-165, -141], [-140, -94], [-78, -64], [-50, -41], [-23, -14], [6, 30], [30, 178]],
[[-161, -155], [-134, -93], [-78, -61], [-7, -2], [8, 18], [21, 140], [156, 163]],
[[-128, -56], [-10, 1], [3, 142], [156, 158]],
[[-124, -62], [-1, 47], [53, 136], [141, 145]],
[[-123, -75], [-9, 0], [9, 17], [20, 48], [53, 122], [125, 129], [138, 141]],
[[-118, -79], [-9, 11], [35, 121], [130, 136]],
[[-113, -98], [-82, -80], [-13, 34], [36, 50], [51, 120]],
[[-105, -97], [-90, -87], [-84, -75], [-17, 37], [39, 58], [70, 87], [92, 110]],
[[-97, -84], [-17, 40], [43, 52], [74, 80], [96, 109], [120, 123]],
[[-84, -61], [-13, 48], [76, 79], [98, 101], [104, 107], [123, 126]],
[[-78, -51], [9, 46], [97, 104], [110, 118]],
[[-80, -40], [10, 41], [101, 106], [110, 117], [120, 123], [130, 141]],
[[-78, -35], [13, 40], [106, 114], [141, 148]],
[[-76, -39], [12, 40], [46, 50], [125, 136], [142, 145]],
[[-70, -41], [14, 35], [44, 48], [115, 149]],
[[-70, -48], [15, 33], [114, 153]],
[[-72, -53], [18, 28], [116, 124], [135, 151]],
[[-73, -62], [141, 147], [174, 177]],
[[-74, -66], [168, 171]],
[[-74, -69]],
[],
[[-62, -57]],
[[-75, -60], [0, 160]],
[[-180, 180]],
[[-180, 180]],
[[-180, 180]],
];
/** The flat map shows rows 1 to 23 of LAND (84 north to 54 south). */
const MAP_ROWS = 23;
const MAP_COLS = 72;
const isLand = (lat: number, lon: number): boolean => {
const row = LAND[Math.min(LAND.length - 1, Math.max(0, Math.floor((90 - lat) / 6)))];
for (const [west, east] of row) if (lon >= west && lon <= east) return true;
return false;
};
const toVector = (lat: number, lon: number) => {
const la = (lat * Math.PI) / 180;
const lo = (lon * Math.PI) / 180;
return { x: Math.cos(la) * Math.cos(lo), y: Math.sin(la), z: Math.cos(la) * Math.sin(lo) };
};
/** Small repeatable random number source, so the server and the browser agree on the first frame. */
const makeRandom = (seed: number) => () => {
seed = (seed * 1664525 + 1013904223) % 4294967296;
return seed / 4294967296;
};
const format = (n: number) => Math.floor(n).toLocaleString("en-US");
type Ping = { id: number; city: number; from: number; ms: number; born: number };
const firstBars = Array.from({ length: 28 }, (_, i) => Math.round(36 + 14 * Math.sin(i * 0.9) + 9 * Math.sin(i * 2.3 + 1)));
const firstFeed: Ping[] = [
{ id: -1, city: 1, from: 0, ms: 31, born: 0 },
{ id: -2, city: 8, from: 7, ms: 44, born: 0 },
{ id: -3, city: 11, from: 10, ms: 27, born: 0 },
{ id: -4, city: 13, from: 12, ms: 52, born: 0 },
];
const tile = "relative overflow-hidden rounded-3xl bg-white/[0.04] ring-1 ring-inset ring-white/10";
export default function BentoGridWithDottedGlobeAndLivePings() {
const reduce = useReducedMotion();
const still = Boolean(reduce);
const rootRef = useRef<HTMLElement>(null);
const inView = useInView(rootRef, { amount: 0.15 });
const globeRef = useRef<HTMLCanvasElement>(null);
const mapRef = useRef<HTMLCanvasElement>(null);
const requestsRef = useRef<HTMLSpanElement>(null);
const pings = useRef<Ping[]>([]);
const angle = useRef(-1.75);
const [feed, setFeed] = useState<Ping[]>(firstFeed);
const [bars, setBars] = useState<number[]>(firstBars);
// New pings arrive on a timer. The canvases read them from a ref; the feed and the bars read them from state.
useEffect(() => {
if (still || !inView) return;
const random = makeRandom(7);
let id = 0;
let last = 1;
const timer = window.setInterval(() => {
let city = Math.floor(random() * cities.length);
if (city === last) city = (city + 1) % cities.length;
const ping: Ping = { id: ++id, city, from: last, ms: Math.round(18 + random() * 46), born: performance.now() };
last = city;
pings.current = [...pings.current.filter((p) => ping.born - p.born < PING_LIFE), ping];
setFeed((f) => [ping, ...f].slice(0, 4));
setBars((b) => [...b.slice(1), ping.ms]);
}, PING_EVERY);
return () => window.clearInterval(timer);
}, [still, inView]);
// One animation loop paints both canvases and the request counter.
useEffect(() => {
const globe = globeRef.current;
const map = mapRef.current;
const g = globe?.getContext("2d");
const m = map?.getContext("2d");
if (!globe || !map || !g || !m) return;
// Evenly spread dots on a sphere, land first so the colour changes once.
const golden = Math.PI * (3 - Math.sqrt(5));
const dots = Array.from({ length: DOTS }, (_, i) => {
const y = 1 - (2 * (i + 0.5)) / DOTS;
const r = Math.sqrt(1 - y * y);
const x = Math.cos(i * golden) * r;
const z = Math.sin(i * golden) * r;
return { x, y, z, land: isLand((Math.asin(y) * 180) / Math.PI, (Math.atan2(z, x) * 180) / Math.PI) };
}).sort((a, b) => Number(b.land) - Number(a.land));
const cells: [number, number][] = [];
for (let row = 0; row < MAP_ROWS; row++) for (let col = 0; col < MAP_COLS; col++) if (isLand(81 - row * 6, -177.5 + col * 5)) cells.push([col, row]);
const vectors = cities.map((c) => toVector(c.lat, c.lon));
let dpr = 1;
let size = 0;
let mapW = 0;
let mapH = 0;
let raf = 0;
let previous = performance.now();
const started = previous;
let speed = SPIN;
let dragging = false;
let lastX = 0;
const ct = Math.cos(TILT);
const st = Math.sin(TILT);
const draw = (now: number) => {
const a = angle.current;
const ca = Math.cos(a);
const sa = Math.sin(a);
const cx = size / 2;
const cy = size / 2;
const R = size * 0.4;
const unit = size / 520;
const project = (v: { x: number; y: number; z: number }, lift = 1) => {
const x1 = v.x * ca + v.z * sa;
const z1 = -v.x * sa + v.z * ca;
return { x: cx - x1 * R * lift, y: cy - (v.y * ct - z1 * st) * R * lift, z: v.y * st + z1 * ct };
};
g.setTransform(dpr, 0, 0, dpr, 0, 0);
g.clearRect(0, 0, size, size);
g.globalAlpha = 1;
const halo = g.createRadialGradient(cx, cy, R * 0.92, cx, cy, R * 1.24);
halo.addColorStop(0, "rgba(45,212,191,0.26)");
halo.addColorStop(1, "rgba(45,212,191,0)");
g.fillStyle = halo;
g.beginPath();
g.arc(cx, cy, R * 1.24, 0, Math.PI * 2);
g.fill();
const body = g.createRadialGradient(cx - R * 0.4, cy - R * 0.5, R * 0.05, cx, cy, R);
body.addColorStop(0, "rgba(17,94,89,0.75)");
body.addColorStop(0.6, "rgba(6,40,40,0.9)");
body.addColorStop(1, "rgba(3,14,16,0.96)");
g.fillStyle = body;
g.beginPath();
g.arc(cx, cy, R, 0, Math.PI * 2);
g.fill();
g.strokeStyle = "rgba(94,234,212,0.4)";
g.lineWidth = 1;
g.stroke();
g.fillStyle = DOT_COLOUR;
for (const d of dots) {
const p = project(d);
if (p.z <= 0) continue;
if (d.land) {
g.globalAlpha = 0.18 + 0.82 * p.z;
g.beginPath();
g.arc(p.x, p.y, (0.9 + 1.0 * p.z) * unit, 0, Math.PI * 2);
g.fill();
} else {
g.globalAlpha = 0.05 + 0.1 * p.z;
g.fillRect(p.x, p.y, unit, unit);
}
}
// Edge cities, the arcs travelling between them and the rings where a ping lands.
g.fillStyle = PING_COLOUR;
g.strokeStyle = PING_COLOUR;
for (const v of vectors) {
const p = project(v);
if (p.z <= 0.05) continue;
g.globalAlpha = 0.35 + 0.5 * p.z;
g.beginPath();
g.arc(p.x, p.y, 2.6 * unit, 0, Math.PI * 2);
g.fill();
}
for (const ping of pings.current) {
const t = (now - ping.born) / PING_LIFE;
if (t < 0 || t > 1) continue;
const from = vectors[ping.from];
const to = vectors[ping.city];
const omega = Math.acos(Math.min(1, Math.max(-1, from.x * to.x + from.y * to.y + from.z * to.z)));
const head = Math.min(1, t / 0.55);
if (omega > 0.01 && t < 0.8) {
g.lineWidth = 2.2 * unit;
g.lineCap = "round";
const tail = Math.max(0, head - 0.45);
let prev: { x: number; y: number; z: number } | null = null;
for (let i = 0; i <= 22; i++) {
const s = tail + ((head - tail) * i) / 22;
const k1 = Math.sin((1 - s) * omega) / Math.sin(omega);
const k2 = Math.sin(s * omega) / Math.sin(omega);
const p = project({ x: from.x * k1 + to.x * k2, y: from.y * k1 + to.y * k2, z: from.z * k1 + to.z * k2 }, 1 + 0.24 * Math.sin(Math.PI * s));
if (prev && p.z > 0 && prev.z > 0) {
g.globalAlpha = (i / 22) * 0.9 * (t > 0.55 ? 1 - (t - 0.55) / 0.25 : 1);
g.beginPath();
g.moveTo(prev.x, prev.y);
g.lineTo(p.x, p.y);
g.stroke();
}
prev = p;
}
}
const p = project(to);
const ring = (t - 0.45) / 0.55;
if (p.z > 0.05 && ring > 0) {
g.lineWidth = 1.5 * unit;
g.globalAlpha = (1 - ring) * Math.min(1, p.z * 2.5);
g.beginPath();
g.arc(p.x, p.y, (3 + 20 * ring) * unit, 0, Math.PI * 2);
g.stroke();
g.beginPath();
g.arc(p.x, p.y, 3.2 * unit, 0, Math.PI * 2);
g.fill();
}
}
// The flat map: the same land data as a grid of dots.
m.setTransform(dpr, 0, 0, dpr, 0, 0);
m.clearRect(0, 0, mapW, mapH);
const cell = mapW / MAP_COLS;
m.fillStyle = DOT_COLOUR;
m.globalAlpha = 0.3;
for (const [col, row] of cells) {
m.beginPath();
m.arc((col + 0.5) * cell, (row + 0.5) * cell, cell * 0.3, 0, Math.PI * 2);
m.fill();
}
m.fillStyle = PING_COLOUR;
m.strokeStyle = PING_COLOUR;
const place = (c: { lat: number; lon: number }) => ({ x: ((c.lon + 180) / 360) * mapW, y: ((84 - c.lat) / (MAP_ROWS * 6)) * mapH });
m.globalAlpha = 0.85;
for (const c of cities) {
const p = place(c);
m.beginPath();
m.arc(p.x, p.y, Math.max(1.6, cell * 0.42), 0, Math.PI * 2);
m.fill();
}
m.lineWidth = 1.5;
for (const ping of pings.current) {
const t = (now - ping.born) / PING_LIFE;
if (t < 0 || t > 1) continue;
const p = place(cities[ping.city]);
m.globalAlpha = 1 - t;
m.beginPath();
m.arc(p.x, p.y, 3 + t * cell * 5, 0, Math.PI * 2);
m.stroke();
m.globalAlpha = (1 - t) * 0.25;
m.fill();
}
if (requestsRef.current) requestsRef.current.textContent = format(REQUESTS_AT_LOAD + ((now - started) / 1000) * REQUESTS_PER_SECOND);
};
const frame = (now: number) => {
raf = 0;
const dt = Math.min(0.05, (now - previous) / 1000);
previous = now;
if (!dragging) {
speed += (SPIN - speed) * 0.03;
angle.current -= speed * dt;
}
draw(now);
schedule();
};
const schedule = () => {
if (!raf && inView && !still && !document.hidden) {
previous = performance.now();
raf = requestAnimationFrame(frame);
}
};
const fit = () => {
dpr = Math.min(2, window.devicePixelRatio || 1);
size = globe.clientWidth;
globe.width = globe.height = Math.round(size * dpr);
mapW = map.clientWidth;
mapH = map.clientHeight;
map.width = Math.round(mapW * dpr);
map.height = Math.round(mapH * dpr);
draw(performance.now());
};
const down = (e: PointerEvent) => {
dragging = true;
lastX = e.clientX;
};
const move = (e: PointerEvent) => {
if (!dragging) return;
const dx = e.clientX - lastX;
lastX = e.clientX;
angle.current -= dx * 0.007;
speed = Math.max(-3, Math.min(3, dx * 0.4));
if (still) draw(performance.now());
};
const up = () => {
dragging = false;
};
const observer = new ResizeObserver(fit);
observer.observe(globe);
observer.observe(map);
globe.addEventListener("pointerdown", down);
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
window.addEventListener("pointercancel", up);
document.addEventListener("visibilitychange", schedule);
fit();
schedule();
return () => {
cancelAnimationFrame(raf);
observer.disconnect();
globe.removeEventListener("pointerdown", down);
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
window.removeEventListener("pointercancel", up);
document.removeEventListener("visibilitychange", schedule);
};
}, [inView, still]);
const sorted = [...bars].sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
const rise = (i: number) => ({
initial: still ? false : ({ opacity: 0, y: 26, scale: 0.97 } as const),
animate: { opacity: 1, y: 0, scale: 1 },
transition: { type: "spring" as const, stiffness: 150, damping: 20, delay: 0.08 + i * 0.09 },
});
return (
<section ref={rootRef} className="relative overflow-hidden bg-neutral-950 px-5 py-16 text-white sm:px-6 sm:py-20">
<style>{`
@keyframes bento-grid-with-dotted-globe-and-live-pings-pop {
from { opacity: 0; transform: scaleY(0.2); }
to { opacity: 1; transform: none; }
}
@keyframes bento-grid-with-dotted-globe-and-live-pings-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.25; }
}
.bento-grid-with-dotted-globe-and-live-pings-pop { animation: bento-grid-with-dotted-globe-and-live-pings-pop 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; }
.bento-grid-with-dotted-globe-and-live-pings-blink { animation: bento-grid-with-dotted-globe-and-live-pings-blink 1.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.bento-grid-with-dotted-globe-and-live-pings-pop,
.bento-grid-with-dotted-globe-and-live-pings-blink { animation: none; }
}
`}</style>
<span aria-hidden="true" className="pointer-events-none absolute left-1/4 top-0 h-80 w-[640px] -translate-y-1/2 rounded-full bg-teal-500/15 blur-3xl" />
<div className="relative mx-auto max-w-6xl">
<motion.div className="max-w-2xl" initial={still ? false : { opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}>
<p className="flex items-center gap-2 text-sm font-semibold uppercase tracking-widest text-teal-300">
<span className="bento-grid-with-dotted-globe-and-live-pings-blink h-2 w-2 rounded-full bg-amber-300" />
Tessel Edge network
</p>
<h2 className="mt-4 text-4xl font-semibold tracking-tight sm:text-5xl">Your site, answered from the city next door.</h2>
<p className="mt-4 text-base leading-7 text-neutral-400">{cities.length} edge locations hold a copy of every page. A request never crosses an ocean unless it has to.</p>
</motion.div>
<div className="mt-10 grid gap-4 lg:grid-cols-3">
<motion.div {...rise(0)} className={`${tile} min-h-[440px] lg:col-span-2 lg:row-span-2 lg:min-h-[470px]`}>
<div className="relative z-10 max-w-xs p-6 sm:p-8">
<h3 className="text-xl font-semibold tracking-tight">Live traffic</h3>
<p className="mt-2 text-sm leading-6 text-neutral-400">Each arc is a visitor being handed to the closest location. Drag the globe to look around.</p>
<ul className="mt-5 space-y-2 text-sm text-neutral-300">
<li className="flex items-center gap-2"><span className="h-2 w-2 rounded-full bg-amber-300" />Edge location</li>
<li className="flex items-center gap-2"><span className="h-2 w-2 rounded-full bg-teal-300" />Land, drawn as dots</li>
</ul>
</div>
<canvas
ref={globeRef}
aria-hidden="true"
className="absolute left-1/2 top-[34%] aspect-square w-[122%] max-w-[600px] -translate-x-1/2 cursor-grab touch-pan-y active:cursor-grabbing lg:left-auto lg:right-[-4%] lg:top-[4%] lg:w-[68%] lg:max-w-none lg:translate-x-0"
/>
<p className="sr-only">A rotating globe of dots with pings arriving at {cities.map((c) => c.name).join(", ")}.</p>
</motion.div>
<motion.div {...rise(1)} className={`${tile} p-6`}>
<p className="text-sm text-neutral-400">Requests answered today</p>
<p className="mt-2 text-4xl font-semibold tabular-nums tracking-tight">
<span ref={requestsRef} aria-hidden="true">{format(REQUESTS_AT_LOAD)}</span>
<span className="sr-only">About 48 million and counting</span>
</p>
<dl className="mt-5 space-y-3">
{shares.map((s, i) => (
<div key={s.region}>
<div className="flex justify-between text-xs text-neutral-300">
<dt>{s.region}</dt>
<dd className="tabular-nums">{s.share}%</dd>
</div>
<div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-white/10">
<motion.div className="h-full origin-left rounded-full bg-teal-300" style={{ width: `${s.share * 2}%` }} initial={still ? false : { scaleX: 0 }} animate={{ scaleX: 1 }} transition={{ duration: 0.9, ease: [0.22, 1, 0.36, 1], delay: 0.5 + i * 0.12 }} />
</div>
</div>
))}
</dl>
</motion.div>
<motion.div {...rise(2)} className={`${tile} flex flex-col p-6`}>
<div className="flex items-baseline justify-between">
<p className="text-sm text-neutral-400">Median response</p>
<p className="text-xs text-neutral-400">last 28 pings</p>
</div>
<p className="mt-2 text-4xl font-semibold tabular-nums tracking-tight">
{median}
<span className="ml-1.5 text-lg font-medium text-neutral-400">ms</span>
</p>
<div aria-hidden="true" className="mt-auto flex h-20 items-end gap-[3px] pt-5">
{bars.map((b, i) => (
<span key={i} className={`flex-1 origin-bottom rounded-sm transition-transform duration-500 ease-out motion-reduce:transition-none ${i === bars.length - 1 ? "bg-amber-300" : "bg-teal-300/70"}`} style={{ height: "100%", transform: `scaleY(${Math.max(0.12, b / 70).toFixed(3)})` }} />
))}
</div>
</motion.div>
<motion.div {...rise(3)} className={`${tile} p-6 sm:p-8 lg:col-span-2`}>
<div className="grid items-center gap-6 sm:grid-cols-[minmax(0,1fr)_200px]">
<div>
<h3 className="text-xl font-semibold tracking-tight">Ping map</h3>
<p className="mt-2 text-sm leading-6 text-neutral-400">Health checks run from every location, every few seconds.</p>
<canvas ref={mapRef} aria-hidden="true" className="mt-5 block aspect-[72/23] w-full" />
</div>
<ul className="space-y-2" aria-label="Latest pings">
<AnimatePresence initial={false} mode="popLayout">
{feed.map((p, i) => (
<motion.li
key={p.id}
layout={still ? false : "position"}
initial={still ? false : { opacity: 0, y: -14, scale: 0.94 }}
animate={{ opacity: 1 - i * 0.2, y: 0, scale: 1 }}
exit={{ opacity: 0, transition: { duration: 0.15 } }}
transition={{ type: "spring", stiffness: 380, damping: 30 }}
className="flex h-10 items-center justify-between rounded-xl bg-white/5 px-3 text-sm"
>
<span className="flex items-center gap-2">
<span className={`h-1.5 w-1.5 rounded-full ${i === 0 ? "bg-amber-300" : "bg-teal-300"}`} />
{cities[p.city].name}
</span>
<span className="tabular-nums text-neutral-300">{p.ms} ms</span>
</motion.li>
))}
</AnimatePresence>
</ul>
</div>
</motion.div>
<motion.div {...rise(4)} className={`${tile} flex flex-col p-6`}>
<p className="text-sm text-neutral-400">Uptime, last 45 days</p>
<p className="mt-2 text-4xl font-semibold tabular-nums tracking-tight">99.98%</p>
<div aria-hidden="true" className="mt-5 flex h-9 items-stretch gap-[3px]">
{Array.from({ length: 45 }, (_, i) => (
<span key={i} className={`bento-grid-with-dotted-globe-and-live-pings-pop flex-1 origin-bottom rounded-sm ${i === 31 ? "bg-amber-300" : "bg-teal-300/80"}`} style={{ animationDelay: `${0.6 + i * 0.018}s` }} />
))}
</div>
<p className="mt-4 text-xs leading-5 text-neutral-400">One slow day in Singapore on the 14th. Traffic moved to Tokyo in 9 seconds.</p>
</motion.div>
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/bento-grid-with-dotted-globe-and-live-pings.json
Points are spread evenly over a sphere with a golden-angle spiral. Each frame, every point is rotated around the vertical axis, tilted, and drawn at its x and y position if it faces the viewer. Dots near the edge are drawn smaller and fainter, which is what makes the flat circle read as a ball.
From the `LAND` table in the file: one row for every six degrees of latitude, each holding the stretches of longitude that are land. A dot is bright if its latitude and longitude fall inside one of those stretches. It is a rough outline, which is all a dot globe needs, and it adds almost nothing to the file size.
Each ping remembers the city it came from. The arc is a short run of points along the shortest path between the two cities on the sphere, pushed outward in the middle so it lifts off the surface. The run moves from start to end over about half the ping's life, then the ring expands at the destination.
No. Pings are generated on a timer from a small repeatable random source. To show real traffic, push your own events into the `pings` ref and the `feed` state in the same shape.
One animation loop paints both canvases in well under two milliseconds on a laptop. The pixel ratio is capped at 2, and the loop and the ping timer both stop when the section is off screen or the tab is hidden.
Related: Animated Bento Grid with Live Mini Demos, Bento Grid with Six Feature Tiles, Count Up Stats Row with Rolling Digits.
More feature sections or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.