Aperçu en direct
Cliquez et faites glisser pour faire tourner le globeGlobe Terrestre 3D Interactif
Visualisation 3D WebGL de haute performance avec shaders atmosphériques et repères géographiques.
Problème et solution
Restituer un rendu planétaire réaliste dans le navigateur sans baisse de framerate sur les appareils modestes.
Fonctionnalités
- Shaders de diffusion atmosphérique sur mesure en WebGL
- Éclairage jour/nuit synchronisé avec l'heure réelle
- Repères géographiques interactifs
- Intégration iframe réactive synchronisée avec le thème clair/sombre
Stack technique
| Technologie | Version | Objectif | Pourquoi ce choix |
|---|---|---|---|
| React | ^19.0.1 | UI runtime for both entries | Component model; canvas work stays outside React and is driven through a ref |
| TypeScript | ^7.0.2 | Types for the whole project | Only static gate in CI (lint = tsc --noEmit) with strict mode on |
| Vite | ^8.3.0 | Build tool and dev server | Fast builds with dual entry via rollupOptions.input |
| Tailwind CSS | ^4.3.3 | Utility styling | No config file; @theme tokens in index.css |
| d3-geo | ^3.1.1 | Projection maths only | geoOrthographic/geoPath/graticule without committing to a renderer |
| topojson-client + world-atlas | ^3.1.0 / ^2.0.2 | Country polygons | Free, offline, no API key; India's entry is replaced by official vectors |
| lucide-react | ^0.546.0 | Icons | Tree-shakeable icon set |
| @vercel/analytics | ^2.0.1 | Pageview analytics | Zero-config on Vercel |
| Vitest | ^5.0.4 | Unit tests | Fast, node environment - everything under test is pure logic |
| pnpm | 10.33.2 (pinned) | Package manager | Deterministic lockfile in CI |
| GitHub Actions | ubuntu-latest, Node 22 | CI | Native to the repo host; lint -> test -> build gates |
| Vercel | — | Static hosting and CDN | Static assets + headers + rewrites with no server |
| OSM Nominatim | public API | Geocoding search | Free and keyless |
Architecture
Two Runtime Entries, One Renderer
- vite.config.ts builds both index.html -> src/main.tsx -> App.tsx and embed.html -> src/embed.tsx
- Both entries render the same EarthGlobe component, so fixes reach both surfaces
- App.tsx (721 lines) adds customizer panels, search, persistence and the embed generator
- embed.tsx (418 lines) is deliberately inert: globe only, no chrome, no storage, command API
Pure Config Layer (the architectural keystone)
- src/config/parseGlobeConfig.ts (405) - location.search -> GlobeConfig, the only URL reader
- src/config/buildEmbedUrl.ts (188) - GlobeConfig -> embed URL / iframe snippet, the only URL writer
- src/config/embedTrust.ts (36) - postMessage trust boundary (source === window.parent)
- src/config/resolveMarker.ts (121) - URL signals -> GlobeMarker with per-surface policy
- No React and no DOM in this layer, which is what makes the round-trip test possible
Renderer & Data
- components/EarthGlobe/EarthGlobe.tsx (~1500) - requestAnimationFrame loop, projection setup, gestures
- d3-geo geoOrthographic/geoPath/geoGraticule for maths only; projection configured once at module scope
- topojson-client feature() decodes world-atlas countries-110m once at import time
- india-official.json (37 features), india-internal-borders.json and india-outer-boundary.json are vendored Survey of India vectors
- Self-contained renderer package with zero knowledge of the main app or the embed
State, Search & Delivery
- hooks/useMapSettings.ts (460) - state, localStorage persistence, storage-event reconciliation
- services/geocoding.ts (370) - Nominatim client, rate budget, offline curated table
- utils/coordinates.ts - hemisphere-correct lat/lng formatting used by all four call sites
- Vercel CDN serves dist/ with cleanUrls, a /embed rewrite and CSP frame-ancestors * on /embed(.*)
- No API gateway, queue, cache server, worker or message broker exists
Structure du projet
Points d'API
Bharat Atlas (static SPA on Vercel) (port 443)
| Méthode | Route | Objectif |
|---|---|---|
| GET | / | Main app SPA - customizer, search, embed generator (rewritten to /index.html) |
| GET | /embed | Chrome-free iframe target; rewritten to /embed.html and served with CSP frame-ancestors * |
| GET | /embed.html | Same embed entry, served directly; configured by URL query params |
OpenStreetMap Nominatim (external) (port 443)
| Méthode | Route | Objectif |
|---|---|---|
| GET | /search | Resolve a place name to lat/lng for the search pin; debounced, abortable, ~1 req/s budget with a 30 s cooldown on 429 |
Décisions de conception clés
One Build, Two Entries, One Renderer
Décision : index.html and embed.html are built from one config and both render the same EarthGlobe component
Pourquoi : The embed must never diverge visually or behaviourally from the app; a second renderer would double the surface area of every fix
Avantages : One renderer, every fix reaches both surfaces
Compromis : The embed downloads saved-settings branches it never runs (~3.5 kB, ~0.3 kB gzipped)
Canvas 2D + d3-geo Instead of WebGL
Décision : Hand-written Canvas 2D painter with d3-geo used only for projection maths
Pourquoi : Zero backend, zero keys, static-only hosting - a hand-painted orthographic globe at 110m resolution does not need a GPU stack
Avantages : Negligible library weight, no shader or context-loss failure modes, full control of the drawing passes
Compromis : Per-frame CPU scales with geometry (177 country + 37 state polygons every frame); no LOD, culling or instancing
Vendored Survey of India GeoJSON Replaces the Atlas Entry
Décision : Filter ISO 356 out of world-atlas and draw India from india-official.json (37 features incl. J&K and Ladakh)
Pourquoi : Natural Earth's India does not match official boundaries, and cartographic accuracy should not require a tile server
Avantages : Authoritative borders, fully offline, no map service at runtime
Compromis : ~+210 KB payload; TopoJSON quantisation was rejected because it changes cartographic fidelity
Byte-Stable Embed URL Generation
Décision : Fixed parameter order in buildEmbedUrl, plus a round-trip test that parses generated URLs back through parseGlobeConfig
Pourquoi : A pasted embed snippet is a public API surface that cannot change
Avantages : A silent behavioural break becomes a red build; the CI workflow flags the round-trip test as load-bearing
Compromis : Genuine awkwardness is preserved to keep the byte format stable (e.g. DEFAULT_APP_ZOOM exists only to make a non-behaviour visible)
Deliberately Lenient postMessage Validation
Décision : Coerce-then-clamp instead of strict typing; id optional, numeric strings accepted, longitude wraps, zoom clamps
Pourquoi : Hosts send string coordinates, out-of-range zoom or a marker without id - a typeof check would reject those and silently stop the globe moving
Avantages : Existing embeds keep working; only genuinely unusable payloads are rejected, with a commandRejected event
Compromis : Hand-written and only partly uniform; 3 of 20 commands still fail silently
postMessage Trust Limited to window.parent
Décision : Require event.source === window.parent, with EMBED_ALLOWED_PARENT_ORIGINS shipping empty and outbound events targeting the referrer origin
Pourquoi : Embedding is a public, unauthenticated capability - the realistic threat is a third party messaging an embed already on screen
Avantages : Blocks pop-up/opener/sibling-frame attacks at zero cost to embedders
Compromis : Any site that iframes the globe has full script control; acceptable because nothing secret is transmitted
Layered localStorage Persistence
Décision : Diff writes field-by-field against a last-known ref, merge them over a fresh disk read, skip unchanged writes, and reconcile on the storage event
Pourquoi : Whole-blob last-write-wins destroyed a sibling tab's unrelated fields and fired spurious events
Avantages : Correct multi-tab behaviour; a slider drag collapses into a single serialisation
Compromis : A ~45-value hook return, three parallel write effects, no schema version for forward migration
Opaque Canvas With a Neutral Backdrop
Décision : background=false substitutes pure white/black instead of clearing to transparent; every frame is fully painted
Pourquoi : Removes clearRect, avoids compositing cost and letterboxing, and makes the theme halo legible
Avantages : Simpler and cheaper frames, predictable look across hosts
Compromis : The globe cannot be composited over a host page background - a real limitation for embeds
Frame-Rate-Independent Physics Constants
Décision : Inertia 0.93^(dt*60), auto-rotation speed*dt*10, wheel zoom 1.08 in / 1/1.08 out
Pourquoi : Flat velocity decay behaves differently at 60 vs 120 Hz and asymmetric zoom steps are not reversible
Avantages : Identical feel on 60 Hz, 120 Hz and ProMotion displays
Compromis : Constants are scattered across three sites rather than shared
Tests
Ce qui est testé
- parseGlobeConfig (67 cases) - theme polarity rules, bound-palette chain, isOff semantics, onlyIndia presence-sensitivity, unclamped speed, zoom master switch, zoomLevel clamping, lat/lng pairing, reduced motion
- buildEmbedUrl (~45 cases) - round trip generate -> parse -> assert survival, exhaustive toggle combinations, byte-stable parameter order, iframe snippet generation
- resolveMarker (25 cases) - app lat/lng precedence, curated id/exact-name/substring matching, saved-selection fallback, embed centroid behaviour
- embedTrust (13 cases) - empty-allowlist default, source !== parent rejection, configured allowlist, referrer-derived origin
- background (40 cases) - colour normalisation with 15 rejection cases, parseShowBackground, resolveBackgroundColor, resolveFrameBackground
- themePresets (31 cases) - exactly 12 presets in a 6/6 split, hex-only colours, polarity partition
- ColorPicker (27 cases) - reference-identity with the shared validator, RGB <-> HSV round trips, clamping, draft/commit pipeline
- GlobeControls (14 cases) - parseCoordinate clamping and parseFloat prefix semantics
- geocoding (13 cases) - query shape, network-error degradation, abort, non-OK, 429 cooldown, dedupe, abort-signal passthrough
- coordinates (13 cases) - hemisphere labelling including zero-as-positive and both-negative pairs
Couverture manquante
- The ~1500-line EarthGlobe render loop, hit-testing and gesture physics have zero tests (no jsdom, no canvas mock)
- No integration or E2E tests - nothing verifies that an embed URL renders or that the ready handshake fires
- No component-render tests for ErrorBoundary, Navbar, CityDetailsCard, LocationSearchBar or CodeExportModal
- No tests for useMapSettings (460 lines of persistence and cross-tab logic)
- No coverage tooling and no FPS instrumentation - the long-frame logger exists because no performance claim was ever measured
Défis clés
- Optimiser les shaders GPU pour maintenir 60 images par seconde stables
- Communication inter-fenêtres sécurisée via postMessage
Apprentissages clés
- Mathématiques 3D et programmation de shaders Three.js / GLSL
- Synchronisation d'état réactive entre widgets intégrés et application hôte