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:
- Zero dependencies. No Three.js, no shader compilation, no WebGL context management.
- Simple to debug. You can inspect each layer, log pixel values, and tweak parameters live.
- Easy to maintain. A junior developer can read and modify this code. Shaders require specialized knowledge.
- Small footprint. The entire implementation (starfield, comets, and halo) fits in under 200 lines.
- Performance. Canvas 2D is hardware-accelerated on all modern browsers. Drawing a few hundred stars and one comet per frame is trivial.
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.