diff --git a/.oxlintrc.json b/.oxlintrc.json index 1d32906..1b1db6d 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,3 +1,6 @@ { - "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/.astro/**"] + "$schema": "./node_modules/oxlint/configuration_schema.json", + "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/.astro/**"], + "jsPlugins": ["@northguild/gmt-oxlint"], + "extends": ["./node_modules/@northguild/gmt-oxlint/config/recommended.json"] } diff --git a/apps/site/package.json b/apps/site/package.json index 00ed23a..eb0b627 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -10,6 +10,7 @@ "preview": "wrangler dev" }, "dependencies": { + "@northguild/gmt": "^1.19.0", "@northguild/theme": "workspace:*", "astro": "7.2.7" }, diff --git a/apps/site/public/stars.bin b/apps/site/public/stars.bin new file mode 100644 index 0000000..b0b03d5 Binary files /dev/null and b/apps/site/public/stars.bin differ diff --git a/apps/site/src/components/Aurora.astro b/apps/site/src/components/Aurora.astro new file mode 100644 index 0000000..2a914bc --- /dev/null +++ b/apps/site/src/components/Aurora.astro @@ -0,0 +1,307 @@ +--- +import { + SITES, + SKY_CREDITS, + compass, + coords, +} from "../scripts/aurora.sites"; +--- + + + + + +
+
+ { + SITES.map((site, i) => ( + + )) + } +
+ { + SITES.map((site, i) => ( + + )) + } +
+ { + SKY_CREDITS.map(([what, source]) => ( + <> +
{what}
+
{source}
+ + )) + } +
+
+ + + + diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro index 786b77f..6928e46 100644 --- a/apps/site/src/pages/index.astro +++ b/apps/site/src/pages/index.astro @@ -1,5 +1,6 @@ --- import Base from "../layouts/Base.astro"; +import Aurora from "../components/Aurora.astro"; import RepoCard from "../components/RepoCard.astro"; // Descriptions are each package's own `package.json` `description`, verbatim. @@ -22,6 +23,7 @@ const repos = [ --- +

NorthGuild

Open-source tools for developers, built in the frigid north.

