JK
>LOADING // LOADING
INITIALIZING VIEWA 100% client-side React SPA that paints an orthographic 3D globe of India on an HTML canvas using official Survey of India GeoJSON (37 states/UTs, including J&K and Ladakh) as its basemap. One Vite build produces two surfaces — the main configurator app (index.html) and a chrome-free embed (embed.html) driven entirely by query parameters and a 20-command postMessage API. No backend, no database, no API keys: geodata is bundled at build time and hosted as static assets.
An interactive country globe normally means a paid mapping API key, a tile server and a backend. This project sidesteps all three: it vendors Survey of India cartography instead of calling a commercial basemap, uses d3-geo purely for projection maths while painting with Canvas 2D instead of WebGL, and ships a single static bundle. The result is a globe that any third-party site can drop in as an iframe and script.
| Technology | Version | Purpose | Why Chosen |
|---|---|---|---|
| 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 |
| Method | Route | Purpose |
|---|---|---|
| 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 |
| Method | Route | Purpose |
|---|---|---|
| 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 |
Decision: index.html and embed.html are built from one config and both render the same EarthGlobe component
Why: The embed must never diverge visually or behaviourally from the app; a second renderer would double the surface area of every fix
Benefits: One renderer, every fix reaches both surfaces
Trade-offs: The embed downloads saved-settings branches it never runs (~3.5 kB, ~0.3 kB gzipped)
Decision: Hand-written Canvas 2D painter with d3-geo used only for projection maths
Why: Zero backend, zero keys, static-only hosting - a hand-painted orthographic globe at 110m resolution does not need a GPU stack
Benefits: Negligible library weight, no shader or context-loss failure modes, full control of the drawing passes
Trade-offs: Per-frame CPU scales with geometry (177 country + 37 state polygons every frame); no LOD, culling or instancing
Decision: Filter ISO 356 out of world-atlas and draw India from india-official.json (37 features incl. J&K and Ladakh)
Why: Natural Earth's India does not match official boundaries, and cartographic accuracy should not require a tile server
Benefits: Authoritative borders, fully offline, no map service at runtime
Trade-offs: ~+210 KB payload; TopoJSON quantisation was rejected because it changes cartographic fidelity
Decision: Fixed parameter order in buildEmbedUrl, plus a round-trip test that parses generated URLs back through parseGlobeConfig
Why: A pasted embed snippet is a public API surface that cannot change
Benefits: A silent behavioural break becomes a red build; the CI workflow flags the round-trip test as load-bearing
Trade-offs: Genuine awkwardness is preserved to keep the byte format stable (e.g. DEFAULT_APP_ZOOM exists only to make a non-behaviour visible)
Decision: Coerce-then-clamp instead of strict typing; id optional, numeric strings accepted, longitude wraps, zoom clamps
Why: 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
Benefits: Existing embeds keep working; only genuinely unusable payloads are rejected, with a commandRejected event
Trade-offs: Hand-written and only partly uniform; 3 of 20 commands still fail silently
Decision: Require event.source === window.parent, with EMBED_ALLOWED_PARENT_ORIGINS shipping empty and outbound events targeting the referrer origin
Why: Embedding is a public, unauthenticated capability - the realistic threat is a third party messaging an embed already on screen
Benefits: Blocks pop-up/opener/sibling-frame attacks at zero cost to embedders
Trade-offs: Any site that iframes the globe has full script control; acceptable because nothing secret is transmitted
Decision: 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
Why: Whole-blob last-write-wins destroyed a sibling tab's unrelated fields and fired spurious events
Benefits: Correct multi-tab behaviour; a slider drag collapses into a single serialisation
Trade-offs: A ~45-value hook return, three parallel write effects, no schema version for forward migration
Decision: background=false substitutes pure white/black instead of clearing to transparent; every frame is fully painted
Why: Removes clearRect, avoids compositing cost and letterboxing, and makes the theme halo legible
Benefits: Simpler and cheaper frames, predictable look across hosts
Trade-offs: The globe cannot be composited over a host page background - a real limitation for embeds
Decision: Inertia 0.93^(dt*60), auto-rotation speed*dt*10, wheel zoom 1.08 in / 1/1.08 out
Why: Flat velocity decay behaves differently at 60 vs 120 Hz and asymmetric zoom steps are not reversible
Benefits: Identical feel on 60 Hz, 120 Hz and ProMotion displays
Trade-offs: Constants are scattered across three sites rather than shared