diff --git a/app/globals.css b/app/globals.css
index 1a9e97d..e8c94e2 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -942,37 +942,4 @@ body {
margin-left: 0.25em;
}
-/* Hero visual placement: anchor the image to bottom-right and allow it to extend past the section */
-.hero-visual {
- position: absolute;
- right: 0;
- bottom: 0;
- z-index: 10;
- width: min(85vw, 950px);
- max-width: none;
- transform: translateX(0) translateY(0);
-}
-@media (min-width: 768px) {
- .hero-visual {
- right: -2.5rem; /* push slightly into the right gutter */
- bottom: -3rem; /* push slightly below the fold */
- width: min(85vw, 950px);
- }
-}
-
-@media (min-width: 1024px) {
- .hero-visual {
- right: -4rem;
- bottom: -5rem;
- width: min(90vw, 1200px);
- }
-}
-
-/* ensure the image inside the container preserves its object-fit and does not add extra padding */
-.hero-visual img,
-.hero-visual .next-image {
- object-fit: contain;
- object-position: right bottom;
- padding: 0;
-}
diff --git a/app/page.tsx b/app/page.tsx
index 812e85f..d6adcb3 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -6,6 +6,7 @@ import Image from 'next/image';
import Link from 'next/link';
import type { CSSProperties } from 'react';
import Navbar from "@/components/Navbar";
+import HeroGlobe from "@/components/HeroGlobe";
import { useTranslations } from "@/lib/i18n";
function Typewriter({ text, className = "", speed = 45 }: { text: string; className?: string; speed?: number }) {
@@ -269,24 +270,11 @@ export default function Home() {
-
- TECTONIC
+ TECTONIC
{tFooter("desc")}
diff --git a/components/HeroGlobe.tsx b/components/HeroGlobe.tsx
new file mode 100644
index 0000000..2ebbcaf
--- /dev/null
+++ b/components/HeroGlobe.tsx
@@ -0,0 +1,232 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { decodeDots, HUBS } from "@/lib/world-land";
+
+/* --- TYPES --- */
+
+interface HeroGlobeProps {
+ /** Applied to the wrapping element. The canvas fills it and stays square. */
+ className?: string;
+ /** Degrees of rotation per second. */
+ speed?: number;
+ /** Longitude at the centre of the globe when the animation starts. */
+ startLongitude?: number;
+}
+
+/* --- GEOMETRY --- */
+
+const DEG = Math.PI / 180;
+const VIEW_LATITUDE = 22; // tilt: how far above the equator the camera sits
+const DEPTH_BANDS = 4; // dots are batched into this many depth buckets per frame
+
+/** [lon, lat] in degrees to a unit vector with z through the north pole. */
+function toVector(lon: number, lat: number): [number, number, number] {
+ const p = lat * DEG;
+ const l = lon * DEG;
+ const c = Math.cos(p);
+ return [c * Math.cos(l), c * Math.sin(l), Math.sin(p)];
+}
+
+/** Flatten [lon, lat] pairs to [x0, y0, z0, x1, y1, z1, ...]. */
+function toBuffer(points: Array<[number, number]>): Float32Array {
+ const out = new Float32Array(points.length * 3);
+ for (let i = 0; i < points.length; i++) {
+ const [x, y, z] = toVector(points[i][0], points[i][1]);
+ out[i * 3] = x;
+ out[i * 3 + 1] = y;
+ out[i * 3 + 2] = z;
+ }
+ return out;
+}
+
+/* --- COMPONENT --- */
+
+export default function HeroGlobe({
+ className = "",
+ speed = 2,
+ startLongitude = -32,
+}: HeroGlobeProps) {
+ const wrapRef = useRef(null);
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const wrap = wrapRef.current;
+ const canvas = canvasRef.current;
+ if (!wrap || !canvas) return;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ const land = toBuffer(decodeDots());
+ const hubs = HUBS.map(([lon, lat]) => toVector(lon, lat));
+
+ const S0 = Math.sin(VIEW_LATITUDE * DEG);
+ const C0 = Math.cos(VIEW_LATITUDE * DEG);
+
+ let size = 0;
+ let frame = 0;
+ let running = false;
+ let last = 0;
+ let spin = -startLongitude * DEG;
+
+ function render() {
+ if (!size) return;
+ const mid = size / 2;
+ const r = mid - 2;
+ const sa = Math.sin(spin);
+ const ca = Math.cos(spin);
+
+ ctx!.clearRect(0, 0, size, size);
+
+ /* Spin about the polar axis, tilt to the viewing latitude, drop the depth
+ component. Dots are collected into depth bands so each band is one fill
+ instead of one fill per dot. */
+ const bands: Array> = [];
+ for (let b = 0; b < DEPTH_BANDS; b++) bands.push([]);
+
+ for (let i = 0; i < land.length; i += 3) {
+ const vx = land[i];
+ const vy = land[i + 1];
+ const vz = land[i + 2];
+ const rx = vx * ca - vy * sa;
+ const depth = rx * C0 + vz * S0;
+ if (depth <= 0.02) continue;
+ const band = Math.min(DEPTH_BANDS - 1, Math.floor(depth * DEPTH_BANDS));
+ bands[band].push(
+ mid + r * (vx * sa + vy * ca),
+ mid - r * (vz * C0 - rx * S0),
+ );
+ }
+
+ for (let b = 0; b < DEPTH_BANDS; b++) {
+ const pts = bands[b];
+ if (!pts.length) continue;
+ const t = (b + 0.5) / DEPTH_BANDS;
+ const dot = r * (0.0055 + 0.0055 * t);
+ ctx!.beginPath();
+ for (let i = 0; i < pts.length; i += 2) {
+ ctx!.moveTo(pts[i] + dot, pts[i + 1]);
+ ctx!.arc(pts[i], pts[i + 1], dot, 0, Math.PI * 2);
+ }
+ ctx!.fillStyle = `rgba(138,79,34,${(0.2 + 0.55 * t).toFixed(3)})`;
+ ctx!.fill();
+ }
+
+ // hub network: project the visible hubs, then link the near pairs
+ const shown: Array<[number, number]> = [];
+ for (const [vx, vy, vz] of hubs) {
+ const rx = vx * ca - vy * sa;
+ if (rx * C0 + vz * S0 <= 0.08) continue;
+ shown.push([mid + r * (vx * sa + vy * ca), mid - r * (vz * C0 - rx * S0)]);
+ }
+
+ ctx!.lineCap = "round";
+ ctx!.beginPath();
+ for (let i = 0; i < shown.length; i++) {
+ for (let j = i + 1; j < shown.length; j++) {
+ if (Math.hypot(shown[i][0] - shown[j][0], shown[i][1] - shown[j][1]) < r) {
+ ctx!.moveTo(shown[i][0], shown[i][1]);
+ ctx!.lineTo(shown[j][0], shown[j][1]);
+ }
+ }
+ }
+ ctx!.lineWidth = Math.max(r / 260, 0.5);
+ ctx!.strokeStyle = "rgba(194,65,12,0.3)";
+ ctx!.stroke();
+
+ ctx!.beginPath();
+ for (const [x, y] of shown) {
+ ctx!.moveTo(x + r * 0.012, y);
+ ctx!.arc(x, y, r * 0.012, 0, Math.PI * 2);
+ }
+ ctx!.fillStyle = "rgba(180,83,9,0.85)";
+ ctx!.fill();
+
+ // the silhouette - the curve the old asset never closed
+ ctx!.beginPath();
+ ctx!.arc(mid, mid, r, 0, Math.PI * 2);
+ ctx!.lineWidth = Math.max(r / 200, 0.6);
+ ctx!.strokeStyle = "rgba(138,79,34,0.28)";
+ ctx!.stroke();
+ }
+
+ function tick(now: number) {
+ if (!running) return;
+ const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;
+ last = now;
+ spin -= speed * DEG * dt;
+ render();
+ frame = requestAnimationFrame(tick);
+ }
+
+ function start() {
+ if (running) return;
+ running = true;
+ last = 0;
+ frame = requestAnimationFrame(tick);
+ }
+
+ function stop() {
+ running = false;
+ cancelAnimationFrame(frame);
+ }
+
+ function resize() {
+ const next = Math.round(wrap!.clientWidth);
+ if (!next) return;
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
+ size = next;
+ canvas!.width = Math.round(next * dpr);
+ canvas!.height = Math.round(next * dpr);
+ canvas!.style.width = `${next}px`;
+ canvas!.style.height = `${next}px`;
+ ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
+ render();
+ }
+
+ const motion = window.matchMedia("(prefers-reduced-motion: reduce)");
+
+ // Only animate while the globe is actually on screen and motion is welcome.
+ const io = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting && !motion.matches) start();
+ else stop();
+ },
+ { threshold: 0 },
+ );
+
+ function onMotionChange() {
+ if (motion.matches) {
+ stop();
+ render();
+ } else {
+ start();
+ }
+ }
+
+ const ro = new ResizeObserver(resize);
+ resize();
+ ro.observe(wrap);
+ io.observe(wrap);
+ motion.addEventListener("change", onMotionChange);
+
+ return () => {
+ stop();
+ ro.disconnect();
+ io.disconnect();
+ motion.removeEventListener("change", onMotionChange);
+ };
+ }, [speed, startLongitude]);
+
+ return (
+
+
+
+ );
+}
diff --git a/components/Navbar.tsx b/components/Navbar.tsx
index 3318af8..edb9979 100644
--- a/components/Navbar.tsx
+++ b/components/Navbar.tsx
@@ -218,7 +218,7 @@ export default function Navbar() {
className="logo-hover-zoom h-9 w-auto object-contain sm:h-10"
priority
/>
-
+
TECTONIC
diff --git a/lib/world-land.ts b/lib/world-land.ts
new file mode 100644
index 0000000..88f99b2
--- /dev/null
+++ b/lib/world-land.ts
@@ -0,0 +1,29 @@
+// Geometry for the hero globe.
+//
+// Land is sampled as an even dot field rather than drawn as coastlines: 1344 points
+// taken from a Fibonacci sphere (5200 candidates, evenly spaced with no polar clustering)
+// and kept where they fall on land. Source for the land test is Natural Earth 1:110m
+// (public domain, naturalearthdata.com). Points below 60S are dropped - Antarctica reads
+// as noise at hero size.
+//
+// Encoded as deltas in tenths of a degree so the table stays small.
+
+const PACKED_DOTS: number[] = [-711,-536,-29,20,47,12,-29,19,47,11,-76,7,47,11,47,10,2349,5,-2378,12,2149,7,-2178,9,2473,8,-2426,2,-29,16,2148,6,325,2,-2426,2,-76,5,2225,1,-2101,3,2024,2,325,2,-2426,2,2148,6,-2101,3,2025,2,-2101,4,2148,6,-2101,3,850,0,1175,2,-1901,1,1700,1,325,2,-2101,3,849,0,1175,2,-201,2,-1900,1,2024,3,-1900,1,850,0,1174,2,-200,2,-1901,1,850,1,1175,1,-1901,1,1700,1,325,2,-201,2,-1900,1,850,0,1174,2,-201,2,-1900,1,850,1,1174,1,-1900,1,850,1,850,0,324,2,-200,2,-1901,1,850,0,1175,2,-1901,1,1700,1,-1900,1,2225,0,-201,2,-1900,1,850,1,1174,2,-201,2,-1900,1,850,0,1174,2,-1900,1,850,0,850,1,324,1,-200,2,-1901,1,850,0,1175,2,-201,2,-1900,1,850,1,1174,1,-1900,1,850,1,850,0,324,2,-2101,2,1901,0,-1051,1,1175,2,-1901,1,1700,1,325,1,-1051,1,850,1,-1900,1,849,0,1175,2,-201,2,-1900,1,850,0,1174,2,-1900,1,850,0,850,1,324,1,-200,2,-1901,1,850,1,1175,1,-1901,1,1700,1,-1900,1,850,1,1174,1,-1900,1,850,1,1174,1,-1900,1,1699,1,-1900,1,850,1,1174,1,-1900,1,850,1,850,0,-726,3,850,0,-1901,1,850,1,1175,1,-1901,1,1700,1,-1900,1,850,1,1174,1,-1900,1,850,1,1174,1,-1900,1,1699,1,-1900,1,850,1,1174,1,-1900,1,850,1,-201,2,325,1,850,0,-1901,1,850,1,1175,1,-1901,1,1700,1,-1900,1,850,1,1174,1,-1900,1,850,1,-726,2,1700,1,-1901,1,850,0,1175,2,-1901,1,850,0,-1050,2,849,0,325,1,850,1,-1900,1,849,0,-725,3,1699,1,-1900,1,2225,0,-1900,1,849,0,526,0,-2426,1,525,0,850,0,850,0,525,0,-2425,1,2750,0,-3276,1,1376,0,1375,0,-2426,1,1375,0,-1900,1,1375,0,850,0,-1051,1,1901,0,-1051,1,-726,2,1901,0,-1051,1,-1050,1,850,1,1174,1,-1900,1,850,1,-1051,1,325,1,1699,1,-1900,1,850,1,-726,2,850,0,-1051,2,850,0,1175,2,-1901,1,850,0,-726,3,850,0,-1050,2,850,0,-726,3,850,0,-1051,1,325,1,-201,2,850,1,1174,1,-1900,1,850,1,-1051,1,850,0,-726,3,850,0,-1050,2,324,1,-200,2,850,0,-726,3,850,0,-1051,1,325,1,525,0,-726,2,850,1,-726,2,850,1,850,0,-1900,1,849,0,-725,3,849,0,-1050,2,2225,0,-1900,1,-201,2,850,0,-726,2,850,1,-1051,1,325,1,525,0,-726,2,850,1,-1050,1,324,1,-200,2,850,0,1174,2,-1900,1,850,0,-1051,2,325,1,-201,1,850,1,850,0,324,1,-1900,1,850,1,-1051,1,325,1,525,0,-726,2,850,0,-1050,2,2225,0,-1901,1,-200,2,850,0,849,1,325,1,-1900,1,850,0,-1051,1,325,1,525,0,-726,2,850,1,-1051,1,2225,0,-1900,1,-201,2,850,0,850,1,325,1,-1901,1,850,0,-1050,1,850,1,-526,1,-200,1,850,1,-1051,1,2225,0,-1900,1,850,1,850,0,-1901,1,850,0,1175,2,-1901,1,850,0,850,1,-1900,1,324,1,-200,1,849,1,1175,1,-1900,1,849,1,-1050,1,850,1,-726,2,850,0,850,1,-1901,1,325,1,850,0,-1051,2,850,0,850,0,-1576,2,850,1,850,0,-1900,1,850,1,-726,2,850,1,-726,2,850,0,-1051,2,850,0,850,1,-1576,2,850,0,850,0,-1901,1,1175,2,-1051,1,850,1,-726,2,850,1,850,0,-1900,1,850,0,-726,3,850,0,849,1,-725,2,-1051,1,850,1,850,0,-1576,2,850,1,850,0,-1901,1,850,0,325,2,-1051,1,850,0,124,3,-1050,1,850,1,850,0,-1576,2,850,1,124,3,-1051,1,850,0,-726,3,850,0,850,1,-1900,1,849,0,325,1,-1050,1,849,1,124,3,-1050,1,850,1,850,0,-1576,2,850,0,-1051,2,850,0,325,1,-1051,2,850,0,124,3,850,0,-1900,1,850,1,-201,1,525,0,-1050,1,850,1,-201,1,325,1,-1051,2,850,0,-201,2,325,1,-1051,1,850,1,325,1,-1051,1,526,0,324,1,-200,2,324,1,-1050,1,850,0,849,1,-1050,1,525,0,-200,1,525,0,-1576,2,850,0,325,1,-526,1,1375,0,-1900,1,850,0,-201,2,325,1,-1051,1,850,1,325,1,-525,1,850,0,-526,1,-200,1,324,2,-1050,1,850,0,324,1,-525,1,325,1,-201,2,325,1,-1051,1,525,0,325,1,850,0,-1900,1,849,1,325,1,-1050,1,849,1,850,0,-1050,1,850,1,-526,1,-200,1,324,1,-525,1,325,1,324,1,526,0,-1901,1,850,0,325,1,-201,2,-200,2,849,0,-525,1,850,0,-1050,2,324,1,-200,2,-201,1,325,2,-201,1,-201,2,526,0,-201,1,850,1,-1901,1,850,0,325,1,-201,2,325,1,-525,1,849,0,-525,1,-200,2,324,1,-525,1,325,1,849,0,-1900,1,1375,0,-525,1,325,1,-201,2,-201,2,850,0,-525,1,850,0,-1051,2,325,1,-201,1,850,1,-1900,1,850,0,525,0,-201,1,526,0,-726,2,-1051,2,850,0,325,1,324,1,526,0,-1051,1,325,1,-201,2,325,1,525,0,-1900,1,849,0,850,0,-525,1,-201,2,-1050,1,1375,0,-525,1,1375,0,-1051,1,850,0,-1900,1,1375,0,-525,1,850,0,-526,1,-200,2,-1051,1,1375,0,-525,1,850,0,-525,1,324,1,-525,1,-1050,1,1375,0,-1051,1,850,1,850,0,-525,1,-525,1,849,0,-525,1,850,0,-1050,2,-1051,1,850,1,325,1,849,0,-525,1,-525,1,850,0,-525,1,-201,2,850,0,-525,1,-526,1,850,0,-525,1,324,1,-525,1,-1050,1,1375,0,-201,2,850,0,-525,1,-525,1,849,0,-525,1,850,0,-1050,2,849,0,-525,1,-525,1,850,0,-525,1,849,0,-525,1,-525,1,850,0,-1900,1,1375,0,-201,2,850,0,-525,1,-526,1,850,0,-525,1,850,0,-525,1,-526,1,850,0,-1900,1,850,1,850,0,-526,1,325,1,525,0,-1050,1,850,0,-526,1,850,1,-1050,1,324,1,526,0,-1051,1,325,1,525,0,325,1,-1051,1,325,1,525,0,-1900,1,1174,1,850,1,-1051,1,526,0,324,1,-525,1,850,0,-525,1,-526,1,850,0,-1900,1,850,0,525,0,325,1,-526,1,850,0,-525,1,-525,1,850,0,-1901,1,1375,0,850,0,-525,1,-525,1,850,0,-526,1,-525,1,850,0,-525,1,850,1,-1901,1,850,0,325,1,525,0,-1900,1,1174,1,525,0,-200,1,525,0,-1050,1,324,1,525,0,-1900,1,2225,0,-1050,1,525,0,324,1,-1050,1,525,0,325,1,-525,1,849,0,-525,1,-525,1,850,0,-1901,1,2225,0,-525,1,-525,1,850,0,-525,1,-526,1,850,0,-525,1,-1375,1,2225,0,-1051,1,525,0,325,1,-1900,1,1375,0,324,1,-525,1,850,0,-1050,2,849,0,-1900,1,1375,0,850,0,-525,1,-525,1,849,0,-1900,1,1375,0,325,1,-1900,1,1375,0,324,1,525,0,-1900,1,850,0,850,1,-1900,1,1375,0,849,0,-525,1,-525,1,850,0,-1900,1,1375,0,-526,1,850,0,-1900,1,1375,0,850,0,-525,1,-526,1,325,1,525,0,-1900,1,1700,1,324,1,-1900,1,850,0,525,0,325,1,-1901,1,2225,0,-525,1,-525,1,850,0,-1901,1,1375,0,-525,1,850,0,-1900,1,2225,1,-1901,1,850,0,525,0,325,1,-1900,1,1375,0,324,1,325,1,-1900,1,1375,0,-526,1,850,0,526,0,-2426,1,2225,1,-1051,1,526,0,324,1,-1900,1,1375,0,325,1,-1901,1,2225,0,-1900,1,1375,0,-525,1,850,0,-1901,1,2225,1,-525,1,325,1,-1901,1,1375,0,325,1,-1900,1,1375,0,850,0,-1901,1,1375,0,-525,1,325,1,525,0,-1900,1,1699,1,-1900,1,2225,0,-1900,1,1375,0,324,1,-1900,1,1700,1,525,0,-201,2,-1900,1,1375,0,850,0,-525,1,-1901,1,2225,0,-1900,1,1375,0,850,1,-1050,1,525,0,-1901,1,2225,0,-525,1,-1900,1,2225,0,-1901,1,1376,0,-526,1,850,0,-1900,1,1375,0,850,1,-1901,1,1375,0,325,1,-1900,1,1375,0,850,0,-526,1,-1900,1,2225,1,-1900,1,1699,1,-1900,1,1375,0,850,0,-525,1,-201,2,525,0,-1900,1,850,0,850,1,-1900,1,2224,0,-1900,1,1375,0,325,1,-525,1,-1375,1,2224,0,-525,1,-1900,1,2225,0,-1900,1,849,1,850,0,-525,1,-1375,1,2225,0,-1900,1,1375,0,-526,1,850,0,-1900,1,1375,0,850,1,-1051,1,526,0,-1901,1,2225,0,-1900,1,1375,0,325,1,-526,1,-1375,1,2225,0,-525,1,-1900,1,1375,0,850,0,-1901,1,1375,0,-525,1,850,0,-525,1,-1375,1,2225,0,-1901,1,1375,0,325,1,-1900,2,2225,0,-526,1,-1900,1,1375,0,850,0,-1900,1,849,1,850,1,-1900,1,2225,0,-1900,1,1375,0,-526,1,850,0,-525,1,-1375,1,850,0,1375,0,-525,1,-1901,1,1375,0,850,0,-525,1,-1375,1,850,0,850,1,-1901,1,2225,0,-525,1,-525,1,-1375,1,2225,0,-1901,1,2225,0,-525,1,-1900,1,1375,0,850,1,-1901,1,1375,0,-525,1,850,0,-525,1,-1375,1,2225,0,-1901,1,1375,0,-1900,1,1375,0,850,0,-525,1,-1375,1,850,0,849,1,-1900,1,1375,0,850,0,-525,1,-1375,1,1699,1,-1900,1,1375,0,850,0,-525,1,-525,1,-1376,1,2225,0,-1900,1,1375,0,-525,1,850,1,-1901,1,1376,0,849,0,-1900,1,1375,0,-1900,1,1375,0,850,1,-1901,1,1376,0,849,0,-1375,1,850,0,-525,1,-1375,1,2225,0,-1901,1,1375,0,-525,1,850,1,-1900,1,1375,0,850,0,-526,1,-1900,1,1375,0,850,1,-1900,1,1375,0,-525,1,849,0,-525,1,-1375,1,2225,0,-1900,1,1375,0,-1900,1,1375,0,849,1,-1900,1,1375,0,850,0,-525,1,-1375,1,850,0,-1375,1,2224,0,-1900,1,1375,0,-525,1,850,1,-1900,1,1375,0,849,0,-525,1,-1900,2,1375,0,850,0,-525,1,-1375,1,2224,0,-525,1,-1900,1,1375,0,850,1,-1900,1,1375,0,-1901,1,1375,0,850,0,-525,1,-1375,1,2225,0,-525,1,-1376,1,850,0,-1375,1,2225,0,-1900,1,1375,0,-525,1,850,1,-1901,1,1375,0,850,0,-1900,2,1375,0,-1900,1,1375,0,850,0,-526,1,-1375,1,850,0,1375,0,-525,1,-525,1,-1375,1,2225,0,-1901,1,1375,0,-525,1,850,1,-1900,1,1375,0,850,1,-1901,1,1375,0,-1900,1,1375,0,850,1,-1900,1,1375,0,850,0,-526,1,-1900,2,1375,0,850,0,-525,1,-1375,1,-526,1,1375,0,850,0,-525,1,-1375,1,2225,0,-525,1,-1375,1,849,0,-1375,1,2225,0,-525,1,-1375,1,1700,1,-526,1,-1375,1,2225,0,-525,1,-1375,1,2225,0,-2750,1,2225,0,-526,1,-1375,1,1700,1,-525,1,-1375,1,2225,0,-526,1,-1375,1,850,0,-1375,1,2225,0,-525,1,-1375,1,2225,0,-526,1,-1375,1,2225,0,-2750,1,1375,0,850,0,-525,2,324,2,-1900,1,1375,0,850,1,-1900,1,1375,0,-1901,1,1376,0,849,1,-525,1,-525,1,1375,0,-525,1,-525,1,-1376,1,2225,0,-525,1,-1375,1,-525,1,2225,0,-1901,2,1376,0,849,0,-1900,2,1375,0,-1900,1,2225,1,-525,1,849,1,-2750,1,2225,0,-1900,2,1375,0,850,0,-2750,1,849,1,1375,0,-1900,1,1375,0,850,1,-525,1,850,1,-526,1,850,1,-2750,1,2225,0,-525,2,850,1,-2750,1,2224,0,-525,1,-1375,1,2225,0,-2750,1,2225,1,-1900,1,1375,0,1375,0,-526,1,-525,1,850,1,-2750,1,2225,1,850,0,-2750,1,1375,0,849,1,-2750,1,2225,0,-1375,1,2225,0,-2750,1,1375,0,-525,1,1375,0,-1900,1,1375,0,849,1,-2750,1,2225,0,850,1,-2750,1,1375,0,850,1,-525,1,-1375,1,2224,0,-2750,1,2225,0,-1375,1,2225,0,-2750,1,1375,0,850,1,-2750,1,2225,0,-1375,1,2224,0,-2750,1,1375,0,850,1,-525,1,850,1,-2750,1,2225,1,-526,1,1375,0,-2750,1,2225,0,-525,2,850,1,-2750,1,2225,0,-1376,1,2225,0,-2750,1,2225,1,-2750,1,850,1,1375,0,-2426,3,1375,0,325,2,-525,2,324,3,325,2,325,3,-526,2,-525,1,325,3,-1375,1,850,1,849,1,-1050,4,850,1,-1576,5,2225,1,-525,1,-525,2,324,3,-1375,1,850,1,1375,0,850,1,-2750,1,850,1,1375,0,324,3,-2750,1,3075,2,-525,2,-1375,1,-526,2,2225,0,-525,2,-1375,1,850,1,1375,0,-525,2,-2426,5,2225,0,-1375,1,1499,9,-1375,1,-525,2,1699,3,-1375,1,1499,9,-1375,1,-525,3,325,4,-526,2,850,2,850,2,-1050,5,-526,3,325,5,325,5,-526,3,325,6,-525,3,1174,8,-525,4,850,3,-1375,1,324,7,-200,12];
+
+/** Absolute [lon, lat] pairs in degrees, one per land dot. */
+export function decodeDots(): Array<[number, number]> {
+ const out: Array<[number, number]> = [];
+ let lon = 0;
+ let lat = 0;
+ for (let i = 0; i < PACKED_DOTS.length; i += 2) {
+ lon += PACKED_DOTS[i];
+ lat += PACKED_DOTS[i + 1];
+ out.push([lon / 10, lat / 10]);
+ }
+ return out;
+}
+
+/** Cities used as network nodes on the globe. */
+export const HUBS: Array<[number, number]> = [
+ [-74, 40.7], [-0.1, 51.5], [8.5, 47.4], [103.8, 1.3], [139.7, 35.7], [55.3, 25.2], [-46.6, -23.5], [28.0, -26.2], [-118.2, 34.1], [72.9, 19.1], [13.4, 52.5], [-99.1, 19.4],
+];