diff --git a/apps/site/src/scripts/aurora.moon.ts b/apps/site/src/scripts/aurora.moon.ts new file mode 100644 index 0000000..ad0506f --- /dev/null +++ b/apps/site/src/scripts/aurora.moon.ts @@ -0,0 +1,7 @@ +// Generated by scripts/build-moon.mjs from NASA SVS's CGI Moon Kit. Do not edit. +// 128×64 greyscale albedo, equirectangular, longitude 0 at the centre. +export const MOON = { + width: 128, + height: 64, + data: "qKioqKioqKipqampqKioqKioqKipqKmpqaipqamqqqqrq6urrK2trKurq6urq6urq6uqq6urq6qpqKenp6enqKenp6enpqenp6anp6enqKeoqKioqKioqaipqKmpqKeop6eoqKmpqqqqqqqqqamoqqmoqKenp6eoqKinp6enp6emp6ioqKenpqempqekpqeqq6yrq6moqqelpaSkpKWmpqenqKenqKqrqqioqaqqq6qqqqmqqaiop6enqKipqqqpp6WmpaSlpaWmp6ipq6uqqqmpqqmoqKioqKyqqampqamoqKioqKamp6Wmp6enp6iop6eoqainp6iqqKenqKeopqanp6enqauqqqqtra2wr6+urKurqqioqKenqqqop6ioq6ysq66wsbCxsK+trKyrq6ysrq2traysrKqpqKmprKuqqqqpq6qsra2sra2trKqpqquqp6ipp6epq6qnp6ioqKmpqKipqamqq6uqqamnqKepqqmpqainqqupqKmop6ampaWmpqanqaqrraytra6trKupqKipqKiopqenqKemp6eqrrW3tLCwsK+wsbOysbCxsbKysLCurq+vrqyrqqqrqq2wsK+vsLGxsK+wsK2sq6yqqqmqqaanqKiop6ioqKmoqKurraurrKytrqurq6yrra6tq6urrKuqqampqainp6eqqKemp6iqrKurq6upqqqoqKWkpaSlp6alqqqnpqWmpaenqKmqqaipq62trrGztLKvrq2usbKxsbCwsbGusK2wsLOysa6urq2srKqrq6upp6anqKipqamqqamop6ioqKeoqKmqqqioqaqqq6qtrrCys7Gyr66traysqqinp6qpqqqpqaenqKioqKimpqWnp6empaSkpqeoqKelpKeppqWlpKSmpqinqKeorKurqa2wr7GysrKzsbGwsbGytre4t7e2trOyrq2rq6iop6ilo6anpqeoqaiqqKenqqqrqqiqqampqaqqqKioqKiqqautraywsrGopaepp6inq6urqqusp6enp6ipqaioqaeop6eoqqWkoqKjpaalpaSmpqeop6impaWnpKWnqaqtsLGtrKyqq6qoq66uq62vsLGxs7e3t7e0sa6rp6SlpaWioKChpKWmpqenqKmqq6qrrK6wrq2sr62trq+urauqqqquqqepqKmnqKSkpKWlpaiqqqunp6emqKqqqqyqq6mnqqmmp6WloqKipqOhpKSlpKemp6mrqaupp6Woqaursba5t7S0sK2pqamoqKqrra6vsbCusbO2tLCsq6ampKejo6Oko6KjpKepqaysrrO2tri5u7i1ubWztLOys7S2sa+tra2oqq2ur62sqqmoqqqppaSlpqWlpqeqqq2opaeoo56hpJ6bm5yam5uam5WdmZeenqKhqaiuqqqopqurq6eutLi2tbWwqqiopKCfo6efoaKhoKKoq7SypqKgmaGhnp2foKKYmp+hq6+wsbnCxcTDxcrLxsDCxcfExcC+u7u4srGvsK2oqauvq6ewrK2rq6enpKSlpaqrpqe1s6mpqKqknJqdnqKZk5SYl5mZk5mVkpqhmqGboqSpp6mqqaqqqa+0r6qnqqibmZ2RkZORl5mRiYOJkZaWm5mZl56YoJuRj5WTm6Ccoayrsr3BwLWxuLu/wMjN08/Nz8rGx8XGw7uzsrGsqaaropyjp6mmoqeio5yblqSjrLCkoZuWlpydnaOorrWvo5iZlpaYlJabkpGTopubmZ+uraSgp6CcnqSkp5+fpKCfmpyRk4qIhH97dHJ3enZ4fnF3gouQh4WMjYqPkJadmZynr7rCtLWzqaOlrq+urLW5vsDGyczRy9HKvbi0r6utpZycmp2hoZ+knqGfmJyanKaqoJSYp6Cel56ZmaOtsrmimZ2blZabmJSWkJGWlJOUlpqjmJWOjZGQlJeeop+XlZKOiYaBfISGgHx5cHFvcnVzbHN3eX+De3t+en55fn+Ji52rs7ursKSnpJWZqKB+e4yaqri+xcbDusG1r6utp6eem5OXmZqenJaZmJubn5ySlqGUlpicnp+jnpudop+gpJ+ioKOdoZ6TlpeRk5CTk5CRk5iXj42Nin1ucX+SlI6FgXl4dHNzdYWQgIJ9gYCKiIqHh352ent5dnZzdG9ubW+EjJymrKyblqGhnpqZkIKCkJyrsbe7wsi7t7qys6mjmpeTlaCelpOZmJOamZubmJWWkI2ho6qpoqCal5aWlJual5+Vnp2emJ2Ul52VjZKSjZKWmJqhmIqEgGRlb3d4dWdpZG6Jg4uZjYyGgn97j4tld4WajYF8gnp6gHt3cHFxeIiTmZqWoJmen5iloZGXpamuq6ivur2+zcW3ureqpaWdnJudqJmUj46TlZmZl5iZmJSYlK24s6agnZqcp6WalZWTk6ChqJiRlZSloZiOlZGPj46SppWJh4JqZ2lobGpsZVtcaXiTkZOIfIB7bGJnbnJ2gY6VkIR7fYqLgH19f3qIkpeXlZiSiZaMmp6TkJiVoqSmnaKxvcfDubi3t7O1sauqp6SelJmQjJGXnKqimJmYpKuep6u2tKygoqOqrJuamp6cn56cm5GXnaCZm56emJOVko6Wi3yAbV5cXF1jYGVobHaEjoyRcmJibFpXW11saGJpeYmRiH+Gg4eNgH16hYeSkZSWqo6CgYyfl5iblpGbnqWjp66xtrawt77HtayxuL+vqaCioJeTipCVn5mTm56oqp+tq7u+qKCdnqCioqOnqqShmaOkqqOUnKepsquckpWOjpOEfndeWFxTV1lZYVpWWHV8foRpV11YTU5XXF5hYGRrhIqNiY6XjIZ9eoOGgIKEiZOUioqChomAlpman56hnp2hrKust7Cts7OtrLW3sKamoqmooqWXlo+TnJeaoKKjo620t6uko5+fn56hrLK1uamfnKOam5KYl5qsrZ2Sk4+HgoGAeHtsUlBTVFFZVVlTbnh1dmhTS0xQT1JXX1pcZ3F1hoiOjIiGeW90fX98gY2MmJCEm5OPj4+WqqicoqKho56qtbC8ta60q6GopK6kl5aZnpylqrKtoqCanZ6fnKGdssq6sraqpaeio6W0wsCyp6OSnKCPjZKQlJ2Vj5CXlZKJhX1vWFVSTlFRVVtSVlpka2ttUlVTTEpNUlNeW2BreHZ2hJSBd4F+aG9ydmd2kYqNjY+WmJuPjo6UmJyepJ+hpqKtw9XEtreupamrpKugmaGcnZ6lp6OmpaWnnJecoKKwvsC5sqqfpqqvrLW1t6ihnZ2Ul52ai4yQkIeHlZiRkI2OgmxcTFBQUFZYUVBTWVhcX1tQV1dST01TVVhYV2dweW99gVxXXm1sXW5vaXeKmZWQkZeqmpCNiI6Rl5unoqCdpq3Q3bi1vb2yqK6lo52joqOenJufnaCdqLGvpaCgr6qyp6ivoqitpqaxsLCtqKSsqJybnp6UlJqXjYmNkI+Kh4dzXUVKUGRUTVJTUVdZWVZaV1RcVE5PTlBVVVZTX257bXRkUFdWWF1agYJ9jZGYnZOVm6CTh4mVnJScoZyfp6aZo6Gsr6y0srqzsKSjm6ClsbO0nZGZoqCdqrStqrKrrq2eo6mptrWsrbG1rqypo6iupqaloJyVkZ6TjIuMlpOGhW1WXWBeUlFMS01XVVxkW1hYXVtVVFNTVFNXVV5rcnxxZFtTU1FZWU5ogZCUmKGjnJWelZKNjpeeop2gpauvpZ6Vjo6ep6ewtrW1n5qYn6OirJhzZZGnppeorKWrv6+wra+Znq+8uLGttsKpra2wp6ibmJiXoIuKmZCLjo2ZmZaCalVjX15eTU5MTVFUbmpfYFZdVlFXV1NPVmphdHVsY3hmU1VTVllSS1B+n52Wkp2Yl4mKlo2XkZeaoKewsKOSj4+Njpmlp6Sys76om56dm6KjcF5fg6OmmJecm6e2tsC5v7m1vru+r7K8uK2op7GjopqYnpucoZSSjZCXmqGdm416Zl9tZ09NT0tLUVqCf2VgUVBRXVJbV1ZVV1ZldXd2f15KSU9ZU01PV3uSk5OFhoWGjnp0f4uKlZakqqadn5SOk46TkqGmpamkpaWhnaKqrKOEcHaPoaGYnKars7W0urSytbrFu7i1r66qo6arsKKdn5+qrqmrpZ6fnJyUpp6ej4p2ZWhNTWNMRkdPWGFgY1tXVVloX1tdW1paV1RagYpzV0dNTlxPT05Ve3txl4iLj3JnZF5eaXmLlJ+fmJ+YmpORkJCVn6GipKCfnKWhpqumoaGVnJeToJqkp6iyrq67rKu0vLS6qKOjoqCgqaKknJ+fobKxrqyspJ2WlpednqOklI1uV1JKUkRDRU1OUFRVY1lTVl9iZ1lhZGBkX3N8cGhYVlhQUUtLTVZvY2B4maOXal1XVExOcISWjJJ8hJidjZCNiZSWm6qpqKmkoqCloq62n6GclZemqKexu7ywuLetqbm2p760n5mZn6CjoqKio6muraqwtbe6qJ2XipOam5WWjXtiU01HSkVCQ0hLUlRgbFFdcXh5c3N0c2hmb21ZVVxxVHhmU05KRVFTTHKDpp1vV1NOS1tvlYh6gmNqf4yIj5OLipaarbKxq6Wmo6WfnKCemaulnrK3s7q+wNrJrquywriptK+knJyknJyrpKCgo7qpr6qqsKqlnpeRpJ2Yj5eNiGxqVU1MR0FBR0dUUl1mYldyhH2JhXl1aFtKVlROS2JfblJAQEZKQkJAWXeNoHlZUlJVj3yUjIF+XFdfY3aJlpKMlpuwtbKoqbSnpZ6am5OVnpidrbS1v7q9wLyztLi+vrSupaOmpaieoqqlq6OgoaKioqCupJ6clpaelJKTlZici4BvUkhHRkZFSEdPZX10W2x9g6GReXZmV1Nna1RHWnhxST5AS0JDQEVKYHSWkH1ueX+GgoKOk4N/b2t1eYSOlpaVk7LAtq6wrq6gm5yZmZ6XmqClvLC1v7q5uLO2sK+0p6ulp6ygn7Sqsa2uo6Wdm5+up6anoJykopeTlZiXrKydjYVoTE5TTEpLSlVthn5maXF7eXJkY1dIaHx+eW11hXlQSU1CRURERldkZ22HjIeId25mdIuUk4R2e4uEgo+Xl5uVnL++uK6mqayjoKGgnp+fnqCmsrTAtba2uayloaqhqraroJ6dqqqutbWunKKgpJmlrbOrtMSooqOZmJellJqUmYZeR0NFSkpLTGBpaWRmbnt2alddUktdcnyOiJuNeWFNR0dITlFOVWZsV1R6hYOFcXaLiJGYh29qdX6Dn5+fsJmht9HBs6ujo52jraWgopqkqqq3uq6urL60paOjq6O4xK6tpqOqsLK+wKyor5+Vk56pr7GxtqadnpaYlY6So5OXiFhLQ0NHSUlIVVxcXWJnbXd5XVtaYWFeco2WpJiHdlJPTFBoaFReXlZPTEdSanBofn15gohtX09ghJGZlKKtmp6/ysmysKyrnZ6mo6KhnqKkqqmwr6ansK+gn5+mq7Gusbispquqsbe9srCpopiSmaStqKSkpqiioJ2QioqDmJp/ZU9HQkZJSERKS1JYW2VgYW5sZmh6fm58j5eUkpWMhXNVWYuFgXFUVUtLTFVic3d6goGIfGFaXWqOjYiPmqOSmKvEy8Syp6ymrqmgoKeipq2nlpCipaWssaKboa2qq6Crsa6mqqulrreqpKablpOXnKOqqqeypqmknpWRi4mTa396XkZBTU9ISEtJT1hfXVdicHNnjY6NioiPlpGSlqWlh2Nsh4SAd1xVUU9XZGF1fnyBiIl9bGZ8hIqLlJCQlI+NkbrRyLKlo5+mn6Cdnpqit6mdl5ehoq+tpaipuLKxqqivqKWppaikoqGeo5yWlZSeoKuxr62tq6Wam5WZkY1ZjZFqVEZCTUlJSVFQWnZoUl9wd1lyhoqNi5GinpeWrZt9b3GGjpV7YVRSUV1ohoiDgISFhIl5a3qNjZONhpOamZKTrsq4qJ6anJyUlpuZmaSopquinqqnsKaipq27xruqorCqp6SjoJyblpuYnpiRlqa3vLW4tLOopqGPl5eMj46Pi39lT1ZOUElST05Xa1dQVl93Wl1/iH2Gh5WSlKKhqpmOfXuDjpV9ZVpaaXuclo6HiJKKkpKKkZeclY+Eip2cjoabqqqeoZWYlY+VmZWZlpewt6KcoqWqnqqzsrzCt6uloLappaKdmpeZj5OYlJGmpLm+vb/AurKYm4mQlI+Sk4+Qn39gcVxhYXBcT01ZVVlYYHBjXnKEiYaKjZCToJqjrp+NdmV1lYZ5dW9ncICCjIaIg4ySkZuYm5qRi4eMkYyHi4+XoZ+YmZmPjpeYkJiam6ippZyxppyTmKWyq6qynp2gnp6opaSgnJePkJGXmZ2jrLS6s6aUpaOQmpORlJSKhouOg35rbG52e2pjUk1qfFtgXlhgaoaKh4uRm5WTk6CvsIJqZ3OZi4aEgWdyfnWDgH+SjI6SoJyin4+Pi5CbjIWPlpGdmZialZuTlZeVkZeboKCZo6ChkJCTnp6Vn6yakJudlqCZm5qcmJKZmJicoKOnrqmOboSAlJiYk5iOkJCTmY+FgHZ5iHx0YFhcY3RnXGBaV1WAi5OMmo+RkI6YlaKujnVnX45+hHRueIGEdoF9f42WnJ6pm5ulnpiSi5GEiKOcnZ+WmJeTj5Kgi6aUj5aTk5qtk5qMlIiJhI+XmpiUkomPl5SOi4uWmJqjlqSopaiwq6B6Z3igqqGjlpijoKilnpB/goWEXktJTHNrcHBuXVNZW4KUi4mMiI6Tl5OQmp2hg4KKe4x9hHCDn5eAhoOHkJSaiZGVj5SdrJeFgoSRpaCgnHCBoJiakZOUqI+Mi4mKl5idjoiEhI+CjJCQioOLiIiWlpySj5CfnKecn5ybpK+wrJR5iqqqqqOWlpWiwMirn5mMi4FZU0tHX2ldbmRVWFFbcpGMm5CPmp2cjI+frpuQk5KJlZWWlZCUmZOLjYiOiZOPi5eNk5Gbi357h4+Wk5eWm6yan5qcmZaZmZGOiZCKioiCgIKBhH96h4uBhH97f5CTpaWjn5qXl5CRlJymrLKjk5yprrOnnpqWl5mqsqCYmaCJfGBWUlZqdmJoaFVWWGF8mZypm56snJCSm6ajn6OYkJKYoKOpoaKmopefioeMoZ2PjY2LjIh1cYuMioWGiJKQnJmalpSZkZyWmYmRlo+NiZCLbXp8bHGBiHuAfnh8jJ6cpK2knZCSj4+Tl6Grp56amqmnqaSglJORk5WZmpCap5uQd2VqbYNodXuGd3d2jaKnpq2ro5qRk52klpuYmZOZlpmcrrm3rKybo6KPho6QoZmOiYaHhnx8f32Ak5eZkZOQlZyfl6iZjpCbjoSOfYl6eoJ3d3Juc32KdXh8goWHm5qgm6aejouSlpCTlpyek5ChnaGloZqWk42WlpGWiJOdp5SBf3eGcWNueaOjlJmtt8W6q6+mnZydqpygnpORkaGblpmsy7rGs6SqnJSNm4+LioVod4mCg4OHlJWTlpWKlJSJiZCWjXmAhpmTdGtdhHyCeWx2d2lde3xugF9tgIOJlKaZpJqSj42QkIuRkpOVmKWenKSZnJGQmZCLjJiNkoyQhXCBhIOBfIeQk6ipq7q8xrWtraWno5yYlJeVkZOSnJmanaivs7ahm5WSkpSciIBocnZvcnV0fY2eoZ2VlI2RiYSFko94ZISJkJWCdHB+fHptaXd6dWN8f259bXJ8fn6InJyVkZmVlJCMjImNi5KXlpiTmZaUl5OWl46Jpo+GhIeFfoGQjo2Kkp2epa65v8C/ubSvp6SbmqOYl5mZnpiXmJeXoa6fppmVlpuQi4t8e3tua3d2b4F7hpaemZONiZaSi4GKkIyEiZiQioF6eXuAcm98hYx3aXp/bGNkcXd4foWRnZKMlJSPl5KQkI6PjY+Ul5WVmJWZl5KSo5iVh36KjpOPkZeal5qMmaavtrSzur67ta2spqOrn6GjnpSWnZ+joqOlmouOjoyKh4iPgXRtdG1kcXhxeHmFh5eYlY6LkoeJiI+WmZicl42GfXV3foyAd4KDgnVwdHpwZGx7dXl9g4aHk4yGjY2UjYuLi4mNiY+Zm5mZmJmglZGTnJN7doyJg36Rl5mUl5ahpbCxv8Oyt7Szs7OnnKKbnKKTlZmcoqWsp5+WjYuMjo+KiHhzfH56cGNoeXt3dnuHlJuMjYiVjpCHjbizmJSZiYB0d3t7fnyAiIV6dXJudGllZWxwdHd7foiMjIiPi4yWkJKPkJGTkJahmZiVlZiZmJGQl5CNg4V+goqNl5yZlZ6jsLe0tLe5tLaxq6CTkpWUlZehqaWlr7anmo+Li4+Pk5GSb3aFeYN7enV1eniEeIaJjIuMj5aVjYuYkpWNi4R/i3BvgoF1cH+Jj3h1dXR1bm9scnJxend4hpWKkJOPk5mQlJWUlpSZkZmSk5KVoaSYkoyMj4uKgX2Dh4yUmZWYnJ6wtLq9ucW8t7mupqmfn5mZlp+bnZ2ioJuUjJqVoaCMhnVodYJnaHx/gHiHkoiLhoSHk56VjYaJi5WLiYqDg4+JhoCCfmx2f35/en9reHJscmxudHZudnh8goSFkpmVnJOblZ+fk4qPlpaWk5aeoJOMi5OcmY+De4KNkZSVk5qnsKi1v7uvtbCuuaujnpuZnqGeoJ+io6Wfmo+Pm5mThHqRdGpte3ZwfISQg4GGiImSh4qTkoyDh42Qk4+Tg4OIiYOGiYN8c3uCe3h5hXZ8dGhqZ2t4c3t6dnp8foOLlI2SlpOWmpeTkZSanpuVmZuamJSRlpealol6hJaVlpiboaqjq7extKyyq6espKKhnKGkpKCZm6WnmpWcnpOSkZWPhouBbWZ4e4SDgYqIkYCDkpGRk6OakpGQiJGYlpKTiIeLjIt7fH96dn6BeYB0dHt+cm1wdHZ2eXqGg399iIqRjpCQmqCempOUlZuXnJifoKmhm5qZnaWmopecpZ6Xl5+qn6iyv7G1trCsq6uqrKCmpaadoJuhoqCYnJ+fmpeeoqOenKCDhpeTg3p9hX5zgYiPoJqaub6soo+PjJGYmp+PkYd6am9ycoKKiYWKh3yCi4l9enp6fnt+iIuGgoCEi5KWk5acnKChnaCgoJminaesraqopKOnq62wqqqnnp+jqaurq7G/vb+0rKSkp6qqsK6spaSgmpqgn6CdoZ6koaOfoJiar6ydjoKEf4iLh4l/e4uVnJ+utKylk4+TlpyXkJWNg4R8g4WIh4mChZmLho6TlISCgH+BgoaHiomJi4yVkpKVl5Wbm6ufoqegoKGeqK2wrrG0s7OwqqSvr6msp6aos6+yvsm/xrmtr62vq6qvpqWmqaeko6Ofpa6sra6tsamfnaWuppuYi3yKiI+XmKCYlpuYmZ+stbCjnZ+dm5WOkpGLhZGHhImMj4mXmJCPjJickIyPj5KTkpGLipOQlJeXm5mdnaKkoZuuo6mkpqqpqa+vr66ys7Cus7Wyrainqauvs7a/wsK+v7Kws7OwqbK1sKeorq+qq6qrr7a6tLm1sq+qqaipoZ2Yl5SZqq2mpKWlpp6pp6ayw7Khn5+inpqQl6agnpSOjZOWlaShmZ2enJmYmJ2cnp6hoJ6cnp6fn6GhoKaipaGlp6WpqKmorKupq66xtbe7t7WwrK2rq6iprKyrr7SytbOwsa+srqqsr7Wrrq+uq62vrqyprK2ws7GpqKWop6ShpKKeoJ+fnpudoaCjoqKhpaasraSkqa+opJ+hoqisqqaenJyanJ2enp+hnJudnp6fnqGhoqKlpqSioZ+hoqOkoqKjpaaopaepqampqKqssrOyrqmtqaiqq6mmpqmprauusK2traqqra+vsK2srbCtrq6tra6urKinp6enpaSkpKGlpaKenp+dnJ6ioZ+jo6ajpaanq6mopqWlpKenpautqaKipKCnp6emqaWgoaChoqWlpKGlp6alo6KioqKjpqempaWnqKeoqqyrrKimq6qoqqutq6usrK2rqqaqraywr66trKyusLGzsbOzsrOxtLOxsK2srq2sq6unpaenqayqqquop6eopqKhpKenpKakoqWqqqahoaGlpqWopqeoqKysqqimpaqrrKqrq6GgpaampqWjpKWko6GhoqGjoqSlp6qtq6mqqKeoqKuqqqinp6enqqmoq6qoqKyrrq2usbOwsbCxsLK0t7e3t7a3trS1trSzsrGwrKutq6urrKuqqKqtr7CwrauqqKanqaenpqirq6qrqKmnpqelpaWlp6emrK6tqaioqKmnp6urpaWnp6OlpaiopqSkpKOkpKOio6WnqquqqamoqaepqaipqKmoqKqqq6qoqKinqqqpqautq6qrq66vsbCws7Gztba3trazs7CwrqqpqamoqaqrrKyrq6ytsa2rqqmqqKWmo6OlpKeop6ipp6mopaWjpqinqKiqqaqnqamqqqqrra2rqqiop6epqaalqKmnqKmpqaioqampqaimqKqqqaqqrKmqqqmpqKinqKeopqmoqKurra6rqaqqqqqtr7CurbGzsLCvr66rq6urq62wsLCtq6qsrrCsrauqrK6vrauqqaioqqypp6ekpqqqqamopqanqaqsq62vr62qqquqqqipqamoqamop6inpaenqaioqamoqqqqp6epqKWlpaalpqanqKenp6aoqaenp6eoqKiqrK2tq6qqqqqrrK2srq6rra2sq6uqq6yrrK2tqqmnpqWmp6epqKiqqqqsq6urrK2sqaenp6ampqWmpqanp6anpaemp6mrqaqqqayssLCys7S0srGxsbCwsLCvr6+vr66trK2ur66wrq+vsLCwrq2urq+wr6+vr66traytra6trq2traysq6ytra6trq+vraurqqurraysra2urq6vr7CwsbGwsbGxsbGxsbCwsK+vrq6urrCwsLCvrq6vrq6ura6ura2tra2urq+wsLA=", +}; diff --git a/apps/site/src/scripts/aurora.shaders.ts b/apps/site/src/scripts/aurora.shaders.ts new file mode 100644 index 0000000..70cfccd --- /dev/null +++ b/apps/site/src/scripts/aurora.shaders.ts @@ -0,0 +1,364 @@ +// GLSL for the aurora worker. Units: kilometres and seconds. World frame: +// x east, y up, z north, viewer at the origin. +// +// The aurora follows Lawlor & Genetti: a thin emissive sheet whose ground +// track (the "footprint") is a 2D map, times a height profile. Each curtain's +// track is a curve z(x), so the footprint pass writes one texture row per +// curtain: its path, brightness along the arc and ray height. The sky pass +// marches altitudes from the lower border up and measures the distance to +// that path at each step. + +export const MAP_W = 2048; +export const MAP_H = 4; +export const CURTAINS = 2; + +export const COMMON = ` +precision highp float; +#define MAP_W ${MAP_W}.0 +#define MAP_H ${MAP_H}.0 +#define CURTAINS ${CURTAINS} +uniform float uT; +uniform vec3 uSpring, uCyan, uPurple; +// Ground shift per km of altitude along a field line (toward magnetic south). +uniform vec2 uTilt; +const float H0 = 100.0; // lower border, km +const float RE = 6371.0; // Earth radius, km +const float SIG = 8.0; // sheet half-thickness (gaussian sigma), km +float tt; +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p), f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), + mix(hash(i + vec2(0.0, 1.0)), hash(i + 1.0), f.x), f.y); +} +// Per site, per curtain: base distance north, then the x range drawn +// (centre, half-width), all km. +uniform vec3 uC0, uC1; +vec3 curtain(float k) { return k < 0.5 ? uC0 : uC1; } +vec2 span(float k) { return curtain(k).yz; } +// Ground distance north of the viewer: a base plus folds at three scales. +float path(float x, float k) { + float s = 1.0 + 0.6 * k; + float z = curtain(k).x; + z += 90.0 * s * (noise(vec2(x / 420.0 + k * 7.0, tt * 0.0012 + k)) - 0.5); + z += 35.0 * s * (noise(vec2(x / 140.0 + k * 3.0, tt * 0.003 + 2.0 * k)) - 0.5); + z += 10.0 * (noise(vec2(x / 45.0 + k * 11.0, tt * 0.008)) - 0.5); + return z; +} +// Long bright stretches and dark gaps; a faint arc is always left. +float lit(float x, float k) { + vec2 q = vec2(x / 260.0 - tt * 0.002, k * 9.0 + tt * 0.003); + float n = 0.62 * noise(q) + 0.31 * noise(q * 2.03 + 17.0); + return 0.08 + 0.92 * smoothstep(0.38, 0.62, n); +} +float knots(float x, float k) { + return 0.45 + 0.55 * noise(vec2(x / 55.0 + tt * 0.004, k * 5.0 + tt * 0.01)); +} +// How rayed (rather than smooth) this stretch of the arc is. +float rayed(float x, float k) { + return smoothstep(0.3, 0.7, noise(vec2(x / 180.0 + k * 4.0, tt * 0.003 + k))); +} +// Fine striations along the arc, drifting about 0.5 km/s. +float rays(float x, float k, float c) { + float r = 0.6 * noise(vec2(x / 3.5 + tt * 0.15, k * 13.0 + tt * 0.02)) + + 0.4 * noise(vec2(x / 1.6 - tt * 0.1, k * 17.0)); + return mix(1.0, r * 1.4, c); +} +float rayHeight(float x, float k) { + return rayed(x, k) * (0.3 + 0.7 * noise(vec2(x / 90.0 + k, tt * 0.002))); +} +// Colour by altitude: bright spring-cyan at the border, cyan to blue above, +// faint purple at the top. +vec3 ramp(float h) { + vec3 blue = mix(uCyan, uPurple, 0.5); + vec3 c = mix(mix(uSpring, uCyan, 0.55), uCyan, smoothstep(100.0, 125.0, h)); + c = mix(c, blue, smoothstep(125.0, 180.0, h)); + return mix(c, uPurple, smoothstep(180.0, 260.0, h)); +} +`; + +export const QUAD_VS = ` +attribute vec2 aPos; +varying vec2 vUv; +void main() { vUv = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); } +`; + +// Rows 2k: path z as ±200 km around the base (two bytes), sqrt of fine +// brightness, ray height. +// Rows 2k+1: sqrt of coarse brightness (no rays), for far-away pixels. +export const FOOT_FS = `${COMMON} +varying vec2 vUv; +void main() { + tt = uT; + float row = floor(vUv.y * MAP_H); + float k = floor(row / 2.0); + vec2 sp = span(k); + float x = sp.x + (vUv.x * 2.0 - 1.0) * sp.y; + float fade = smoothstep(0.0, 0.08 * sp.y, sp.y - abs(x - sp.x)); + float zn = clamp((path(x, k) - curtain(k).x + 200.0) / 400.0, 0.0, 1.0); + float coarse = lit(x, k) * knots(x, k) * fade; + if (row - 2.0 * k < 0.5) { + float fine = coarse * rays(x, k, rayed(x, k)); + gl_FragColor = vec4(floor(zn * 255.0) / 255.0, fract(zn * 255.0), + sqrt(min(fine / 1.5, 1.0)), rayHeight(x, k)); + } else { + gl_FragColor = vec4(sqrt(coarse), 0.0, 0.0, 1.0); + } +} +`; + +export const skyFs = (steps: number, top: number) => `${COMMON} +#define STEPS ${steps} +uniform sampler2D uMap, uMoonTex; +uniform vec3 uVoid, uTeal, uIce; +uniform mat3 uCam; +uniform vec2 uTan; +uniform float uPxAng; +uniform vec4 uMoon; // direction, angular radius +uniform vec3 uMoonN, uSun; +uniform float uMoonlight; +uniform float uGlow; // city light near the horizon, 0..1 +uniform float uLights; // lit windows and streets, 0..1 +varying vec2 vUv; +const float H1 = ${top}.0; +const float PI = 3.14159265; + +// Path of curtain k at x: (z, dz/dx, fine brightness, ray height). Two +// fetches and a manual lerp, since the two-byte z cannot be filtered. +vec4 fetchPath(float x, float k) { + vec2 sp = span(k); + float u = clamp((x - sp.x + sp.y) / (2.0 * sp.y), 0.0, 1.0) * MAP_W - 0.5; + float i = floor(u), f = u - i; + float v = (2.0 * k + 0.5) / MAP_H; + vec4 a = texture2D(uMap, vec2((i + 0.5) / MAP_W, v)); + vec4 b = texture2D(uMap, vec2((i + 1.5) / MAP_W, v)); + float z0 = curtain(k).x - 200.0; + float za = z0 + 400.0 * (a.r + a.g / 255.0); + float zb = z0 + 400.0 * (b.r + b.g / 255.0); + float bf = mix(a.b, b.b, f); + return vec4(mix(za, zb, f), (zb - za) * MAP_W / (2.0 * sp.y), 1.5 * bf * bf, + mix(a.a, b.a, f)); +} +float fetchCoarse(float x, float k) { + vec2 sp = span(k); + float c = texture2D(uMap, vec2((x - sp.x + sp.y) / (2.0 * sp.y), + (2.0 * k + 1.5) / MAP_H)).r; + return c * c; +} +// Sharp lower border, then an exponential tail whose height grows with g. +float profile(float h, float g) { + return smoothstep(94.0, 106.0, h) * exp(-max(h - 106.0, 0.0) / (25.0 + 45.0 * g)); +} +vec3 moon(vec3 rd) { + float cm = dot(rd, uMoon.xyz); + if (cm < cos(uMoon.w * 3.0) || uMoon.y < -0.02) return vec3(0.0); + vec3 o = (rd - uMoon.xyz * cm) / sin(uMoon.w); + float r = length(o); + float px = uPxAng / uMoon.w; + float cover = 1.0 - smoothstep(1.0 - px, 1.0 + px, r); + vec3 n = o - uMoon.xyz * sqrt(max(0.0, 1.0 - r * r)); + vec3 north = normalize(uMoonN - uMoon.xyz * dot(uMoonN, uMoon.xyz)); + vec3 west = cross(north, uMoon.xyz); + float lat = asin(clamp(dot(n, north), -1.0, 1.0)); + float lon = atan(dot(n, west), dot(n, -uMoon.xyz)); + float alb = texture2D(uMoonTex, vec2(0.5 + lon / (2.0 * PI), 0.5 - lat / PI)).r; + float day = max(dot(n, uSun), 0.0); + float halo = exp(-(1.0 - cm) / (uMoon.w * uMoon.w * 6.0)) * 0.06; + float full = 0.5 - 0.5 * dot(uSun, uMoon.xyz); + return uIce * (cover * alb * (2.2 * day + 0.03) + halo * full); +} +// Sky seen along rd (above the horizon): background, city glow, moonlight, +// the aurora and the moon. Before tone mapping. j is the step jitter. +vec3 skyColour(vec3 rd, float j) { + float c2 = dot(rd.xz, rd.xz); + vec3 acc = vec3(0.0); + for (int i = 0; i < STEPS; i++) { + float s = (float(i) + j) / float(STEPS); + float h = H0 - 6.0 + (H1 - H0 + 6.0) * s * s; + float dh = (H1 - H0 + 6.0) * (2.0 * s + 1.0 / float(STEPS)) / float(STEPS); + // Distance along the ray to altitude h over a curved Earth. + float t = min(2.0 * h / (rd.y + sqrt(rd.y * rd.y + 2.0 * c2 * h / RE)), 2500.0); + float dt = dh / max(rd.y + c2 * t / RE, 0.03); + vec2 g = rd.xz * t - uTilt * (h - H0); + vec2 dg = rd.xz * dt - uTilt * dh; + float pxKm = t * uPxAng; + float dens = 0.0, gh = 0.0; + for (int k = 0; k < CURTAINS; k++) { + float fk = float(k); + vec4 m = fetchPath(g.x, fk); + vec2 n = normalize(vec2(-m.y, 1.0)); + float d = (g.y - m.x) * n.y; + // Widen the sheet by this step's travel across it, keeping its integral. + float dn = abs(dot(dg, n)) + pxKm; + float s2 = SIG * SIG + dn * dn / 12.0; + float bc = fetchCoarse(g.x, fk); + float b = mix(m.z, bc, smoothstep(1.5, 4.0, pxKm / 3.0)); + dens += b * exp(-0.5 * d * d / s2) * SIG * inversesqrt(s2) + + bc * 0.1 * exp(-0.5 * d * d / (16.0 * SIG * SIG)); + gh = max(gh, m.w); + } + acc += ramp(h) * dens * profile(h, gh) * dt; + } + float e = max(rd.y, 0.0); + float air = exp(-0.12 * (1.0 / (e + 0.04) - 1.0)); + vec3 col = uVoid + uTeal * 0.05 * exp(-e * 8.0) + + uGlow * (uTeal * 0.09 * exp(-e * 9.0) + uIce * 0.035 * exp(-e * 22.0)) + + uMoonlight * (uIce * 0.02 + uTeal * 0.05) * (0.5 + 0.5 * exp(-e * 3.0)); + return col + (acc * 0.035 + moon(rd)) * air; +} +// City lights along the shore, by azimuth bin: on or off, and how bright. +float shoreLight(float az) { + float cell = floor(az * 950.0); + float h = hash(vec2(cell, 3.7)); + float f = fract(az * 950.0) - 0.5; + return step(1.0 - 0.16 * uLights, h) * fract(h * 41.3) * max(0.0, 1.0 - abs(f) * 2.4); +} +void main() { + vec2 ndc = vUv * 2.0 - 1.0; + vec3 rd = normalize(uCam * vec3(ndc * uTan, 1.0)); + // Static per-pixel jitter: breaks banding without shimmer. + float j = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); + vec3 col; + if (rd.y >= 0.0) { + col = skyColour(rd, j); + } else { + // Water: the sky mirrored and broken by slow ripples that crowd toward + // the horizon; strongest at grazing angles (Fresnel). + float d = -rd.y; + float az = atan(rd.x, rd.z); + vec2 q = vec2(az * 700.0, 0.06 / d); + float w = noise(q + vec2(0.0, uT * 0.35)) + 0.5 * noise(q * 2.3 - vec2(0.0, uT * 0.25)) - 0.75; + vec3 rr = normalize(vec3(rd.x, d * max(0.15, 1.0 + 0.9 * w), rd.z)); + float fresnel = 0.02 + 0.98 * pow(1.0 - d, 5.0); + col = uVoid * 0.5 + skyColour(rr, j) * fresnel * 0.85; + // City lights stretched into broken vertical streaks. + float streak = shoreLight(az + w * 0.0004) * exp(-d * 45.0) + * smoothstep(0.35, 0.75, noise(vec2(az * 900.0, d * 260.0 - uT * 0.6))); + col += uIce * streak * 0.35; + } + col = 1.0 - exp(-col * 1.7); + col += (hash(gl_FragCoord.xy) - 0.5) / 255.0; + gl_FragColor = vec4(col, 1.0); +} +`; + +const PROJECT = ` +uniform mat3 uCam; +uniform vec2 uTan; +// Camera space is (right, up, forward); clip w is the forward distance, so +// anything behind the viewer is clipped. +vec4 project(vec3 p) { + vec3 c = p * uCam; + return vec4(c.xy / uTan, 0.0, c.z); +} +`; + +// Electrons spiralling down field lines and flaring where they reach the +// lower border. Each lands at the curtain's shape at its landing time, so it +// stays put while the curtain drifts on. +export const SPARK_VS = `${COMMON}${PROJECT} +attribute vec4 aSeed; +uniform float uPx; +varying vec3 vCol; +void main() { + float k = aSeed.z < 0.6 ? 0.0 : 1.0; + float life = 10.0 + 10.0 * fract(aSeed.z * 7.13); + float ph = fract(uT / life + aSeed.y); + vec2 sp = span(k); + float x0 = sp.x + (aSeed.x * 2.0 - 1.0) * sp.y * 0.9; + float lift = 1.0 + 12.0 * pow(fract(aSeed.w * 13.7), 2.0); + tt = uT + (0.4 - ph) * life; + float z0 = path(x0, k) + (fract(aSeed.w * 5.3) - 0.5) * 2.0 * SIG; + float bright = lit(x0, k) * knots(x0, k); + tt = uT; + float hit = H0 + lift; + float h = mix(420.0, hit, smoothstep(0.0, 0.4, ph)); + float spin = uT * (2.0 + 3.0 * aSeed.w) + aSeed.y * 40.0; + vec2 g = vec2(x0, z0) + uTilt * (h - H0) + + 0.03 * (h - hit) * vec2(cos(spin), sin(spin)); + vec4 c = project(vec3(g.x, h - dot(g, g) / (2.0 * RE), g.y)); + float glow = smoothstep(0.45, 0.65, ph) * (1.0 - smoothstep(0.75, 1.0, ph)); + vCol = mix(uCyan, ramp(hit), glow) * 0.3 * glow * bright; + gl_PointSize = 1.3 * clamp(250.0 / max(c.w, 1.0), 0.6, 2.0) * uPx; + gl_Position = c; +} +`; + +// Catalogue stars: J2000 directions turned into the sky of date. Fainter +// near the horizon (extinction) and when the moon brightens the sky. They +// twinkle; they never move except with the sky itself. +export const STAR_VS = ` +precision highp float; +${PROJECT} +attribute vec3 aDir; +attribute vec2 aMag; +uniform mat3 uStars; +uniform vec3 uIce, uCyan; +uniform float uPx, uT, uLimit, uFade; +varying vec3 vCol; +void main() { + vec3 w = uStars * aDir; + float alt = w.y; + float m = aMag.x + 0.12 * (1.0 / (max(alt, 0.0) + 0.04) - 1.0); + float b = pow(2.512, -m); + float a = min(2.6 * sqrt(b), 1.6) * smoothstep(uLimit + 0.3, uLimit - 0.6, m) * uFade; + float seed = fract(sin(dot(aDir.xy, vec2(91.7, 37.3)) + aDir.z * 11.1) * 4375.85); + float amp = 0.1 + 0.3 * (1.0 - smoothstep(0.05, 0.5, alt)); + float tw = 1.0 + amp * sin(uT * (1.1 + 2.0 * seed) + seed * 60.0) + * sin(uT * 0.37 + seed * 17.0); + vCol = mix(uIce, uCyan, 0.35 * smoothstep(0.4, -0.1, aMag.y)) * a * tw; + gl_PointSize = uPx * (1.6 + 1.4 * min(sqrt(b), 1.2)); + gl_Position = alt > 0.0 ? project(w) : vec4(0.0, 0.0, 2.0, 1.0); +} +`; + +export const POINT_FS = ` +precision mediump float; +varying vec3 vCol; +void main() { + vec2 q = gl_PointCoord * 2.0 - 1.0; + float m = max(0.0, 1.0 - dot(q, q)); + gl_FragColor = vec4(vCol * m * m, 1.0); +} +`; + +// The skyline as a strip of directions from the ridge down below the frame, +// drawn last so it hides sky, stars and sparks. +export const RIDGE_VS = ` +precision highp float; +${PROJECT} +attribute vec4 aRidge; // direction, haze 0..1 (negative on the bottom edge) +varying float vHaze, vDown; +varying vec3 vDir; +void main() { + vDown = step(aRidge.w, -0.5); + vHaze = mix(aRidge.w, -1.0 - aRidge.w, vDown); + vDir = aRidge.xyz; + gl_Position = project(aRidge.xyz); +} +`; + +export const RIDGE_FS = ` +precision highp float; +uniform vec3 uVoid, uTeal, uIce; +uniform float uMoonlight, uLights; +varying float vHaze, vDown; +varying vec3 vDir; +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +void main() { + // Distant ridges are paler at the top, fading within a degree or so. + float rim = vHaze * exp(-vDown * 60.0); + vec3 col = uVoid * 0.45 + (uTeal * 0.035 + uIce * 0.006) * rim + + uIce * 0.02 * uMoonlight; + // A scatter of lit windows and street lights on a city skyline. + vec3 d = normalize(vDir); + vec2 g = vec2(atan(d.x, d.z) * 950.0, asin(d.y) * 1100.0); + float h = hash(floor(g)); + vec2 f = fract(g) - 0.5; + // Only past the first kilometre: the viewer's own surroundings stay dark. + float on = step(1.0 - 0.13 * uLights, h) * step(0.02, vDown) * step(0.003, vHaze); + col += uIce * on * exp(-dot(f, f) * 14.0) * (0.25 + 0.5 * fract(h * 37.0)); + gl_FragColor = vec4(1.0 - exp(-col * 1.7), 1.0); +} +`; diff --git a/apps/site/src/scripts/aurora.sites.ts b/apps/site/src/scripts/aurora.sites.ts new file mode 100644 index 0000000..e807317 --- /dev/null +++ b/apps/site/src/scripts/aurora.sites.ts @@ -0,0 +1,150 @@ +// Places the sky can be seen from. The worker picks one per visit from the +// browser's time zone (or at random) and the visitor can switch in the info +// popover; the popover shows the same site's details. Plain data only: the page's own +// script and scripts/build-skyline.mjs both import this. + +/** One aurora curtain: its ground distance north and the x range it spans. */ +export type Curtain = { + /** Typical distance north of the viewer, km. */ + base: number; + /** Centre and half-width of the east–west range drawn, km. */ + centre: number; + half: number; +}; + +export type Site = { + id: string; + /** Short place name, then the country. */ + name: string; + country: string; + /** Where the viewer stands, in a few words. */ + spot: string; + lat: number; + lon: number; + /** Eye height above the ground (above the sea for a site over water), metres. */ + eye: number; + /** Camera heading from north toward east, degrees. */ + heading: number; + /** Camera pitch and vertical field of view, degrees. The frame's bottom + * edge sits at pitch − fov / 2, just below the horizon. */ + pitch: number; + fov: number; + /** Magnetic dip and declination, degrees (approximate, for ray tilt). */ + dip: number; + decl: number; + /** The near and far curtain. Farther south, the aurora sits farther north. */ + curtains: [Curtain, Curtain]; + /** City light on the sky near the horizon, 0 (none) to 1 (a capital). */ + glow: number; + /** Faintest star magnitude visible on a moonless night. */ + limit: number; + /** What stands between the viewer and the sky, for the popover text. */ + ground: string; + /** Water in front reflects the sky (the skyline script finds the shore). */ + water: boolean; + /** Add OpenStreetMap building heights to the skyline. */ + buildings: boolean; + /** Lit windows and streets on the skyline, 0 (none) to 1 (a capital). */ + lights: number; + /** Credits for the data behind this site's skyline. */ + credits: [string, string][]; + /** Browser time zones that start a visit at this site. */ + zones: string[]; +}; + +const COPERNICUS: [string, string] = [ + "Terrain", + "Copernicus DEM GLO-30 © DLR e.V. 2010–2014 and © Airbus Defence and Space GmbH 2014–2018, provided under COPERNICUS by the European Union and ESA.", +]; +const OSM: [string, string] = [ + "Buildings", + "© OpenStreetMap contributors (ODbL).", +]; + +export const SITES: Site[] = [ + { + id: "isafjordur", + name: "Ísafjörður", + country: "Iceland", + spot: "Sólgata, by the church", + lat: 66.075075, + lon: -23.124175, + eye: 2, + heading: 20, + pitch: 32, + fov: 70, + dip: 77, + decl: -11, + curtains: [ + { base: 170, centre: 250, half: 650 }, + { base: 420, centre: 500, half: 1400 }, + ], + glow: 0.1, + limit: 5.5, + ground: "mountains", + water: false, + buildings: false, + lights: 0, + credits: [COPERNICUS], + zones: ["Atlantic/Reykjavik"], + }, + { + id: "helsinki", + name: "Helsinki", + country: "Finland", + spot: "the main pier on Suomenlinna, across the water to the city", + lat: 60.1493, + lon: 24.9848, + eye: 3, + heading: -25, + pitch: 25, + fov: 60, + dip: 74, + decl: 11, + curtains: [ + { base: 550, centre: -250, half: 1000 }, + { base: 900, centre: -400, half: 1600 }, + ], + glow: 1, + limit: 4.2, + ground: "skyline", + water: true, + buildings: true, + lights: 1, + credits: [COPERNICUS, OSM], + zones: ["Europe/Helsinki", "Europe/Mariehamn"], + }, +]; + +/** Credits that hold for every site. */ +export const SKY_CREDITS: [string, string][] = [ + ["Stars", "Yale Bright Star Catalogue, 5th revised edition (CDS V/50)."], + ["Moon", "NASA's Scientific Visualization Studio."], +]; + +const POINTS = [ + "north", + "north-northeast", + "northeast", + "east-northeast", + "east", + "east-southeast", + "southeast", + "south-southeast", + "south", + "south-southwest", + "southwest", + "west-southwest", + "west", + "west-northwest", + "northwest", + "north-northwest", +]; + +/** Heading in degrees as a 16-point compass word, e.g. "north-northeast". */ +export const compass = (deg: number) => + POINTS[Math.round((((deg % 360) + 360) % 360) / 22.5) % 16] ?? "north"; + +/** Latitude and longitude as "66.0751° N, 23.1242° W". */ +export const coords = (lat: number, lon: number) => + `${Math.abs(lat).toFixed(4)}° ${lat < 0 ? "S" : "N"}, ${Math.abs(lon).toFixed(4)}° ${lon < 0 ? "W" : "E"}`; diff --git a/apps/site/src/scripts/aurora.sky.ts b/apps/site/src/scripts/aurora.sky.ts new file mode 100644 index 0000000..17015e8 --- /dev/null +++ b/apps/site/src/scripts/aurora.sky.ts @@ -0,0 +1,244 @@ +// What the real sky looks like from a site right now: sidereal time, +// precession, and the sun and moon. Vectors use the scene's world frame: +// x east, y up, z north. Angles in radians unless named *Deg. +// Moon and sun follow Meeus, "Astronomical Algorithms" (2nd ed.), ch. 25 and +// 47, with the larger periodic terms only (moon good to about 0.05°). + +import type { Site } from "./aurora.sites"; + +export type Mat3 = [ + number, + number, + number, + number, + number, + number, + number, + number, + number, +]; +export type Vec3 = [number, number, number]; + +const RAD = Math.PI / 180; +const ARCSEC = RAD / 3600; + +const julian = (ms: number) => ms / 86400000 + 2440587.5; + +/** Row-major 3×3 product a·b. */ +function mul(a: Mat3, b: Mat3): Mat3 { + return Array.from({ length: 9 }, (_, k) => { + const i = Math.floor(k / 3) * 3; + const j = k % 3; + return a[i]! * b[j]! + a[i + 1]! * b[3 + j]! + a[i + 2]! * b[6 + j]!; + }) as Mat3; +} + +function apply(m: Mat3, v: Vec3): Vec3 { + return [ + m[0] * v[0] + m[1] * v[1] + m[2] * v[2], + m[3] * v[0] + m[4] * v[1] + m[5] * v[2], + m[6] * v[0] + m[7] * v[1] + m[8] * v[2], + ]; +} + +/** Column-major copy for gl.uniformMatrix3fv. */ +export const columns = (m: Mat3) => + new Float32Array([m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]]); + +/** J2000 equatorial to mean equator of date (IAU 1976 precession). */ +function precession(t: number): Mat3 { + const zeta = (2306.2181 * t + 0.30188 * t * t + 0.017998 * t ** 3) * ARCSEC; + const z = (2306.2181 * t + 1.09468 * t * t + 0.018203 * t ** 3) * ARCSEC; + const th = (2004.3109 * t - 0.42665 * t * t - 0.041833 * t ** 3) * ARCSEC; + const [cz, sz] = [Math.cos(zeta), Math.sin(zeta)]; + const [cZ, sZ] = [Math.cos(z), Math.sin(z)]; + const [ct, st] = [Math.cos(th), Math.sin(th)]; + return [ + cz * cZ * ct - sz * sZ, + -sz * cZ * ct - cz * sZ, + -cZ * st, + cz * sZ * ct + sz * cZ, + -sz * sZ * ct + cz * cZ, + -sZ * st, + cz * st, + -sz * st, + ct, + ]; +} + +/** Equatorial of date to the world frame, for local sidereal time lst. */ +function horizon(lat: number, lst: number): Mat3 { + const [cl, sl] = [Math.cos(lst), Math.sin(lst)]; + const [cp, sp] = [Math.cos(lat), Math.sin(lat)]; + return [-sl, cl, 0, cp * cl, cp * sl, sp, -sp * cl, -sp * sl, cp]; +} + +const ecliptic = (lon: number, lat: number, eps: number): Vec3 => { + const [cb, sb] = [Math.cos(lat), Math.sin(lat)]; + const [cl, sl] = [Math.cos(lon), Math.sin(lon)]; + const [ce, se] = [Math.cos(eps), Math.sin(eps)]; + return [cb * cl, cb * sl * ce - sb * se, cb * sl * se + sb * ce]; +}; + +// Meeus table 47.A: D, M, M', F, longitude (1e-6°), distance (1e-3 km). +const MOON_LR: number[][] = [ + [0, 0, 1, 0, 6288774, -20905355], + [2, 0, -1, 0, 1274027, -3699111], + [2, 0, 0, 0, 658314, -2955968], + [0, 0, 2, 0, 213618, -569925], + [0, 1, 0, 0, -185116, 48888], + [0, 0, 0, 2, -114332, -3149], + [2, 0, -2, 0, 58793, 246158], + [2, -1, -1, 0, 57066, -152138], + [2, 0, 1, 0, 53322, -170733], + [2, -1, 0, 0, 45758, -204586], + [0, 1, -1, 0, -40923, -129620], + [1, 0, 0, 0, -34720, 108743], + [0, 1, 1, 0, -30383, 104755], + [2, 0, 0, -2, 15327, 10321], + [0, 0, 1, 2, -12528, 0], + [0, 0, 1, -2, 10980, 79661], + [4, 0, -1, 0, 10675, -34782], + [0, 0, 3, 0, 10034, -23210], + [4, 0, -2, 0, 8548, -21636], + [2, 1, -1, 0, -7888, 24208], + [2, 1, 0, 0, -6766, 30824], + [1, 0, -1, 0, -5163, -8379], + [1, 1, 0, 0, 4987, -16675], + [2, -1, 1, 0, 4036, -12831], + [2, 0, 2, 0, 3994, -10445], + [4, 0, 0, 0, 3861, -11650], + [2, 0, -3, 0, 3665, 14403], + [0, 1, -2, 0, -2689, -7003], + [2, 0, -1, 2, -2602, 0], + [2, -1, -2, 0, 2390, 10056], + [1, 0, 1, 0, -2348, 6322], + [2, -2, 0, 0, 2236, -9884], +]; +// Meeus table 47.B: D, M, M', F, latitude (1e-6°). +const MOON_B: number[][] = [ + [0, 0, 0, 1, 5128122], + [0, 0, 1, 1, 280602], + [0, 0, 1, -1, 277693], + [2, 0, 0, -1, 173237], + [2, 0, -1, 1, 55413], + [2, 0, -1, -1, 46271], + [2, 0, 0, 1, 32573], + [0, 0, 2, 1, 17198], + [2, 0, 1, -1, 9266], + [0, 0, 2, -1, 8822], + [2, -1, 0, -1, 8216], + [2, 0, -2, -1, 4324], + [2, 0, 1, 1, 4200], + [2, 1, 0, -1, -3359], + [2, -1, -1, 1, 2463], + [2, -1, 0, 1, 2211], + [2, -1, -1, -1, 2065], +]; + +/** Geocentric ecliptic longitude, latitude (radians) and distance (km). */ +export function moonEcliptic(t: number): [number, number, number] { + const Lp = 218.3164477 + 481267.88123421 * t; + const D = (297.8501921 + 445267.1114034 * t) * RAD; + const M = (357.5291092 + 35999.0502909 * t) * RAD; + const Mp = (134.9633964 + 477198.8675055 * t) * RAD; + const F = (93.272095 + 483202.0175233 * t) * RAD; + const A1 = (119.75 + 131.849 * t) * RAD; + const A2 = (53.09 + 479264.29 * t) * RAD; + const A3 = (313.45 + 481266.484 * t) * RAD; + const E = 1 - 0.002516 * t; + let sl = 0; + let sr = 0; + let sb = 0; + for (const [d, m, mp, f, l, r] of MOON_LR) { + const arg = d! * D + m! * M + mp! * Mp + f! * F; + const e = E ** Math.abs(m!); + sl += l! * e * Math.sin(arg); + sr += r! * e * Math.cos(arg); + } + for (const [d, m, mp, f, b] of MOON_B) { + sb += + b! * E ** Math.abs(m!) * Math.sin(d! * D + m! * M + mp! * Mp + f! * F); + } + const L = Lp * RAD; + sl += 3958 * Math.sin(A1) + 1962 * Math.sin(L - F) + 318 * Math.sin(A2); + sb += + -2235 * Math.sin(L) + + 382 * Math.sin(A3) + + 175 * Math.sin(A1 - F) + + 175 * Math.sin(A1 + F) + + 127 * Math.sin(L - Mp) - + 115 * Math.sin(L + Mp); + return [(Lp + sl / 1e6) * RAD, (sb / 1e6) * RAD, 385000.56 + sr / 1000]; +} + +/** Geocentric ecliptic longitude of the sun (radians). */ +function sunLongitude(t: number): number { + const L0 = 280.46646 + 36000.76983 * t; + const M = (357.52911 + 35999.05029 * t) * RAD; + const C = + (1.914602 - 0.004817 * t) * Math.sin(M) + + (0.019993 - 0.000101 * t) * Math.sin(2 * M) + + 0.000289 * Math.sin(3 * M); + return (L0 + C) * RAD; +} + +const normalize = (v: Vec3): Vec3 => { + const l = Math.hypot(v[0], v[1], v[2]); + return [v[0] / l, v[1] / l, v[2] / l]; +}; + +export type Sky = { + /** J2000 equatorial to world, for the star catalogue. */ + stars: Mat3; + /** Unit vector to the moon (topocentric) and its angular radius. */ + moon: Vec3; + moonRadius: number; + /** Lunar north (ecliptic pole is within 1.5° of it). */ + moonNorth: Vec3; + sun: Vec3; + /** 0..1: how much the moon lights the sky (phase and altitude). */ + moonlight: number; +}; + +export function sky(site: Site, ms: number): Sky { + const jd = julian(ms); + const t = (jd - 2451545) / 36525; + const gmst = + (280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * t * t) * + RAD; + const lst = gmst + site.lon * RAD; + const lat = site.lat * RAD; + const hor = horizon(lat, lst); + const eps = (23.4392911 - 0.0130042 * t) * RAD; + + // Topocentric moon: subtract the observer's position from the geocentric one. + const [ml, mb, dist] = moonEcliptic(t); + const geo = ecliptic(ml, mb, eps).map((c) => c * dist) as Vec3; + const u = Math.atan(0.99664719 * Math.tan(lat)); + const rc = 6378.14 * Math.cos(u); + const rs = 6378.14 * 0.99664719 * Math.sin(u); + const topo: Vec3 = [ + geo[0] - rc * Math.cos(lst), + geo[1] - rc * Math.sin(lst), + geo[2] - rs, + ]; + const range = Math.hypot(...topo); + const moon = normalize(apply(hor, topo)); + const sun = normalize(apply(hor, ecliptic(sunLongitude(t), 0, eps))); + const moonNorth = normalize(apply(hor, [0, -Math.sin(eps), Math.cos(eps)])); + + // Illuminated fraction from the sun–moon angle, faded by moon altitude. + const lit = + (1 - (sun[0] * moon[0] + sun[1] * moon[1] + sun[2] * moon[2])) / 2; + const alt = Math.asin(moon[1]) / RAD; + const up = Math.min(1, Math.max(0, (alt + 2) / 17)); + return { + stars: mul(hor, precession(t)), + moon, + moonRadius: Math.asin(1737.4 / range), + moonNorth, + sun, + moonlight: lit ** 1.5 * up * up * (3 - 2 * up), + }; +} diff --git a/apps/site/src/scripts/aurora.worker.ts b/apps/site/src/scripts/aurora.worker.ts new file mode 100644 index 0000000..c28ee98 --- /dev/null +++ b/apps/site/src/scripts/aurora.worker.ts @@ -0,0 +1,567 @@ +// Aurora background: the real night sky over Ísafjörður, in a Web Worker on +// an OffscreenCanvas so the page's main thread does no WebGL work at all. +// Passes per frame: +// 1. Footprint: each curtain's ground track and brightness into a small +// texture (see aurora.shaders.ts). +// 2. Sky: a fullscreen march through the aurora in perspective, plus the moon. +// 3. Stars from the Bright Star Catalogue, then the sparks, added on top. +// 4. The mountain skyline, drawn last so it hides everything behind it. +// The page sends the theme token values, sizes and visibility; we answer "on" +// once a frame is up and "off" if we give up. + +import { + FOOT_FS, + MAP_H, + MAP_W, + POINT_FS, + QUAD_VS, + RIDGE_FS, + RIDGE_VS, + SPARK_VS, + STAR_VS, + skyFs, +} from "./aurora.shaders"; +import { getSystemTimeZone, getUnixNow } from "@northguild/gmt"; +import { MOON } from "./aurora.moon"; +import { SITES } from "./aurora.sites"; +import { columns, sky, type Sky } from "./aurora.sky"; +import { SKYLINE as HELSINKI } from "./skylines/helsinki"; +import { SKYLINE as ISAFJORDUR } from "./skylines/isafjordur"; + +const FRAME_MS = 1000 / 30; +const BASE_SCALE = 0.6; +const MAX_EDGE = 1280; +const SPARKS = 4000; +const RAD = Math.PI / 180; + +// Wall-clock epoch milliseconds from Temporal (via gmt): the stars and moon +// need the real time. Frame timing uses rAF stamps, not the wall clock. +let wallMs = 0; +const wall = () => (wallMs = getUnixNow() ?? wallMs); + +type Rgb = [number, number, number]; +type Skyline = typeof ISAFJORDUR; + +// Skylines by site id (see scripts/build-skyline.mjs). +const SKYLINES: Record = { + isafjordur: ISAFJORDUR, + helsinki: HELSINKI, +}; + +export type AuroraMessage = + | { + type: "init"; + canvas: OffscreenCanvas; + width: number; + height: number; + colours: string[]; + /** Index into SITES, or -1 to pick from the browser's time zone. */ + site: number; + still: boolean; + hidden: boolean; + } + | { type: "site"; site: number } + | { type: "size"; width: number; height: number } + | { type: "still"; still: boolean } + | { type: "hidden"; hidden: boolean }; + +/** To the page: the frame is up ("on"), we gave up ("off"), or the site drawn. */ +export type WorkerMessage = "on" | "off" | { site: number }; + +const scope = self as unknown as { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(message: WorkerMessage): void; +}; + +/** The site for this browser's time zone (via gmt), else one at random. */ +function siteForTimeZone(): number { + const zone = getSystemTimeZone(); + const match = SITES.findIndex((s) => s.zones.includes(zone)); + return match >= 0 ? match : Math.floor(Math.random() * SITES.length); +} + +const nextFrame = + "requestAnimationFrame" in self + ? requestAnimationFrame + : (cb: FrameRequestCallback) => + setTimeout(() => cb(performance.now()), FRAME_MS); +const cancelFrame = + "cancelAnimationFrame" in self ? cancelAnimationFrame : clearTimeout; + +function parseColours(values: string[]): Rgb[] { + const ctx = new OffscreenCanvas(1, 1).getContext("2d", { + willReadFrequently: true, + }); + return values.map((value) => { + if (!ctx) return [0, 0, 0]; + ctx.clearRect(0, 0, 1, 1); + ctx.fillStyle = value; + ctx.fillRect(0, 0, 1, 1); + const [r = 0, g = 0, b = 0] = ctx.getImageData(0, 0, 1, 1).data; + return [r / 255, g / 255, b / 255]; + }); +} + +function compile( + gl: WebGLRenderingContext, + vs: string, + fs: string, +): WebGLProgram | null { + const program = gl.createProgram(); + for (const [type, src] of [ + [gl.VERTEX_SHADER, vs], + [gl.FRAGMENT_SHADER, fs], + ] as const) { + const shader = gl.createShader(type); + if (!shader) return null; + gl.shaderSource(shader, src); + gl.compileShader(shader); + gl.attachShader(program, shader); + } + gl.linkProgram(program); + if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program; + if (import.meta.env.DEV) { + for (const shader of gl.getAttachedShaders(program) ?? []) { + console.warn("aurora:", gl.getShaderInfoLog(shader)); + } + } + return null; +} + +const bytes = (b64: string) => + Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); + +/** Ridge strip: for each azimuth, the ridge direction, then the far shore + * over water or a direction far below the frame. */ +function ridgeMesh(skyline: Skyline): Float32Array { + const elev = new Int16Array(bytes(skyline.elev).buffer); + const dist = new Uint16Array(bytes(skyline.dist).buffer); + const base = new Int16Array(bytes(skyline.base).buffer); + const out = new Float32Array(elev.length * 8); + elev.forEach((e, i) => { + const az = (skyline.from + i * skyline.step) * RAD; + // 0 within 1 km of the viewer, rising to 1 at 31 km: paler, and lit if a city. + const haze = Math.min(1, Math.max(0, ((dist[i] ?? 0) / 100 - 1) / 30)); + for (const [j, el] of [ + [0, (e / 100) * RAD], + [1, ((base[i] ?? -4000) / 100) * RAD], + ] as const) { + out.set( + [ + Math.sin(az) * Math.cos(el), + Math.sin(el), + Math.cos(az) * Math.cos(el), + j ? -1 - haze : haze, + ], + i * 8 + j * 4, + ); + } + }); + return out; +} + +/** Catalogue records (see scripts/build-stars.mjs) to direction + magnitude. */ +function starData(buf: ArrayBuffer): Float32Array { + const view = new DataView(buf); + const n = Math.floor(buf.byteLength / 6); + const out = new Float32Array(n * 5); + for (let i = 0; i < n; i++) { + const ra = (view.getUint16(i * 6, true) / 65536) * 2 * Math.PI; + const dec = (view.getInt16(i * 6 + 2, true) / 32767) * (Math.PI / 2); + out.set( + [ + Math.cos(dec) * Math.cos(ra), + Math.cos(dec) * Math.sin(ra), + Math.sin(dec), + view.getUint8(i * 6 + 4) / 20 - 2, + view.getInt8(i * 6 + 5) / 50, + ], + i * 5, + ); + } + return out; +} + +function start(init: Extract): void { + const { canvas } = init; + const gl = canvas.getContext("webgl", { + alpha: false, + antialias: false, + depth: false, + stencil: false, + powerPreference: "low-power", + }); + if (!gl || gl.getParameter(gl.MAX_TEXTURE_SIZE) < MAP_W) return; + const foot = compile(gl, QUAD_VS, FOOT_FS); + let skyProgram = compile(gl, QUAD_VS, skyFs(20, 320)); + const sparks = compile(gl, SPARK_VS, POINT_FS); + const stars = compile(gl, STAR_VS, POINT_FS); + const ridge = compile(gl, RIDGE_VS, RIDGE_FS); + if (!foot || !skyProgram || !sparks || !stars || !ridge) return; + + const [voidC, spring, cyan, teal, purple, ice] = parseColours(init.colours); + const colours: Record = { + uVoid: voidC, + uSpring: spring, + uCyan: cyan, + uTeal: teal, + uPurple: purple, + uIce: ice, + }; + // Footprint texture and framebuffer on unit 0. + const map = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, map); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + MAP_W, + MAP_H, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null, + ); + const params = (filter: number, wrapS: number) => { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + }; + params(gl.NEAREST, gl.CLAMP_TO_EDGE); + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + map, + 0, + ); + const complete = + gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE; + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + if (!complete) return; + + // Moon albedo on unit 1 (power-of-two, so it may repeat in longitude). + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, gl.createTexture()); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.LUMINANCE, + MOON.width, + MOON.height, + 0, + gl.LUMINANCE, + gl.UNSIGNED_BYTE, + bytes(MOON.data), + ); + params(gl.LINEAR, gl.REPEAT); + gl.activeTexture(gl.TEXTURE0); + + const buffer = (data: Float32Array) => { + const b = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, b); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); + return b; + }; + const tri = buffer(new Float32Array([-1, -1, 3, -1, -1, 3])); + const seeds = buffer(Float32Array.from({ length: SPARKS * 4 }, Math.random)); + const ridgeBuf = gl.createBuffer(); + let ridgeCount = 0; + let starBuf: WebGLBuffer | null = null; + let starCount = 0; + let starsAt = 0; + + // Uniforms that never change: colours and samplers. + const setup = (program: WebGLProgram) => { + gl.useProgram(program); + for (const [name, rgb] of Object.entries(colours)) { + const loc = gl.getUniformLocation(program, name); + if (loc && rgb) gl.uniform3fv(loc, rgb); + } + gl.uniform1i(gl.getUniformLocation(program, "uMap"), 0); + gl.uniform1i(gl.getUniformLocation(program, "uMoonTex"), 1); + }; + for (const p of [foot, skyProgram, sparks, stars, ridge]) setup(p); + const at = (program: WebGLProgram, name: string) => + gl.getUniformLocation(program, name); + + const attrib = ( + program: WebGLProgram, + name: string, + buf: WebGLBuffer | null, + size: number, + stride = 0, + offset = 0, + ) => { + const a = gl.getAttribLocation(program, name); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.enableVertexAttribArray(a); + gl.vertexAttribPointer(a, size, gl.FLOAT, false, stride, offset); + return a; + }; + + // Everything that depends on where the viewer stands. + let site = SITES[0]!; + let tilt = [0, 0]; + let cam = new Float32Array(9); + let tanY = 1; + // Camera basis as columns (right, up, forward), heading from north. On a + // portrait screen the page's cards cover the middle, so the view widens and + // tilts until the horizon sits about a third of the way down: the aurora then + // shows above the cards and the land or water below them. + const aim = () => { + const aspect = (width || 1) / (height || 1); + const portrait = Math.min(1, Math.max(0, (1 - aspect) / 0.45)); + const fov = (site.fov + 15 * portrait) * RAD; + tanY = Math.tan(fov / 2); + const landscape = + -Math.tan(site.pitch * RAD) / Math.tan((site.fov / 2) * RAD); + const horizon = landscape + (0.3 - landscape) * portrait; + const pitch = Math.atan(-horizon * tanY); + const yaw = site.heading * RAD; + const fwd = [ + Math.sin(yaw) * Math.cos(pitch), + Math.sin(pitch), + Math.cos(yaw) * Math.cos(pitch), + ] as const; + const right = [Math.cos(yaw), 0, -Math.sin(yaw)] as const; + cam = new Float32Array([ + ...right, + fwd[1] * right[2] - fwd[2] * right[1], + fwd[2] * right[0] - fwd[0] * right[2], + fwd[0] * right[1] - fwd[1] * right[0], + ...fwd, + ]); + }; + const place = (index: number) => { + site = SITES[index] ?? SITES[0]!; + // Field lines climb toward magnetic south at the site's dip. + const south = (180 + site.decl) * RAD; + const lean = 1 / Math.tan(site.dip * RAD); + tilt = [lean * Math.sin(south), lean * Math.cos(south)]; + aim(); + const skyline = SKYLINES[site.id]; + const mesh = skyline ? ridgeMesh(skyline) : new Float32Array(0); + gl.bindBuffer(gl.ARRAY_BUFFER, ridgeBuf); + gl.bufferData(gl.ARRAY_BUFFER, mesh, gl.STATIC_DRAW); + ridgeCount = mesh.length / 4; + }; + let width = init.width; + let height = init.height; + place(init.site < 0 ? siteForTimeZone() : init.site); + scope.postMessage({ site: SITES.indexOf(site) }); + + // Quality levels: 0 full, 1 lower resolution and fewer sky steps, 2 also + // half the sparks, 3 stopped. + let level = 0; + let sparkCount = SPARKS; + let still = init.still; + let hidden = init.hidden; + let t = 120; + let raf = 0; + let last = 0; + let samples = 0; + let total = 0; + let tuning = true; + let skyAt = wall(); + let now: Sky = sky(site, skyAt); + + const resize = () => { + const w = width || 1; + const h = height || 1; + const scale = + Math.min(BASE_SCALE, MAX_EDGE / Math.max(w, h)) * (level ? 0.7 : 1); + canvas.width = Math.round(w * scale); + canvas.height = Math.round(h * scale); + aim(); + }; + + const quad = (program: WebGLProgram) => { + const a = attrib(program, "aPos", tri, 2); + gl.drawArrays(gl.TRIANGLES, 0, 3); + gl.disableVertexAttribArray(a); + }; + + const draw = () => { + // The real sky turns slowly; refresh it once a second. + if (wall() - skyAt > 1000) { + skyAt = wallMs; + now = sky(site, skyAt); + } + const tan = [tanY * (canvas.width / canvas.height), tanY]; + const px = Math.max(1, canvas.height / 540); + const view = (program: WebGLProgram) => { + gl.useProgram(program); + gl.uniformMatrix3fv(at(program, "uCam"), false, cam); + gl.uniform2fv(at(program, "uTan"), tan); + gl.uniform1f(at(program, "uT"), t); + gl.uniform1f(at(program, "uPx"), px); + gl.uniform1f(at(program, "uMoonlight"), now.moonlight); + gl.uniform2fv(at(program, "uTilt"), tilt); + gl.uniform1f(at(program, "uGlow"), site.glow); + gl.uniform1f(at(program, "uLights"), site.lights); + for (const [name, c] of [ + ["uC0", site.curtains[0]], + ["uC1", site.curtains[1]], + ] as const) { + gl.uniform3f(at(program, name), c.base, c.centre, c.half); + } + }; + + // 1. Footprint. + gl.disable(gl.BLEND); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.viewport(0, 0, MAP_W, MAP_H); + view(foot); + quad(foot); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, canvas.width, canvas.height); + + // 2. Sky and moon. + const sp = skyProgram; + if (!sp) return; + view(sp); + gl.uniform1f(at(sp, "uPxAng"), (2 * tanY) / canvas.height); + gl.uniform4f(at(sp, "uMoon"), ...now.moon, now.moonRadius); + gl.uniform3fv(at(sp, "uMoonN"), now.moonNorth); + gl.uniform3fv(at(sp, "uSun"), now.sun); + quad(sp); + + // 3. Stars and sparks, added on top. + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE); + if (starBuf && starCount) { + view(stars); + gl.uniformMatrix3fv(at(stars, "uStars"), false, columns(now.stars)); + gl.uniform1f(at(stars, "uLimit"), site.limit - 2 * now.moonlight); + gl.uniform1f( + at(stars, "uFade"), + Math.min(1, (performance.now() - starsAt) / 1500), + ); + const a = attrib(stars, "aDir", starBuf, 3, 20, 0); + const b = attrib(stars, "aMag", starBuf, 2, 20, 12); + gl.drawArrays(gl.POINTS, 0, starCount); + gl.disableVertexAttribArray(a); + gl.disableVertexAttribArray(b); + } + view(sparks); + const s = attrib(sparks, "aSeed", seeds, 4); + gl.drawArrays(gl.POINTS, 0, sparkCount); + gl.disableVertexAttribArray(s); + + // 4. Mountains over everything. + gl.disable(gl.BLEND); + view(ridge); + const r = attrib(ridge, "aRidge", ridgeBuf, 4); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, ridgeCount); + gl.disableVertexAttribArray(r); + }; + + const stop = () => { + cancelFrame(raf); + raf = 0; + }; + + const giveUp = () => { + stop(); + level = 3; + scope.postMessage("off"); + }; + + // If frames run long, step quality down; past the last level give up and + // leave the page's CSS gradient showing. + const adapt = (elapsed: number) => { + if (!tuning || elapsed > 250) return; + total += elapsed; + if (++samples < 45) return; + const slow = total / samples > 45; + samples = 0; + total = 0; + if (!slow) { + tuning = false; + } else if (++level === 1) { + const lighter = compile(gl, QUAD_VS, skyFs(12, 260)); + if (lighter) { + setup(lighter); + skyProgram = lighter; + } + resize(); + } else if (level === 2) { + sparkCount = SPARKS / 2; + } else { + giveUp(); + } + }; + + const frame = (stamp: number) => { + raf = nextFrame(frame); + const elapsed = stamp - last; + if (elapsed < FRAME_MS - 2) return; + if (last) { + t += Math.min(elapsed, 100) / 1000; + adapt(elapsed); + } + last = stamp; + draw(); + }; + + const run = () => { + if (raf || level > 2 || still || hidden) return; + last = 0; + raf = nextFrame(frame); + }; + + canvas.addEventListener("webglcontextlost", giveUp); + resize(); + draw(); + scope.postMessage("on"); + run(); + + // Stars arrive after the first frame so they never delay it. Without them + // the aurora still runs. + fetch(new URL("/stars.bin", self.location.origin)) + .then((res) => (res.ok ? res.arrayBuffer() : Promise.reject(res.status))) + .then((buf) => { + if (level > 2) return; + const data = starData(buf); + starBuf = buffer(data); + starCount = data.length / 5; + starsAt = still ? -Infinity : performance.now(); + if (!raf) draw(); + }) + .catch(() => {}); + + scope.onmessage = ({ data }) => { + if (level > 2) return; + if (data.type === "site") { + place(data.site); + skyAt = wall(); + now = sky(site, skyAt); + draw(); + scope.postMessage("on"); + } else if (data.type === "size") { + width = data.width; + height = data.height; + resize(); + draw(); + } else if (data.type === "still") { + still = data.still; + stop(); + draw(); + run(); + } else if (data.type === "hidden") { + hidden = data.hidden; + if (hidden) stop(); + else run(); + } + }; +} + +scope.onmessage = ({ data }) => { + if (data.type === "init") start(data); +}; diff --git a/apps/site/src/scripts/skylines/helsinki.ts b/apps/site/src/scripts/skylines/helsinki.ts new file mode 100644 index 0000000..636c461 --- /dev/null +++ b/apps/site/src/scripts/skylines/helsinki.ts @@ -0,0 +1,12 @@ +// Generated by scripts/build-skyline.mjs. Do not edit. +// Helsinki: 60.1493 N, 24.9848 E, eye 3 m above ground (3.0 m). +// elev: Int16 little-endian, hundredths of a degree above the horizontal. +// dist: Uint16 little-endian, tens of metres to the ridge. +// base: Int16, hundredths of a degree: the far shore over water, or -40°. +export const SKYLINE = { + from: -100, + step: 0.05, + elev: "DwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABEAEQARABPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgE+AT4BPgFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhASEBIQEhAWoAaQBpAGkAaQBpAGkAaQBpAGkAaQBpAGkAaQBpAGkAaQBpAGkAaQBpAGkAaQBlAGUAZQBlAGUAZQBlAGUAZQBlAGUAZQBlAGQAZABkAGQAZABkAGQAZABkAGQAZABkAGQAZABkAGQAZABkAGQAZABkAGIAYgBiAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAugDiAMEAoADHAL4AlwBvAG8AbwBvAG8AbwBvAG8AbwBgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAF8AXwBfAF8AXwBfAF8AXwBfAF8AXwBfAF8AXwBbAFsAWwBbAFsAWwBbAFsAZQBfAF8AXwBfAF8AXwBtAG0AbQBtAG0AbQBtAG0AbQBtAG0AbQBtAG0AbQBtAG0AbQBtAGAAYABgAGAAYABgAGAAYABtAG0AbQBtAG0AbQBtAG0AbgBuAG4AbgBuAG4AbgBtAG0AbQBtAG0AbQBtAG0AbQBtAG0AbQBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AfgCDAKkAiQCJAIkAiQCJAG4AXwBfAF8AXwBfAF8AXwBfAF8AXwBfAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABbAFsAWwBbAFsAWwBbAFsAWwBbAFsAWwBbAFsAWwBbAFsAWwBbAFkAVQBVAFUAVQBVAFQAVABUAFQAVABUAFQARwBHAEcARwBHAEsAVwBXAFcAVwBXAFcAbQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBjAGMAYwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBTAFMAUwBTAFMAUwBTAFMAUwBOAE4ASABIAE4AVwBfAFoAUQBIAEgASABIAEIATQBNAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAEUARQBFAEUARQBFAEUARQBUAFQARQBDAEMAQwBDAEsASwBLAEsASwBLAEsASwBLAEsASwBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAQwBDADwAPAA8AEcARwBHAEcARwBHAEcARwBHAEcARwBHAEcARwBHAEgARwBGAEIAQABAAEAAQABAAEAAQABAAEAAQAA8ADwAPABfADwAPAA8ADwASQBWAGAAVABNAE0AcwB+AIEAlQCAAHwAbwBVAFUAYQBsAF0ATABKAEkAOwA7ADsAOwA7ADsAOwA7ADsAOwBAADsAOwA7ADsAOwA7ADsAOwA7ADsAMwAzAC0ALQAzADMAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEQARABEAEQARABEAEQARABEAEQARABEAEQASgBKAEoASgBVAF0AcQBbAFMASgBLAEoASgBKAEoASgBKAEoASgBKAEoASgBKAEoATgBOAE4ATgBOAE4ATgBOAE4ATgBOAE4ATgBOAE0ATQBNAE0ATQBNAE0AVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFAAUABQAFAATQBNAE0ATQBNAE0AUwBTAFMAUwBtAFMAUwBTAFEAUQBRAFEAgwBRAGwAZgBmAGYAZgBYAFgAWABYAFgAWABYAFgAYABgAGAAYABgAFIAUgBSAFIAUgBSAFIAUgBSAFIAUgBSAFIAWABYAFgAWABYAFgAWABYAFgAWABYAFgAeACCAIYAggBfAF8AXwBfAF8AXwBfAF8AXwBfAF8AXwBfAFIAUgBSAFIAUgBSAFIAUgBSAFIAUgBSAFIAUgBSAEcARwBLAEsASwBLAEsASwBLAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAE0ATQBNAEkASQBKAEoASgBQAFAAUABQAFAAUABpAGkAaQBpAGkAaQBpAGkAaQBpAFAAUABLAEsASwBCAEIAQQBBAEEAQQBBAEIAQgBCAEcARwBcAFwAXABcAFwAXABcAFwAXABcAGIAYgBpAGkAYgBiAGIAYgBhAGEAYQBhAGEAYQBhAGEAYQBhAEQARgBJAEkASQBJAEkASQBJAFsAWwBbAFsAWwBbAFsAWwBbAFEAUQBRAFEAUQBRAFEAUQBRAEUARQBFAEUARQBFAEYARgBGAEYARgBGAEYARgBGAEMAQQBBAEEAQQBBAEEAQQBBADwAPABAAEAAQABAAEAAQABAAEYARgBGAEYARgBGAEYAQgBCAEIARQBFAEUARQBFAEUARQBFAEAAQABAADYAQwBDAEMAQwBDAEMAQwA2ADYANgA2ADUANQA1ADYANgA2ADYANgA2ADYANgA2ADsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBCAEIAQgBCAEIAQgBCAEIAOgA6AEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAWABYAJAAWABYAFgAOQA5ADkAOQA5ADkAOQA5AOMA4wDjAOMAOQA5ADkAOQA5ADkAOQAyADIAMgAzADMAMwAzADEAMQA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+ACsAKwArACsAKwArACsAKwArAE4ATgBOAE4ATgBOAE4ATgBOAE4AOAAsACwALAAsACwALAAsAD0APQA9AD0APQA9AD0APQA9ADYANgBBAEEAQQBBAEEAQQBEAEQARABEAEQARABEAEMAQwBDAEMAQwBDAEIAQgBCAEIAQgAzADMAKQCDAIMAgwCDAIMAgwCDAIMAkgCSAJIAkgCSAJIAkgCSAJIALQAtAC0AegB6AKEAoQChAKEAoQChAKEAKQArACkAKQC0ALQAtAC0ALQAtAC0AIsAiwCLAC4ALgAsACwALAAsAH4ApQClAKUApQClAKUApQClAH0ANgAsADEAKwA0ADgANAA0ADgAKwArADMAMwA0ADcANwA8ADwAPAA3ADcAOAA4ADoASAA4ADgAOAA4ADgAOAA4ADgAOAA4ADYANgA2ADoAOgA6ADoAOQA2ADYANQAqACoALwAvAC8ALwAuAC4ALgAuADkAOQA5ADkAOQA5ADkANwA3ACgAKAAoADgAOAArADAAMAAwACoAKgAqACoAOQAtADgAOAA4ADgAOAA4ADgAMwAzADMAMwAzADMALQAtAC0ALQArACsAKwArADAAMAAwACsAKwAsACwALAAsACwALAAsACwAKwAsACwALAArACsAKwArACsAKgAnACcAJwAnACcAJwAkACQAJAAfACQAJwAtAC0ALQAtACcAJwAnACcAIgAiAB8AHwAcAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAfAB8AHwApACkAHwAfAB8AHQAdACYAJgAmACYAHAAmACgAKAAoABsAHAAcABoAGgAaAB0AHQAdABwAHAAaABoAGgAaACEAIQAhABsAGwAbABsAGwAbACIAJQAlACUAIQAhACEAHwAhACQAJAAkACIAIAAZACQAJAAkACQAJAAkACMAGwAbABsAGgApACkAGwAcABwAHAAcABwAHQAdAB0AHQAdACAAIAAgACAAIAAgAB4AHgAeAB4AJQAlACUAJQAkACQAJAAkAB4AHgAeAB0AJAAkAB8AIgAiACIAIgAdAB4AHwAfAB8AHwAfAB8AGQAZABkAFwAeAB4AHgAcABwAHAAYABYAFgAWABUAGwAZABkAGQAXABYAFAAcABwAFwAXABoAGgAXABgAGQAZABkAGAAbABsAGwAbACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAgACQAJAAkACYAJgArACsAKwArACsAKwArACsAKwAuAC4ALgAuAC4ALwAvAC8ALwAvAC8ALwAvAC8ALwAvAC8ALwAvAC8ALwAvAC8ALwArACsAKwArACsALAAsACwALAAsACwALAAsACwALAAsACwALQAtAC0ALQAtAC0ALQAnACwALAAsACwALAAsAC8ALwAvAC8ALwAvAC8ALwAuAC4ALgAuAC4ALgAuAC4ALgAuAAEBAQEuAC4ALgAwADAAMAAwADAAMAAuAC4ALgAuAC4ALQAtAC0ALgAuAC4ALgAuAC8ALwAxADEALwAvAC8ALwAvAC8ALwAvAC8ALwAvAC8AMQAyADIAMgAxAC4ALgAxADEAMQAxADEALgAuACgALAAvAC8ALwAvAC8ALwAvAC0ALQAsACwALAAsACkAJwAdAB0AJwAnACcAJwAnACIAIgAmACYAJgAmACYAJAAiACUAJQAlACUAKABhAGEAYQBhAGEAYQBhAGEAYQBhAGEAYQBhAGEAYQBhAGEAYQBhAGEAYQAnACcAJgAoACkAKgArACwALQAvADAAMQAyADMANAA1ADYANwA3ADgAOAA6ADsAPAA9AD4APQA9AD0APQA9AD0APgA+AD8APwA/AEAAQAA/AD4APgA9ADwAOwBPAE8ATwCFAIUAhQCFAIUAhQCFAIUAhQCFAIUAhQCFAIUAhQCFAIUAhQCFADAAMAAwAC8ALwAuAC0ALQA4ADgAOAA4ADgAOAAqACoAKgA7ADsAOwA7ADsAOAA4ACUAJQAoAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAIEAgQCBAHoAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbAA9AD0ALAAsAC0ALQAuAC8ALwAwADAAMAAxADEAMgAyADMAMwAzADMAMwAyADIAMgAxADEAMQAxADEAMQAxADIAMgAyADIAMgAyADEAMQAwADAAMAAvAC8ALwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzACsAKwAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAuAC4ALgAuAC4ALgAuAC4ALgAuAC4ALgA/AD8APwA/AD8APwA/AD8APwBGAEYARgBGAEYARgBGAEYARgBGAEYAOQA5ADkAOQA5ADkAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADoAOgA6ADoAOgA6ADoAOgA6ADoASwBLAEsASwBLAE4ATgBOAE4ATgBOAE4ATgBOAE4ASABIAEgASABIAEgASABIAEgASABIAEgATQBNAE0ATQBNAE0ATQBNAE0ATQBNAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAPAA8ADwAPAA8ADwAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADkAOQA5ADkAOQA5ADkAOQA5ADkAOAA4ADgAOAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADgAMgAyADIAMgAyADIAMgA1ADUANQA2ADoAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAQABAAEAAQABAAEAAQABAAEAAPwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA9AD0APQBBAEEAQQBBAEEAQQBBAEEAQQBBAEEAQQA=", + dist: "GAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABUAFQAVABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwATAAgBCQEJAQkBCQEJAQkBCQEJAQkBCQEJAQkBCQEJAQkBCQEJAQkBCQEJAQkBCQH7APsA+wD7APsA+wD7APsA+wD7APsA+wD1AOUA5QDlAOUA5QDlAOUA5QDlAOUA5QDlAOUA5QDlAOUA5QDlAOUA5QDlAOIA4gDiAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAgECAQIBBAEEAQQBBAHdAN0A3QDdAN0A3QDdAN0A3QDgAOAA4ADgAOAA4ADgAOAA4ADgAOAA8wDzAN4A3gDeAN4A3gDeAN4A3gDeAN4A3gDeAN4A3gCrAKsAqwCrAKsAqwCrAKsAegHuAO4A7gDuAO4A7gD7APsA+wD7APsA+wD7APsA+wD7APsA+wD7APsA+wD7APsA+wD7ALoAugC6ALoAugC6ALoAugDRANEA0QDRANEA0QDRANEA4gDiAOIA4gDiAOIA4gDRANEA0QDRANEA0QDRANEA0QDRANEA0QDTANMA0wDTANMA0wDTANMA0wDTANMASAFHAUgBGAEYARgBGAEYAdMAsQDPAM8AzwDPAM8AzwDPAM8AzwDPACEBIQEhASEBIQEhASEBDgEOAQ4BDgEOAQ4BDgEOAQ4BDgEOAQ4BDgH2APYA9gD2APYA9gD2APYA9gD2APYA9gD2APYA9gD2APYA9gD2AD0B+AD4APgA+AD4APkA+QD5APkA+QD5APkADwEPAQ8BDwEPAT8B9gD2APYA9gD2APYAswH2APYA9gD2APYA9gD2APYA9gD2APYA9gD2APYA9gD2APYA9gD8APwA/AD8APwA/QCYAZgBmAH9AP0A/QD9AP0A/QD9AP0A/QD9AP0A/QD9AP0A/QAtAS0BLQEtAS0BLQEtAS0BLQE1ATUBBAEEAVABUAFQAVABUAEEAQQBBAEEAS4BCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIAQgBCAEIASMBIwEjASMBIwEjASMBIwE1ATUBIwE0ATQBNAE0ATYBNgE2ATYBNgE2ATYBNgE2ATYBNgExATEBMQExATEBMQExATEBMQExATEBMQExATEBMQExATEBMQExATEBMQExATEBMQExAToBOgE6ATgBOAE4ATgBOAE4ATgBOAE4ATgBOAE4ATgBOAE4AS0BLQEtAS0BPQE9AT0BPQE9AT0BPQE9AT0BPQFJAUkBSQEMAkkBSQFJASgBJwEnASgBJwEmASYBJwEnAScBKAEnAScBJwEnAScBKAEoASgBJgEmASUBNAE0ATQBNAE0ATQBNAE0ATQBNAGBAjQBNAE0ATQBNAE0ATQBNAE0ATQBKwErAR0BHQEcARwBIgEiASIBIgEiASIBIgEiASIBIgEiASABIAEgASABIAEgASABIAEgASABIAEwATABMAEwATABMAEwATABMAEwATABMAEwATABMAEwARUBFQEVARUBFQEVARUBFQEVARUBFQEVARUBDwEPAQ8BDwH+AP4A/gD+AP4ADwH+AA8BDwEPAQ8BDwEPASABIAEgASABIAEgASAB/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8ANoA2gDaANoA2gDaANoA/QD9AP0A/QD9AP0A/QD9AP0A/QD9APcA9wD3APcAHAEcARwBHAEcARwBSgFKAUoBSgHpAUoBSgFKAUsB5wDnAOcA6gHnAPgBvwK/Ar8CvwLiAOIA4gDiAOIA4gDiAOIAWQJZAlkCWQJZAuUA5QDlAOUA5QDlAOUA5QDlAOUA5QDlAOUA5ADkAOQA5ADkAOQA5ADkAOQA5ADkAOQAsQGyAbIBsgHoAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAOcA5wDnAOcA5wDnAOcA5wDnAOcA5wDnAOcA5wDnAOAA4ADrAOsA6wDrAOsA6wDrAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAOgA6ADoAMYAxgDCAMIAwgDhAOEA4QDhAOEA4QBsAWwBbAFsAWwBbAFsAWwBbAFsAeEA4QBDAkMCQwJkAWQBqwGrAasBqwGrAaUBpQGlAa0BrQFkAWQBZAFkAWQBZAFkAWQBZAFkAW0BbQFuAW4BbQFtAW0BbQFiAWIBYgFiAWIBYgFiAWIBYgFiAbMBoQG1AbUBtQG1AbUBtQG1AWABYAFgAWABYAFgAWABYAFgAWkBaQFpAWkBaQFpAWkBaQFpAcEAowGjAaMBowGjAaUBpQGlAaUBpQGlAaUBpQGlAawBqAGoAagBqAGoATQCNAI0AsMBwwGbAZsBmwGbAZsBmwGbAZ0BnQGdAZ0BnQGdAZ0BpAGkAaQBnQGdAZ0BnQGdAZ0BnQGdAZoBmgGaAb8BqQGpAakBqQGpAakBqQG/Ab8BvwG/AZ0BnQGdAbgBuAG4AbgBuAG4AbgBuAG4AcUBzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADMAMwAzADHAMcAxwDHAMcAxwDHAMcAuQG5AckAyQDJAMkAyQDJAMkAyQDJAMkAyQDJAMkAyQDJAMkAyQDJAMkAyQDJAMkATAFMAZEBTAFMAUwBgwGDAYMBgwGDAYcBhwGHAYQBhAGEAYQBhwGHAYcBhwGHAYcBhwGBAYEBgQGHAYcBhwGHAaUBpQFeAV4BXgFeAV4BXgFeAV4BXgFeAV4BXgFeAV4BXgFeAV4BXgFeAV4BXgFeAWABYAFgAWABYAFgAWABYAFgAUkBSQFJAUkBSQFJAUkBSQFJAUkBYgFdAV0BXQFdAV0BXQFdAWkBaQFpAWkBaQFpAWkBaQFpAbkBuQFeAV4BXgFeAV4BXgG+Ab4BvgG+Ab4BvgG+AWYBZgFmAWYBZgFmAW4BbgFuAW4BbgHGAcYBZwGhAaEBoQGhAaEBoQGhAaEBqgGqAaoBqgGqAaoBqgGqAaoBFgEWARYBqgGqAasBqwGrAasBqwGrAasBjgEaAY4BjgGpAakBqQGpAakBqQGpAagBqAGoASQBJAEoAQ8BDwEPAaUBpgGmAaYBpgGmAaYBpgGmAaUBJwEKARMBIgEOASgBDgEOASgBIgEiAQoBCgETAasBqwElASUBJQGrAasBIgEiAQ8B1AMiASIBIgEiASIBIgEmASYBJgEmARgBGAEYARABEAEQARABFAElASUBGwEHAQcBGwEbARsBGwG5AbkBuQG5ARIBEgESARIBEgESARIBswGzARYBFgEWAREBEQElARoBGgEaASUBJQElASUBEgESAREBEQERAREBEQERAREBDwEPAQ8BDwEPAQ8BDQENAQ0BDQEUARQBFAEUAQ8BDwEPARQBFAEZARkBGQEZARkBGQEZARkBFAEUARQBFAEUARQBFAEUARQBFgEYARgBGAEYARgBGAF3A3cDdwNhAXADdQNiAWEBYQFhAW0DbQNlAWUBZgFmAWgBaAFrA6ADoAOgA6ADoAOIA4gDiAOIA4gDiAOcA5wDnANTAVMBzAPMA8wDjwOPA8YDxgPGA8wDkAO8A8IDwgPCA98D3APZA8MDwwPDA9oD2gPaA9YD1gPDA8MDwwPDA14EXgReBLcDtwO3A7cDtwO3A7AD3APcA9wD2APYA9gD4gPTA7IDsgOyA6wDzgOVAWUBZQFlAWUBZQFlAakDkwGTAZMBZwFjAWMBZwHIA8gDZQFlAWUBawFlAWUBZQFlAZMBkwGTAZMBkwGTAWcBZwFnAWcBlQGVAZUBlQGSAZIBkgGSAWcBZwFnAWcBmAGYAZQBlgGWAZYBlgGPAZoBlwGPAY8BjwGPAY8BagEhAiECaAGMAYwBjAEgAiECIQIkAogBiAGIAYsBIAIiAh8CHwIZAh8CnAFmAWYBiQGJAWQBZAGQAYoBZQFlAWUBigGQAZABkAGQAZgBmAGYAZgBmAGYAZgBmAGYAZgBmAGYAZgBmAGYAZgBmAGYAZgBmAGYAZgBmAGQAaIBogGiAagBqAGdAZ0BnQGdAZ0BnQGdAZ0BnQGfAZ8BnwGfAZ8BpwGnAacBpwGnAacBpwGnAacBpwGnAacBpwGnAacBpwGnAacBpwGdAZ0BnQGdAZkBmwGeAZ4BngGeAZ4BngGeAZ4BngGaAZoBogGiAaIBogGiAaIBogG0AbkBuQG5AbkBuQG5AbMBswGzAbMBswGzAbMBswG9Ab0BvQG9Ab0BvQG9Ab0BvQG9ASUBJQG9Ab0BvQGtAa0BrQGtAa0BrQGnAacBpwGnAacBtAG0AbQBsAGwAbABsAGwAbMBswG1AbUBswGzAbMBswGzAbMBswGzAbMBswGzAbMBqgGtAa0BrQGqAbcBtwGqAaoBqgGqAaoBpQGlAaEBowGtAa0BrQGtAa0BrQGtAa8BrwGtAa0BrQGtAa8BhwKvAd8ChAKEAoQChAKEArcBtwGCAoICggKCAoIChgK+AXMCcwJzAnMCRAI9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQBoAmgCPgA+AD4AKAI+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APwA/AD8APwA/AD8APwA/AD8APwA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgBDAEMAQwA9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD8APwA/AD8APwA/AD8APwBhAmECYQJhAmECYQJCAkICQgI/Aj8CPwI/Aj8CVAJUAj4APgBbAj4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+AD4APgA+ADwAPwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD8APwA/AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQA9AD0APQAcARwBNwE3ATcBOAE4ATgBOAE4ATgBOAE4AToBOgE6AToBOgE6AToBOgEvAS8BLwEvAS8BLwEuAS4BLgEuAS4BLgEuAS4BLgEuAS4BLgEvAS8BLwEvATQBNAEgASABIAEgASABIAEgASABIAEgASABIAEgAS4BLwEdAR0BHQEdAR0BHQEdAR0BHQEdAR0BHQEdAR0BHQEbARsBGwEbARsBGwEbARsBGwEbARsBGwEWARYBFgEWARYBFgEWARYBFgEUARQBFAEUARQBFAEUARQBFAEUARQBFAEUARQBFAEUARQBDgEOAQ4BDgEOAQ4BDgEOAQ4BDgEOAQoBCgEKAQoBCgEKAQoBCgEKAQABAAEAAQABAAEAAQABAAEAAQABDQENAQ0BDQENAQYBBgEGAQYBBgEGAQYBBgEGAQYBGAEYARgBGAEYARgBGAEYARgBGAEYARgBEQERAREBEQERAREBEQERAREBEQERARcBFwEXARcBFwEXARcBFwEXARcBFwEXARcBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAQwBDAEMAZIBkgGSAZIBkgGSAZIBkgGSAZIBjwGPAY8BjwFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAW4BpgGmAaYBpgGmAaYBpgGGAYYBhgFpAaUBpQGlAaUBpQGlAaUBpQGlAaUBpQGlAW8BbwFvAW8BbwFvAW8BbAFsAWwBbAFsAWwBbAFsAWwBYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFbAVsBWwFcAVwBXAFqAWoBagFqAWoBagFmAWYBZgE=", + base: "rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+Y/5j/mP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+g/6D/oP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/6j/qP+o/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/67/rv+u/7j/uP+4/7j/uP+8/7z/vP+8/7z/vP+8/7z/vP+8/7z/vP+8/7z/vP+8/7z/vP+8//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//T/9P/0//T/9P/0//T/9P/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//X/9f/1//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/7v/u/+7/7v/u//f/9v/2/+7/7v/u/+7/7v/u/+7/7v/u/+7/7v/v/+//7//v/+//7//v/+//8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/y//L/8v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/1//X/9f/1//X/9f/1//X/9f/1//X/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/2//b/9v/3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/h/+H/4f/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4v/i/+L/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/5//n/+f/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/+P/4//j/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9/8=", +}; diff --git a/apps/site/src/scripts/skylines/isafjordur.ts b/apps/site/src/scripts/skylines/isafjordur.ts new file mode 100644 index 0000000..16bb1a0 --- /dev/null +++ b/apps/site/src/scripts/skylines/isafjordur.ts @@ -0,0 +1,12 @@ +// Generated by scripts/build-skyline.mjs. Do not edit. +// Ísafjörður: 66.075075 N, -23.124175 E, eye 2 m above ground (4.3 m). +// elev: Int16 little-endian, hundredths of a degree above the horizontal. +// dist: Uint16 little-endian, tens of metres to the ridge. +// base: Int16, hundredths of a degree: the far shore over water, or -40°. +export const SKYLINE = { + from: -80, + step: 0.2, + elev: "dwd4B3oHeweAB4wHmAekB7EHvgfKB9YH4wfwB/0HCggXCCQIMgg9CEkIVQhhCG0IdQh8CIMIiQiOCI8IjgiMCIsIiwiLCIwIigiJCIgIhwiGCIcIhgiGCIYIhwiJCIsIjQiNCIwIiwiLCIwIjQiMCIwIiwiLCIoIiQiHCIYIhQiFCIUIhgiICIoIjgiRCJMIkgiSCJQIlgiXCJcIlwiXCJcIlQiUCJMIlAiVCJgImwidCJ0ImwidCKAIpQinCKcIpgimCKcIqQisCK4IrgivCLAIsgizCLIIsgiwCLAIsQi0CLgIuQi3CLYItgi4CLoIvgi9CLoIuAi3CLYItwi3CLcIsgiuCK0IrAipCKkIqgirCK8IrwisCKkIqAioCKoIqwirCKcIpAihCJ8IngidCJkIlQiTCJEIjgiMCIoIigiJCIkIhwiFCIQIgwiECIUIhwiKCI4IkQiXCJwIngikCKkIqgisCK4IsAiyCLYItgixCKsIpgiiCJ4ImQiWCJEIjgiLCIUIggh9CHoIeAh0CHAIaghlCF8IXAhZCFYIVQhSCFEITAhJCEUIPwg5CDQIMAgtCCsIJAgjCCEIIQghCCAIHggfCB0IGwgbCBoIFggSCA4IDAgLCAsIBggDCAEIAAj/BwAIAggFCAcIAgj9B/kH+Af4B/gH8wfvB+0H6wfrB+kH5wflB+QH4gffB94H3gffB9wH2AfUB9MH0wfSB9AHzQfKB8UHwwfBB8AHwAe8B7QHrwerB6gHpgeeB5gHlAePB4wHiAeEB38HeAdzB2wHYgdVB0gHOwcuByEHFQcLBwcHBQcBB/0G+Qb3BvcG9wb5BvoG/Ab9Bv0G+gb4BvYG9AbxBu4G6wbqBucG4gbdBtgGzQbCBrYGqQaaBooGewZpBlgGUAZHBj8GOAYxBikGIQYcBhgGGAYbBh4GHgYfBh8GHQYZBhUGEQYLBgUG/wX0BeQF1QXDBagFjQV6BW0FZAVXBUsFQAU0BSkFIAUZBREFBQX6BO0E0ATCBKMEiwRwBFsEVgRPBEkEQQQ3BC0EHQQLBPcD2QO8A6oDnAONA3gDZwNXA0cDOAMpAx0DEAMBA/MC5QLYAssCuAKnApYChQJzAmECVAJDAjACHwIMAvwB6wHaAcgBuAGmAZcBiAF5AWsBXQFQAUEBNgEsASMBGQEQAQYB/ADzAOkA4ADXAM8AxgC9ALUArQClAJ0AlQCOAIYAfgB3AG4AZQBcAFIASQBAADwAPAA/AEUASQBKAEoASgBLAEwATQBOAE4ATgBOAE4ATQBLAEoARABGAEgASABGAD8AQABCAEIAQwBEAEYARwBIAEgASABIAEkASgBLAEkASABHAEUAQQBCAFEAZQBqAG4AdwB6AH8AggCEAIQAhACEAIMAhACEAIQAhQCFAIcAigCMAI0AkACRAJIAkQCSAJIAkwCUAJUAlwCXAJcAlgCWAJYAlQCVAJQAlwCbAJ0AnwCgAKMApACmAKgAqQCrAKwArwCwALEAsgCzALQAtQC2ALcAuAC5ALoAvAC9AL8AwADCAMIAwwDEAMUAxwDJAMoAywDLAMsAzADMAMwAzgDPANAA0QDTANQA1QDWANcA2ADZANkA2gDaANkA2QDYANcA1wDWANYA1QDVANUA1QDWANYA1wDYANgA2wDdAN8A4ADfAOAA4ADhAOEA4QDiAOIA4gDiAOAA3wDfAN4A3QDdANwA2wDYANkA2QDcANsA2wDeAN4A3QDcANoA3ADeAN8A4ADgAN8A3wDdANkA1wDVANQA0gDSANQA1gDWANIAzwDPAM4AzADMAM4AzwDQANMA1QDXANoA3QDdANwA2wDaANgA1wDWANQA0wDSAM4AyADEAMAAvgC8ALoAuQC0ALUAtAC0ALQAtACzALMAswCyALMAtAC0ALMArwCtAKoApwCnAKUApACpALMAvQDHANIA3ADmAPAA+gADAQ0BFwEfAScBLwE2AT8BRwFQAVoBZAFtAXcBgAGKAZQBnwGqAbYBwgHOAdkB5AHvAfgBAgILAhQCHgIpAjgCSAJbAm4CfwKNAp0CqgK3AsUCywLOAtECzgLNAswCzALUAuAC7AL2AgADCwMVAyADKgM0AzwDQgNIA1ADVwNgA2sDdgOCA4UDigOPA48DjQOLA4sDlgOjA68DuwPOA+ID9gMEBA4EGQQjBCsENAQ9BEAEQwRGBEgESQRKBEwETgRQBFQEVwRbBF8EYgRoBHEEewSFBJQEogStBLkExATMBNME2wTgBOQE6ATrBOoE6gTpBOcE6QTtBPIE9gT7BAIFBQUIBQwFEAUQBRMFFwUXBRkFHgUjBSgFLAUzBTgFOQU7BT0FPQU9BT0FPQU7BTgFNgU0BTEFLwUvBS8FMgU3BTwFPQU/BUIFRQVGBUYFRwVKBUsFSgVLBUwFTQVMBU4FTwVOBUwFSgVIBUcFRgVFBUIFQAVABUIFRgVGBUYFSgVQBVMFUQVQBVEFVAVXBVkFXAVhBWYFaQVrBW4FcgV2BXoFfgWDBYcFjAWRBZcFngWiBaYFqwWvBa8FrgWwBbYFuAW3BbkFvAXBBcMFxQXIBc0F0gXTBdUF1QXWBdcF2gXcBd8F4gXkBeUF5wXpBegF5wXpBesF6gXuBfQF+gUBBgUGDgYXBh0GIgYnBi4GMwY2Bg==", + dist: "zADMAMwAzACuAK4ArgCuAK4ArgCuAK4ArgCwALAAsACwALAArgCuAK4ArgCuAK4ArgCuAK4ArgCuAK4ArgCuAK4AsACwALAAsACwALAAsACuAK4ArgCwALAAsACwALAAsACwALAAsACwAK4AsACwALAAsACwALAAsACwALAAsACwALAAsACwALAAsACwALAAsACwAK4ArgCwALAAsACwALAAsACwALAArgCuALAAsACwALAAsACuAK4ArgCuAK4ArgCuAK4ArgCuAK4ArQCtAK0ArQCtAK0ArQCtAK0ArQCtAK0ArQCtAK0ArQCtAK0ArQCtAK0ArQCtAK0ArQCtAK0ArQCtAK4ArgCuAK4ArQCuAK4ArgCuAK4ArgCuAK4ArgCuAK4ArgCuAK4ArgCuAK4ArgCwALAAsACwALAAsACwALAAsACwALAAsACuAK4ArgCuAK4ArQCtAK0ArQCrAKsAqwCrAKoAqgCrAKsAqwCrAKsAqwCrAKsArQCtAK0ArgCuAK4AsACwALAAsACwALAAsACwALAAsQCxALEAsQCzALMAswC0ALQAtAC0ALQAtAC2ALYAtwC3ALcAtwC3ALcAtwC2ALYAtwC3ALcAtwC3ALcAtwC5ALkAuQC5ALkAuQC6ALoAuQC5ALkAuQC5ALkAugC6ALoAugC6ALoAugC8ALwAvAC8ALwAvAC8ALwAvAC8ALwAvAC8AL0AvQC9AL0AvQC9AL0AvwC/AL8AvwC/AMAAwADAAMAAwADCAMIAwgDCAMMAwwDFAMUAxQDFAMYAxgDGAMYAxgDGAMYAxgDFAMUAxQDFAMUAxQDGAMYAyADJAMkAyQDJAMkAywDLAMsAywDLAMwAzADOAM4AzgDMAMwAzADMAMwAzADMAMwAywDLANQA1QDXANcA2ADYANsA4ADjAOQA5gDmAOcA6QDpAOkA6gDsAOwA7ADsAOwA7ADtAO0A7QDwAPAA8wD2APwA/AD8AP4A/wD8APwA+QD5APkA+QD5APsA8ADvAO8ACAEIARABEQERAREBEQERAREBEQERAREBEQERARoBGgEaARoBFwEXARcBHQEXARcBFAEUARQBFAEUARQBFAERAREBEQERAREBGgEaARoBFgEUAQsBCwEKAQsBEQENAfAA8ADwAPAA8ADwAO8A1QDVANUA1QDVANUA1QDVANUA1QDDAMMAwgDCAMAAwADAAMAAwADCAMIAwgDCAMIAwgDCAMMAxQDFAKQLpAvHC9YL4AvgC+UL5QtHDDgMOAwtDC0MKAwjDCMMIwweDB4MGQwVDQ8NDw0EDeMNEw4ZDhMOEw4ZDhkOGQ4ZDhkOKw4lDjcORA5KDkQOPQ43DjEOKw6GDTsHLAcsBwoH7wbtBtgGzAbBBrsGtQa4BskGyQbPBtIGzAbqBoUGgwZ9BngGdQZyBm0GbQZqBmQGYgZfBlkGVwZXBlcGVAZRBlQGVAZXBhkHHAcfBxwHJgcyBywHLAcpBywHLwcvBy8HLwcpBykHKQcpBywHLAcvBy8HLwcvBzIHMgcvBywHHwccBxwHLwcsBzIHSgdEB0EHRAdEB0EHQQdEB04HgAeAB3oHdgdgB10HWgdXB1oHWgdXB1oHVAdUB1QHUQdRB1QHVwdXB1cHSgdHB0EHQQdBB0QHRAc+BzUHEwcQBw0HCgcWBxYHGQcZB3YHegd9B4AHgAeAB20HWgdUB0oHRAdBB0EHQQdEB0QHSgdOB1EHmgedB6cHqgetBz4HOAc1BzUHNQc1BzgHOwc7B0EHRwdOB04HXQdRB04HQQdBB0EHRAcpBy8HMgcvBy8HLAc+BzsHOwc+B0EHRAdHB0oHTgdUB1cHWgddB2MHZwdqB2oHZwdjB10HWgdOB0oHRwc+B/oH+gf6B/oH9wfzB/AH6QfiB8sHwQe6B7cHsQenB60HtwfVB9UH4gdJAUcBRwFGAUYBRgFEAUQBRAFDAUMBQwFBAUEBQQE+AT4BPgE+AT4BPgE7ATsBOAE4ATcBNwE3ATUBNQE1ATUBMgEyATIBMQExAS4BLgEsASsBKQEpASkBKQEmASYBJgElASUBJQElASUBJQEjASMBIwEdAR0BHAEcARwBHAEcARwBHAEcARwBHAEaARoBGgEaARoBGgEaARoBGQEXARcBFwEXARYBEQERAREBEAEOAQ4BDgEOAQ4BDgEOAQ4BDgEOAQ4BDgENAQ0BDQENAQ0BDQEOAQ4BDQENAQ0BDQEIAQgBCAEHAQUBBQEFAQUBBQEFAQUBBQEFAQUBBQEFAQUBBQEFAQQBBAECAQIBAgECAQEBAQEBAQEB/wD/AP4A/gD+APwA/AD7APsA+wD5APkA+QD5APkA+AD4APgA+AD4APgA+QD5APkA+AD4APgA9gD2APYA9QD1APUA9QDzAPMA8gDyAPIA8ADwAPAA8ADvAO8A7wDvAO8A7wDvAPAA8ADwAPAA8ADvAO8A7wDvAO8A7QDtAO0A7QDtAOwA7ADsAOwA7ADqAOoA6gDqAOoA6QDpAOkA6QDpAOkA6QDpAOkA5wDnAOcA5wDnAOcA5wDmAOYA5gDmAOYA5gDmAOYA5gDmAOQA5ADkAOQA5ADkAOMA4wDjAOMA4wDjAOMA4wDjAOMA4wDkAOQA5ADjAOMA4wDjAOMA4ADgAOAA4ADgAOAA3gDeAA==", + base: "YPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8GDwYPBg8A==", +}; diff --git a/package.json b/package.json index 5afe967..9f6b013 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "type": "module", "scripts": { + "dev": "pnpm --filter @northguild/site dev", "build": "pnpm -r build", "check": "pnpm -r check", "lint": "oxlint", @@ -11,6 +12,9 @@ }, "devDependencies": { "@changesets/cli": "^3.0.2", + "@northguild/gmt-oxlint": "^1.2.1", + "geotiff": "3.0.5", + "jpeg-js": "^0.4.4", "oxfmt": "^0.63.0", "oxlint": "^1.78.0" }, diff --git a/packages/theme/primitives.css b/packages/theme/primitives.css index 2b29b55..3776c19 100644 --- a/packages/theme/primitives.css +++ b/packages/theme/primitives.css @@ -50,6 +50,8 @@ position: absolute; inset: 0; border-radius: inherit; + /* Follow the bevel too, or the hairline draws round corners outside it. */ + corner-shape: inherit; padding: 1px; background: linear-gradient( 135deg, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 771ae48..443f4a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@changesets/cli': specifier: ^3.0.2 version: 3.0.3 + '@northguild/gmt-oxlint': + specifier: ^1.2.1 + version: 1.2.1(oxlint@1.89.0) + geotiff: + specifier: 3.0.5 + version: 3.0.5 + jpeg-js: + specifier: ^0.4.4 + version: 0.4.4 oxfmt: specifier: ^0.63.0 version: 0.63.0 @@ -20,6 +29,9 @@ importers: apps/site: dependencies: + '@northguild/gmt': + specifier: ^1.19.0 + version: 1.19.0 '@northguild/theme': specifier: workspace:* version: link:../../packages/theme @@ -1018,6 +1030,10 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-temporal/polyfill@0.5.1': + resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} + engines: {node: '>=12'} + '@manypkg/find-root@3.1.0': resolution: {integrity: sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==} engines: {node: '>=20.0.0'} @@ -1044,6 +1060,15 @@ packages: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@northguild/gmt-oxlint@1.2.1': + resolution: {integrity: sha512-ot1NndXxCaGikCYqe3r4D+nFM3twd1WMwylfj8xfWLCCWSRYgIFXmQ2yLpCcjf53KWxS660jmaIqWndXQh5lQA==} + peerDependencies: + oxlint: '>=1.0.0' + + '@northguild/gmt@1.19.0': + resolution: {integrity: sha512-RWOyb9G4z57t1wF2j7TrHivHW/MHFQHI8Ub63tWUIwhzBhf968gh84wvvBOrzc5GI36GkZbVJxOPj6Rj+ymSDA==} + engines: {node: '>=22.16.0'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -1294,6 +1319,9 @@ packages: cpu: [x64] os: [win32] + '@petamoriken/float16@3.9.3': + resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@pnpm/deps.graph-sequencer@1100.0.1': resolution: {integrity: sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==} engines: {node: '>=22.13'} @@ -1773,6 +1801,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + geotiff@3.0.5: + resolution: {integrity: sha512-OWcL9S9+yDZ6iAlXMt32T1iwUApJM8UiD47xbm6ZP1h33d10fqkPs14EG/ttT5EnefpZSx3G15iDFC5FxUNUwA==} + engines: {node: '>=10.19'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1832,10 +1864,16 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-yaml@4.3.2: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true + jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -1852,6 +1890,9 @@ packages: launch-editor@2.14.2: resolution: {integrity: sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==} + lerc@3.0.0: + resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==} + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -2057,6 +2098,12 @@ packages: package-manager-detector@1.9.0: resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + + parse-headers@2.0.6: + resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2100,6 +2147,10 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -2517,6 +2568,9 @@ packages: vscode-uri@3.2.0: resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + workerd@1.20260825.1: resolution: {integrity: sha512-ccS6TEaaRxgONKawiYGnFYUVGfr2pmN1b7mrNtw0ADVhFaYsEIVoCHnQ4UjhM9EJDzuaNgAWFY82nzVrjWpuOA==} engines: {node: '>=16'} @@ -2548,6 +2602,9 @@ packages: utf-8-validate: optional: true + xml-utils@1.10.2: + resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==} + xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} @@ -2590,6 +2647,9 @@ packages: zod@4.6.7: resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + zstddec@0.2.0: + resolution: {integrity: sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3348,6 +3408,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@js-temporal/polyfill@0.5.1': + dependencies: + jsbi: 4.3.2 + '@manypkg/find-root@3.1.0': dependencies: '@manypkg/tools': 2.1.2 @@ -3383,6 +3447,14 @@ snapshots: '@tybys/wasm-util': 0.10.4 optional: true + '@northguild/gmt-oxlint@1.2.1(oxlint@1.89.0)': + dependencies: + oxlint: 1.89.0 + + '@northguild/gmt@1.19.0': + dependencies: + '@js-temporal/polyfill': 0.5.1 + '@oslojs/encoding@1.1.0': {} '@oxc-project/types@0.154.0': {} @@ -3501,6 +3573,8 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.89.0': optional: true + '@petamoriken/float16@3.9.3': {} + '@pnpm/deps.graph-sequencer@1100.0.1': {} '@poppinss/colors@4.1.6': @@ -4046,6 +4120,17 @@ snapshots: fsevents@2.3.3: optional: true + geotiff@3.0.5: + dependencies: + '@petamoriken/float16': 3.9.3 + lerc: 3.0.0 + pako: 2.2.0 + parse-headers: 2.0.6 + quick-lru: 6.1.2 + web-worker: 1.5.0 + xml-utils: 1.10.2 + zstddec: 0.2.0 + get-caller-file@2.0.5: {} get-east-asian-width@1.7.0: {} @@ -4106,10 +4191,14 @@ snapshots: jju@1.4.0: {} + jpeg-js@0.4.4: {} + js-yaml@4.3.2: dependencies: argparse: 2.0.1 + jsbi@4.3.2: {} + json-schema-traverse@1.0.0: {} jsonc-parser@2.3.1: {} @@ -4123,6 +4212,8 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.12.0 + lerc@3.0.0: {} + lightningcss-android-arm64@1.33.0: optional: true @@ -4330,6 +4421,10 @@ snapshots: package-manager-detector@1.9.0: {} + pako@2.2.0: {} + + parse-headers@2.0.6: {} + path-browserify@1.0.1: {} path-to-regexp@6.3.0: {} @@ -4358,6 +4453,8 @@ snapshots: property-information@7.2.0: {} + quick-lru@6.1.2: {} + radix3@1.1.2: {} readdirp@4.1.2: {} @@ -4775,6 +4872,8 @@ snapshots: vscode-uri@3.2.0: {} + web-worker@1.5.0: {} + workerd@1.20260825.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260825.1 @@ -4807,6 +4906,8 @@ snapshots: ws@8.21.0: {} + xml-utils@1.10.2: {} + xxhash-wasm@1.1.0: {} y18n@5.0.8: {} @@ -4858,4 +4959,6 @@ snapshots: zod@4.6.7: {} + zstddec@0.2.0: {} + zwitch@2.0.4: {} diff --git a/scripts/build-moon.mjs b/scripts/build-moon.mjs new file mode 100644 index 0000000..d57774e --- /dev/null +++ b/scripts/build-moon.mjs @@ -0,0 +1,56 @@ +// Bakes a small lunar albedo map for the aurora's moon from NASA SVS's CGI +// Moon Kit (LRO colour map, longitude 0 at the centre, east to the right). +// Writes apps/site/src/scripts/aurora.moon.ts: 128×64 greyscale, base64. +// +// Run: node scripts/build-moon.mjs +// Moon: NASA's Scientific Visualization Studio. + +import { writeFileSync } from "node:fs"; +import jpeg from "jpeg-js"; + +const SOURCE = + "https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/lroc_color_poles_1k.jpg"; +const W = 128; +const H = 64; +const OUT = new URL("../apps/site/src/scripts/aurora.moon.ts", import.meta.url); + +const res = await fetch(SOURCE); +if (!res.ok) throw new Error(`moon map: HTTP ${res.status}`); +const img = jpeg.decode(Buffer.from(await res.arrayBuffer()), { + useTArray: true, +}); + +// Box-filter down to W×H luminance. +const out = new Uint8Array(W * H); +const sx = img.width / W; +const sy = img.height / H; +for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + let sum = 0; + let n = 0; + for (let j = Math.floor(y * sy); j < Math.floor((y + 1) * sy); j++) { + for (let i = Math.floor(x * sx); i < Math.floor((x + 1) * sx); i++) { + const o = (j * img.width + i) * 4; + sum += + 0.2126 * img.data[o] + + 0.7152 * img.data[o + 1] + + 0.0722 * img.data[o + 2]; + n++; + } + } + out[y * W + x] = Math.round(sum / n); + } +} + +writeFileSync( + OUT, + `// Generated by scripts/build-moon.mjs from NASA SVS's CGI Moon Kit. Do not edit. +// ${W}×${H} greyscale albedo, equirectangular, longitude 0 at the centre. +export const MOON = { + width: ${W}, + height: ${H}, + data: "${Buffer.from(out).toString("base64")}", +}; +`, +); +console.log(`${W}×${H}, ${out.length} bytes`); diff --git a/scripts/build-skyline.mjs b/scripts/build-skyline.mjs new file mode 100644 index 0000000..ad34a36 --- /dev/null +++ b/scripts/build-skyline.mjs @@ -0,0 +1,382 @@ +// Builds the skyline seen from each site in aurora.sites.ts. For each azimuth +// it marches outward over the Copernicus DEM GLO-30 surface model (terrain, +// trees and buildings), optionally adds OpenStreetMap building heights, and +// keeps the highest elevation angle, allowing for Earth curvature and standard +// refraction. Where the site looks over water it also records the far shore, +// so the page can draw the water below it. +// Writes apps/site/src/scripts/skylines/.ts. +// +// Run: node scripts/build-skyline.mjs [site id ...] (default: all sites) +// Terrain: Copernicus DEM GLO-30 © DLR e.V. 2010–2014 and © Airbus Defence and +// Space GmbH 2014–2018, provided under COPERNICUS by the European Union and ESA. +// Buildings: © OpenStreetMap contributors (ODbL). + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fromFile } from "geotiff"; +import { SITES } from "../apps/site/src/scripts/aurora.sites.ts"; + +// Degrees either side of the camera heading, and the azimuth step. City +// sites are sampled finer so a dome a few tenths of a degree wide keeps its +// shape; their wider views need less span. +const spanFor = (site) => (site.buildings ? 75 : 100); +const stepFor = (site) => (site.buildings ? 0.05 : 0.2); +const FAR = 80_000; +const BUILDINGS_FAR = 12_000; // OSM buildings beyond this are below a pixel +const LAND = 1.5; // metres above sea level that count as land, not water +const NO_SHORE = -40; // degrees: the strip runs below the frame +const R_EARTH = 6_371_000; +const R_EFF = (R_EARTH * 7) / 6; // standard refraction +const OUT = new URL("../apps/site/src/scripts/skylines/", import.meta.url); +const DEG = 180 / Math.PI; + +const cache = join(tmpdir(), "northguild-dem"); +mkdirSync(cache, { recursive: true }); +mkdirSync(OUT, { recursive: true }); + +const tileName = (lat, lon) => + `${lat < 0 ? "S" : "N"}${String(Math.abs(lat)).padStart(2, "0")}_00_` + + `${lon < 0 ? "W" : "E"}${String(Math.abs(lon)).padStart(3, "0")}_00`; + +async function load(tile) { + const name = `Copernicus_DSM_COG_10_${tile}_DEM`; + const file = join(cache, `${name}.tif`); + if (!existsSync(file)) { + const res = await fetch( + `https://copernicus-dem-30m.s3.amazonaws.com/${name}/${name}.tif`, + ); + // Open sea has no tile: treat it as sea level. + if (res.status === 404) return null; + if (!res.ok) throw new Error(`${tile}: HTTP ${res.status}`); + writeFileSync(file, Buffer.from(await res.arrayBuffer())); + } + const image = await (await fromFile(file)).getImage(); + const [west, south, east, north] = image.getBoundingBox(); + const [data] = await image.readRasters(); + return { + west, + south, + east, + north, + width: image.getWidth(), + height: image.getHeight(), + data, + }; +} + +const OVERPASS = [ + "https://overpass-api.de/api/interpreter", + "https://overpass.kumi.systems/api/interpreter", +]; + +async function overpass(query) { + for (let attempt = 0; attempt < 6; attempt++) { + const url = OVERPASS[attempt % OVERPASS.length]; + try { + const res = await fetch(url, { + method: "POST", + headers: { "User-Agent": "northguild-skyline/1.0" }, + body: new URLSearchParams({ data: query }), + }); + const text = await res.text(); + if (res.ok && text.startsWith("{")) return text; + console.warn(`overpass ${url}: HTTP ${res.status}, retrying`); + } catch (error) { + console.warn(`overpass ${url}: ${error.message}, retrying`); + } + await new Promise((done) => setTimeout(done, 5000 * 2 ** attempt)); + } + throw new Error("overpass: no server answered"); +} + +const num = (v) => Number.parseFloat(v); + +/** + * OSM buildings and building parts (Simple 3D Buildings) with height, roof + * height and roof shape, cached per bounding box. + */ +async function osmBuildings([south, west, north, east]) { + const file = join(cache, `osm3d-${south}-${west}-${north}-${east}.json`); + if (!existsSync(file)) { + const box = `(${south},${west},${north},${east})`; + writeFileSync( + file, + await overpass( + `[out:json][timeout:180];(way["building"]${box};way["building:part"]${box};);out tags geom;`, + ), + ); + } + const { elements } = JSON.parse(readFileSync(file, "utf8")); + return elements + .filter((e) => e.geometry?.length > 2) + .map((e) => { + const t = e.tags ?? {}; + const levels = num(t["building:levels"]); + const roofLevels = num(t["roof:levels"]); + const roofFromLevels = Number.isFinite(roofLevels) ? roofLevels * 3.2 : 0; + // In Simple 3D Buildings `height` is to the top of the roof. + let height = num(t.height); + if (!Number.isFinite(height)) { + height = Number.isFinite(levels) + ? levels * 3.2 + 1 + roofFromLevels + : 10; + } + let roof = num(t["roof:height"]); + if (!Number.isFinite(roof)) roof = roofFromLevels; + return { + part: "building:part" in t, + height, + roof: Math.min(Math.max(roof, 0), height), + shape: t["roof:shape"] ?? "flat", + points: e.geometry, + }; + }); +} + +/** Roof profile across its width, u from -1 to 1: 1 at the full height. */ +function roofProfile(shape, u) { + const a = Math.min(1, Math.abs(u)); + switch (shape) { + case "dome": + case "round": + return Math.sqrt(1 - a * a); + case "onion": + return 0.8 * Math.sqrt(1 - a * a) + 0.2 * Math.max(0, 1 - 4 * a); + case "cone": + case "pyramidal": + return 1 - a; + case "flat": + return 1; + default: + // Gabled, hipped and the rest: the side seen depends on orientation. + return 0.6; + } +} + +/** Whether point [x, y] is inside polygon pts (local metres). */ +function inside([x, y], pts) { + let hit = false; + for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) { + const [xi, yi] = pts[i]; + const [xj, yj] = pts[j]; + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) { + hit = !hit; + } + } + return hit; +} + +async function build(site) { + const SPAN = spanFor(site); + const AZ_STEP = stepFor(site); + const latRad = site.lat / DEG; + const dLat = (FAR / R_EARTH) * DEG; + const dLon = dLat / Math.cos(latRad); + const names = []; + for ( + let la = Math.floor(site.lat - dLat); + la <= Math.floor(site.lat + dLat); + la++ + ) { + for ( + let lo = Math.floor(site.lon - dLon); + lo <= Math.floor(site.lon + dLon); + lo++ + ) { + names.push(tileName(la, lo)); + } + } + const tiles = (await Promise.all(names.map(load))).filter(Boolean); + + const height = (lat, lon) => { + for (const t of tiles) { + if (lon < t.west || lon >= t.east || lat <= t.south || lat > t.north) { + continue; + } + const x = ((lon - t.west) / (t.east - t.west)) * t.width - 0.5; + const y = ((t.north - lat) / (t.north - t.south)) * t.height - 0.5; + const x0 = Math.max(0, Math.min(t.width - 2, Math.floor(x))); + const y0 = Math.max(0, Math.min(t.height - 2, Math.floor(y))); + const fx = Math.min(1, Math.max(0, x - x0)); + const fy = Math.min(1, Math.max(0, y - y0)); + const at = (i, j) => t.data[(y0 + j) * t.width + x0 + i]; + const top = at(0, 0) * (1 - fx) + at(1, 0) * fx; + const bottom = at(0, 1) * (1 - fx) + at(1, 1) * fx; + return Math.max(0, top * (1 - fy) + bottom * fy); + } + return 0; + }; + + // Local metres east (x) and north (y) of the viewer. + const toLocal = (lat, lon) => [ + ((lon - site.lon) / DEG) * R_EARTH * Math.cos(latRad), + ((lat - site.lat) / DEG) * R_EARTH, + ]; + + const from = site.heading - SPAN; + const count = Math.round((2 * SPAN) / AZ_STEP) + 1; + // On a pier the eye height is above the sea; the model there may show a building. + const h0 = site.water ? site.eye : height(site.lat, site.lon) + site.eye; + const angleTo = (top, r) => Math.atan2(top - h0 - (r * r) / (2 * R_EFF), r); + // Over water the viewer's own pier may be in the model; start just past it. + const near = site.water ? 15 : 300; + + const best = new Float64Array(count).fill(-Infinity); + const bestR = new Float64Array(count).fill(FAR); + const shore = new Float64Array(count).fill(NO_SHORE / DEG); + + for (let i = 0; i < count; i++) { + const az = (from + i * AZ_STEP) / DEG; + let landAt = 0; + // Step grows with distance: 15 m near, about 130 m far. + for (let r = near; r <= FAR; r += Math.max(15, r / 600)) { + const lat = site.lat + (r * Math.cos(az) * DEG) / R_EARTH; + const lon = + site.lon + (r * Math.sin(az) * DEG) / (R_EARTH * Math.cos(latRad)); + const z = height(lat, lon); + if (!landAt && z > LAND) landAt = r; + const angle = angleTo(z, r); + if (angle > best[i]) { + best[i] = angle; + bestR[i] = r; + } + } + // Over water the strip stops at the first land, however near: the + // island's own shore curves down toward the frame. + if (site.water && landAt) shore[i] = angleTo(0, landAt); + } + + if (site.buildings) { + const box = [ + site.lat - 0.11, + site.lon - 0.22, + site.lat + 0.11, + site.lon + 0.22, + ].map((v) => Math.round(v * 1000) / 1000); + const all = (await osmBuildings(box)).map((b) => ({ + ...b, + local: b.points.map((p) => toLocal(p.lat, p.lon)), + })); + const parts = all.filter((b) => b.part); + // Where a building is modelled in parts, draw the parts, not its outline. + const centres = parts.map(({ local }) => [ + local.reduce((sum, [x]) => sum + x, 0) / local.length, + local.reduce((sum, [, y]) => sum + y, 0) / local.length, + ]); + const GRID = 200; + const cell = ([x, y]) => `${Math.floor(x / GRID)},${Math.floor(y / GRID)}`; + const byCell = new Map(); + centres.forEach((c) => { + const key = cell(c); + byCell.set(key, [...(byCell.get(key) ?? []), c]); + }); + const hasParts = (local) => { + const xs = local.map(([x]) => x); + const ys = local.map(([, y]) => y); + for ( + let gx = Math.floor(Math.min(...xs) / GRID); + gx <= Math.floor(Math.max(...xs) / GRID); + gx++ + ) { + for ( + let gy = Math.floor(Math.min(...ys) / GRID); + gy <= Math.floor(Math.max(...ys) / GRID); + gy++ + ) { + for (const c of byCell.get(`${gx},${gy}`) ?? []) + if (inside(c, local)) return true; + } + } + return false; + }; + const shapes = [ + ...parts, + ...all.filter((b) => !b.part && !hasParts(b.local)), + ]; + let used = 0; + for (const { height: tall, roof, shape, points, local } of shapes) { + const r = Math.min(...local.map(([x, y]) => Math.hypot(x, y))); + if (r < 50 || r > BUILDINGS_FAR) continue; + // Ground under the building: the lowest surface-model value at its corners. + const ground = Math.min(...points.map((p) => height(p.lat, p.lon))); + const azs = local.map(([x, y]) => Math.atan2(x, y) * DEG); + const lo = Math.min(...azs); + const hi = Math.max(...azs); + if (hi - lo > 90) continue; // straddles the bearing behind the viewer + const mid = (lo + hi) / 2; + const half = Math.max((hi - lo) / 2, 1e-6); + const first = Math.max(0, Math.ceil((lo - from) / AZ_STEP)); + const last = Math.min(count - 1, Math.floor((hi - from) / AZ_STEP)); + // A part narrower than one step still marks its nearest bin. + const bins = + first <= last + ? [first, last] + : [ + Math.round((mid - from) / AZ_STEP), + Math.round((mid - from) / AZ_STEP), + ]; + for ( + let i = Math.max(0, bins[0]); + i <= Math.min(count - 1, bins[1]); + i++ + ) { + const u = (from + i * AZ_STEP - mid) / half; + const angle = angleTo( + ground + tall - roof + roof * roofProfile(shape, u), + r, + ); + if (angle > best[i]) { + best[i] = angle; + bestR[i] = r; + } + } + used++; + } + console.log( + `${site.id}: ${used} OSM buildings and parts within ${BUILDINGS_FAR / 1000} km (${parts.length} parts)`, + ); + } + + const elev = Int16Array.from(best, (a) => Math.round(a * DEG * 100)); + const dist = Uint16Array.from(bestR, (r) => + Math.min(65535, Math.round(r / 10)), + ); + const base = Int16Array.from(shore, (a) => Math.round(a * DEG * 100)); + + const b64 = (a) => Buffer.from(a.buffer).toString("base64"); + writeFileSync( + new URL(`${site.id}.ts`, OUT), + `// Generated by scripts/build-skyline.mjs. Do not edit. +// ${site.name}: ${site.lat} N, ${site.lon} E, eye ${site.eye} m above ground (${h0.toFixed(1)} m). +// elev: Int16 little-endian, hundredths of a degree above the horizontal. +// dist: Uint16 little-endian, tens of metres to the ridge. +// base: Int16, hundredths of a degree: the far shore over water, or ${NO_SHORE}°. +export const SKYLINE = { + from: ${from}, + step: ${AZ_STEP}, + elev: "${b64(elev)}", + dist: "${b64(dist)}", + base: "${b64(base)}", +}; +`, + ); + + const deg = Array.from(elev, (v) => v / 100); + console.log( + `${site.id}: ground ${h0.toFixed(1)} m, ${tiles.length} tiles, ${count} azimuths, ` + + `elevation ${Math.min(...deg)}° to ${Math.max(...deg)}°`, + ); + for (let az = from; az <= from + 2 * SPAN; az += 10) { + const i = Math.round((az - from) / AZ_STEP); + console.log( + ` ${String(az).padStart(4)}° ${deg[i].toFixed(2).padStart(6)}° ` + + `${(dist[i] / 100).toFixed(1).padStart(5)} km shore ${(base[i] / 100).toFixed(2)}°`, + ); + } +} + +const wanted = process.argv.slice(2); +for (const site of SITES) { + if (!wanted.length || wanted.includes(site.id)) await build(site); +} diff --git a/scripts/build-stars.mjs b/scripts/build-stars.mjs new file mode 100644 index 0000000..5dc3794 --- /dev/null +++ b/scripts/build-stars.mjs @@ -0,0 +1,58 @@ +// Builds apps/site/public/stars.bin from the Yale Bright Star Catalogue, +// 5th revised edition (Hoffleit & Warren 1991, CDS V/50): naked-eye stars +// (V <= 5.5), J2000 positions. Precession to the current date happens at +// runtime in the aurora worker. +// +// Run: node scripts/build-stars.mjs +// Record, 6 bytes little-endian: ra uint16 (0..65536 = 0..360°), +// dec int16 (±32767 = ±90°), vmag uint8 ((V + 2) × 20), b-v int8 (× 50). + +import { writeFileSync } from "node:fs"; +import { gunzipSync } from "node:zlib"; + +const SOURCE = "http://tdc-www.harvard.edu/catalogs/bsc5.dat.gz"; +const LIMIT = 5.5; +const OUT = new URL("../apps/site/public/stars.bin", import.meta.url); + +const res = await fetch(SOURCE); +if (!res.ok) throw new Error(`catalogue: HTTP ${res.status}`); +const text = gunzipSync(Buffer.from(await res.arrayBuffer())).toString( + "latin1", +); + +// Byte ranges from the catalogue ReadMe (1-based, inclusive). +const field = (line, from, to) => line.slice(from - 1, to).trim(); + +const stars = []; +for (const line of text.split("\n")) { + const raH = field(line, 76, 77); + const vmag = field(line, 103, 107); + // Blank coordinates are entries removed from the catalogue (novae, clusters). + if (!raH || !vmag) continue; + const v = Number(vmag); + if (v > LIMIT) continue; + const ra = + (Number(raH) + + Number(field(line, 78, 79)) / 60 + + Number(field(line, 80, 83)) / 3600) * + 15; + const sign = field(line, 84, 84) === "-" ? -1 : 1; + const dec = + sign * + (Number(field(line, 85, 86)) + + Number(field(line, 87, 88)) / 60 + + Number(field(line, 89, 90)) / 3600); + const bv = Number(field(line, 110, 114) || "0.6"); + stars.push({ ra, dec, v, bv }); +} + +const buf = Buffer.alloc(stars.length * 6); +stars.forEach(({ ra, dec, v, bv }, i) => { + const o = i * 6; + buf.writeUInt16LE(Math.round((ra / 360) * 65536) % 65536, o); + buf.writeInt16LE(Math.round((dec / 90) * 32767), o + 2); + buf.writeUInt8(Math.max(0, Math.min(255, Math.round((v + 2) * 20))), o + 4); + buf.writeInt8(Math.max(-127, Math.min(127, Math.round(bv * 50))), o + 5); +}); +writeFileSync(OUT, buf); +console.log(`${stars.length} stars, ${buf.length} bytes`);