Globe dots

도트 지구본

Dots placed along a latitude/longitude grid on a sphere and slowly rotated so it reads as a spinning globe.

Also known as: Dotted spherePoint-cloud globe
···
html
<div class="globe-wrap"><canvas id="gl"></canvas></div>
css
.globe-wrap{position:relative;width:min(260px,82%);aspect-ratio:1;display:grid;place-items:center}
.globe-wrap::before{content:"";position:absolute;inset:-8%;border-radius:50%;filter:blur(10px);
  background:radial-gradient(circle,color-mix(in srgb,var(--accent) 32%,transparent),transparent 70%)}
canvas{position:relative;width:100%;height:100%}
js
const canvas = document.getElementById('gl');
const ctx = canvas.getContext('2d');
const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#5b5bf7';
let W, H, R;
function resize() {
  const r = canvas.getBoundingClientRect();
  W = canvas.width = r.width * devicePixelRatio;
  H = canvas.height = r.height * devicePixelRatio;
  R = Math.min(W, H) * 0.42;
}
resize();
addEventListener('resize', resize);

const dots = [];
const LAT_STEPS = 14, LON_STEPS = 22;
for (let i = 1; i < LAT_STEPS; i++) {
  const lat = (Math.PI * i) / LAT_STEPS - Math.PI / 2;
  const ringR = Math.cos(lat);
  const count = Math.max(4, Math.round(LON_STEPS * ringR));
  for (let j = 0; j < count; j++) {
    const lon = (Math.PI * 2 * j) / count;
    dots.push([ringR * Math.cos(lon), Math.sin(lat), ringR * Math.sin(lon)]);
  }
}

let angle = 0;
function frame() {
  angle += 0.006;
  ctx.clearRect(0, 0, W, H);
  const cx = W / 2, cy = H / 2;
  const cosA = Math.cos(angle), sinA = Math.sin(angle);
  const projected = dots
    .map(([x, y, z]) => [x * cosA - z * sinA, y, x * sinA + z * cosA])
    .sort((a, b) => a[2] - b[2]);
  for (const [x, y, z] of projected) {
    const depth = (z + 1) / 2;
    const px = cx + x * R, py = cy - y * R;
    const size = (1.1 + depth * 2.1) * devicePixelRatio;
    ctx.globalAlpha = 0.2 + depth * 0.8;
    ctx.fillStyle = accent;
    ctx.beginPath();
    ctx.arc(px, py, size, 0, Math.PI * 2);
    ctx.fill();
  }
  requestAnimationFrame(frame);
}
frame();

Each dot's 3D coordinates (x, y, z) come from converting latitude and longitude with the standard spherical-to-Cartesian formula. Multiplying every dot by a y-axis rotation matrix each frame makes the whole sphere appear to spin.

Depth comes from two tricks. Dots closer to the camera (a larger z) are drawn bigger and more opaque, while distant ones shrink and fade — near things large, far things faint. And sorting the dots by z each frame, drawing the back ones first and the front ones last, makes nearer dots naturally occlude the ones behind them (the painter's algorithm).

You could build this with an actual three.js Points geometry, but at a few hundred dots, doing the maths directly on a 2D canvas — as this demo does — is much lighter. One detail that matters: the number of dots per latitude ring should scale with the cosine of that latitude, or dots visibly bunch up near the poles.

When to use

Use for a global-reach section or a "connections worldwide" stat backdrop. More dots cost more CPU — for a card-sized area, keep the count under a few hundred.