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 hero illustration -
+
+
- {/* Scroll Indicator */} -
-
↓
-
{/* Features Section (styled to match provided design) */} @@ -594,7 +582,7 @@ export default function Home() {
Tectonic logo - 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], +];