Skip to content
Go back

Globe Atmosphere, Halo, and Comets with Pure Canvas 2D and MapLibre

Published:  at  10:00 AM

The Problem

MapLibre GL JS added native globe projection in v3. One line of code and you get a spinning 3D earth. But the experience feels incomplete, the globe floats in a black void with no atmosphere, no stars, no sense of depth

You could reach for Cesium, Three.js, or custom WebGL shaders. That works, but it adds complexity, bundle size, and maintenance burden. What if the same effect could be achieved with simpler tools?

The Solution

A layered approach using nothing but HTML5 Canvas 2D and CSS. Three transparent canvases stacked over the map, each responsible for one visual layer:

z-0   CSS radial-gradient        Deep space background
z-1   Canvas (starfield)         Static procedural stars
z-2   Canvas (shooting stars)    Animated comets with trails
z-3   Canvas (halo)              Atmospheric glow around the globe
z-4   Map container              MapLibre GL JS globe

No WebGL. No shaders. No extra libraries. Just requestAnimationFrame, CanvasRenderingContext2D, and a few gradients.

The Starfield

Stars are generated procedurally from a seed. The algorithm places roughly one star per 900 square pixels, with random sizes between 0.2 and 1.5 pixels and alpha values between 0.15 and 0.75. Most stars are white, with a few tinted blue or warm yellow for variety.

const generateStars = () => {
  const count = Math.floor((width * height) / 900);
  for (let i = 0; i < count; i++) {
    const size = Math.random() * 1.3 + 0.2;
    const alpha = Math.random() * 0.6 + 0.15;
    // 80% white, 20% tinted
    const hue = Math.random() > 0.8
      ? Math.random() > 0.5 ? 220 : 40
      : 0;
    // ...
  }
};

Larger stars (size > 1.1) get a soft glow, a semi-transparent circle at 3x their radius. This mimics the way bright stars appear in the night sky.

The starfield is rendered to an offscreen canvas for performance. The visible canvas then draws from this sheet with a parallax offset that responds to the map center position. As you rotate the globe, the stars drift naturally in the opposite direction.

const dx = (center.lng - initialLng) / 360;
const dy = (center.lat - initialLat) / 180;
renderStars(dx * canvasWidth, dy * canvasHeight);

Shooting Comets

Comets are the most dynamic layer. They spawn at random positions with random directions, travel in a straight line, and fade out over their lifetime.

Each comet has a position, velocity vector, trail length, and a maximum life between 80 and 120 frames. Only one comet is active at a time, and a new one spawns with roughly 0.5% probability per frame.

interface ShootingStar {
  x: number;
  y: number;
  len: number;     // trail length: 150–350px
  speed: number;   // 6–14 px/frame
  angle: number;   // random direction
  alpha: number;   // fades over life
  life: number;
  maxLife: number; // 80–120 frames
}

The trail is drawn with a linear gradient from transparent white at the tail to bright white at the head. The head has a soft blue glow (rgba(200, 220, 255, ...)) that fades alongside the comet.

const gradient = ctx.createLinearGradient(tailX, tailY, s.x, s.y);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
gradient.addColorStop(1, `rgba(255, 255, 255, ${s.alpha})`);

ctx.strokeStyle = gradient;
ctx.lineWidth = 1.5;
ctx.moveTo(tailX, tailY);
ctx.lineTo(s.x, s.y);
ctx.stroke();

// Head glow
ctx.fillStyle = `rgba(200, 220, 255, ${0.5 * s.alpha})`;
ctx.arc(s.x, s.y, 2, 0, Math.PI * 2);
ctx.fill();

The animation runs at 60fps via requestAnimationFrame. It starts on mount and pauses when the user hides the sky layer.

The Halo (Atmospheric Glow)

The halo creates the illusion of atmosphere around the globe. It’s a radial gradient drawn on a canvas overlay at z-3, positioned using the projected screen coordinates of the globe.

A helper function calculates the globe center and radius on screen:

const getGlobeCenterAndRadius = (map) => {
  const center = map.getCenter();
  const centerPx = map.project(center);
  const edge = map.project([center.lng + 90, 0]);
  const radius = Math.hypot(centerPx.x - edge.x, centerPx.y - edge.y);
  return { x: centerPx.x, y: centerPx.y, radius: Math.max(radius, 1) };
};

The gradient starts at the globe edge with a bright blue-white and extends outward to 2.8x the globe radius, fading through several stops to complete transparency:

const gradient = ctx.createRadialGradient(cx, cy, globeRadius, cx, cy, maxRadius);
gradient.addColorStop(0.0, 'rgba(200, 235, 255, 1.0)');
gradient.addColorStop(0.03, 'rgba(130, 200, 250, 0.6)');
gradient.addColorStop(0.08, 'rgba(70, 150, 230, 0.35)');
gradient.addColorStop(0.18, 'rgba(40, 100, 200, 0.15)');
gradient.addColorStop(0.35, 'rgba(25, 65, 160, 0.06)');
gradient.addColorStop(0.6, 'rgba(15, 40, 110, 0.02)');
gradient.addColorStop(1.0, 'rgba(10, 25, 70, 0.0)');

ctx.globalCompositeOperation = 'screen';

The screen blend mode makes the glow additive: bright areas get brighter while dark areas remain unaffected. The halo redraws on every map move event, keeping it perfectly aligned with the globe.

Deep Space Background

Behind everything sits a single CSS gradient with no canvas needed:

background: radial-gradient(
  ellipse at center,
  #0c1b33 0%,
  #081222 100%
);

A deep navy blue at the center fading to near-black at the edges. It creates the illusion of a dark universe without drawing attention away from the globe.

Globe Controls

A small toolbar at the top-right of the map lets users toggle the Halo and the Sky (stars + comets) independently. Both controls are only visible when the map is in globe view and zoomed out beyond 3.5. The effects are designed for the global perspective and would look out of place zoomed into a city.

The auto-rotate feature completes the experience, slowly spinning the globe at 0.6 degrees per tick. It pauses on any user interaction and must be manually re-enabled via the toggle button.

Why Canvas 2D (Not WebGL)

Canvas 2D is often dismissed as too slow for visual effects. But for 2D overlays on a map, it’s the right tool:

The only real limitation is the lack of 3D depth. The stars are a flat layer, not a skybox. For a map tool, that’s acceptable. The globe is the focus, and the sky is decoration.

Conclusion

You don’t need complex 3D engines to make a globe look good. With three canvas overlays, a CSS gradient, and careful parameter tuning, MapLibre’s native globe projection goes from a bare spinning earth to a polished celestial experience.

The full implementation is available in the Geoglify codebase. If you build something similar, I’d love to see it.



Next Post
Map Sovereignty - Serving Raster Tiles for Full Tool Compatibility