From e2e8e2a740cf5f6d0eee057cceebe9dc76166559 Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Mon, 24 Aug 2026 23:58:37 +0530 Subject: [PATCH 1/3] fix: remove hard drop shadow from TECTONIC wordmark Pure black at 40% over the bronze gradient muddied the gradient and made the wordmark read as pasted onto the bar. The navbar's own box-shadow is kept: the bar is fixed and translucent, so it is what separates the bar from the hero on scroll. --- components/Navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From bd32f4ba93ccaec98576916d96d00274e62ac420 Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Mon, 24 Aug 2026 23:59:08 +0530 Subject: [PATCH 2/3] fix: remove bouncing scroll arrow from hero A hard-coded arrow glyph in an animate-bounce wrapper. It bypassed en.json unlike the rest of the hero copy, was invisible to screen readers and was not keyboard reachable. The hero fits within one viewport on desktop, so no replacement cue was added. Also drops the matching wordmark drop shadow in the footer. --- app/page.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 812e85f..e06a0c3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -283,10 +283,6 @@ export default function Home() { - {/* Scroll Indicator */} -
-
↓
-
{/* Features Section (styled to match provided design) */} @@ -594,7 +590,7 @@ export default function Home() {
Tectonic logo - TECTONIC + TECTONIC

{tFooter("desc")} From 3cbf076aa7a9aa5c18e65329c8f1bd0b71488099 Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Mon, 24 Aug 2026 23:59:33 +0530 Subject: [PATCH 3/3] feat: replace clipped hero raster with a rotating dot globe tectonic-hero.png never rendered as a sphere. The source file is 626x367 and the globe was already clipped inside the asset, so no CSS change could recover the geometry. It was then upscaled ~2x, mirrored, and pushed off-canvas by negative offsets on .hero-visual, leaving only an arc. Replaces it with a canvas component that draws a slowly rotating globe, with landmasses as an even dot field in the brand palette. Land is sampled from a Fibonacci sphere and tested against Natural Earth 1:110m land polygons (public domain), giving 1344 dots at 2.1 KB gzipped against 139 KB for the PNG. No new dependency. Points are converted to unit vectors once on mount, so each frame is a rotation and a dot product per point with no trigonometry in the loop. Honours prefers-reduced-motion, pauses via IntersectionObserver when off-screen, and redraws at device pixel ratio on resize. The .hero-visual absolute positioning is removed: the globe is now a normal child of the hero grid, so it aligns with the headline instead of floating against the section box. --- app/globals.css | 33 ------ app/page.tsx | 14 +-- components/HeroGlobe.tsx | 232 +++++++++++++++++++++++++++++++++++++++ lib/world-land.ts | 29 +++++ 4 files changed, 264 insertions(+), 44 deletions(-) create mode 100644 components/HeroGlobe.tsx create mode 100644 lib/world-land.ts 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 e06a0c3..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,17 +270,8 @@ export default function Home() {

-
-
- Tectonic hero illustration -
+
+
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/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], +];