Apex Universe: the ecosystem as a live star system, below Overview

A new section, "Apex Universe", directly under Overview:
- Hero: a 20 s Blender flythrough (universe/apex-universe.mp4, poster.jpg) behind the title,
  with "Enter the universe" and "Watch the flythrough".
- The sandbox (universe/universe.js, three.js via the page's import map): AP3X is the star,
  PRIME, Vector and Nexus orbit it, PRIME's stake pools are its asteroid belt, the staking
  reserve is a moon, agents fly as a fleet round Vector with the HAL 8 mothership, Reactor,
  Skyline and LayerZero are jump gates, Cardano, Base and BNB Chain are distant worlds, partners
  are beacons on the outer belt. Inspector with links into the hub's sections, search, layer
  toggles, time controls, a five-chapter tour, a no-WebGL fallback, reduced motion honoured.
- Tour chapters, a field guide and the partner wall, all with official logos.
- universe/data.json holds the figures; a daily job on the desk host updates it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 18:02:22 -05:00
parent 44dcf31f5a
commit f8e464cad8
70 changed files with 657 additions and 1 deletions

View File

@@ -23,6 +23,36 @@ with the hub's own. The same content is also kept as a standalone page,
file and refold it rather than editing the section by hand. Third-party marks file and refold it rather than editing the section by hand. Third-party marks
are the owners' official files. are the owners' official files.
## Apex Universe section
"Apex Universe" (menu entry right below Overview, section id `universe`,
component `UniverseSection`) is the ecosystem as a star system drawn from
public data. AP3X is the star, with a corona of the nine public allocations.
PRIME, Vector and Nexus are its worlds. The stake pools form PRIME's asteroid
belt, and the staking reserve is a moon filled to what is left. Agents fly as
a fleet round Vector with the HAL 8 mothership. The bridges are jump gates,
Cardano, Base and BNB Chain are distant worlds, the partners are beacons on
the outer belt, and comets carry value home.
- The markup (`AU_HTML`) and the component live in the bundle between
`/*AU-BEGIN*/` and `/*AU-END*/`. The styles are in the `<style>` block marked
`au-css-begin`, with every class namespaced `au-`.
- `universe/` holds everything else:
- `universe.js`, the three.js sandbox, which the component loads on demand
through the page's import map;
- `data.json`, the figures;
- the Blender film (`apex-universe.mp4`) and its `poster.jpg`;
- the Blender chapter frames and the sky panorama;
- the baked world textures;
- the logos.
- **Figures refresh daily.** A cron job on the desk host (trading-bot)
publishes the desk's public ecosystem snapshot to `universe/data.json`
through the Gitea API. It commits only when the figures have changed, and
that commit is the deploy.
- **Sources:** the build script, sandbox module and Blender scene live in
`jerry/exec-engine` under `docs/pages/ecosystem/universe/`. Rebuild with
`build_site.py`, which replaces the injected block rather than duplicating it.
## How it is served ## How it is served
Caddy on the HAL 8 host serves this directory as a static site. The box Caddy on the HAL 8 host serves this directory as a static site. The box

File diff suppressed because one or more lines are too long

BIN
universe/apex-universe.mp4 Normal file

Binary file not shown.

1
universe/data.json Normal file

File diff suppressed because one or more lines are too long

BIN
universe/fleet.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

BIN
universe/gate.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
universe/logos/ap3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
universe/logos/base.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
universe/logos/bnb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
universe/logos/cap3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
universe/logos/cardano.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
universe/logos/cstap3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
universe/logos/hal8-eye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
universe/logos/kash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
universe/logos/lbank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
universe/logos/merkl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
universe/logos/minswap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
universe/logos/nexus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
universe/logos/pnsol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
universe/logos/prime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
universe/logos/skyline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
universe/logos/stargate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
universe/logos/uniswap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
universe/logos/usdc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
universe/logos/vector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
universe/poster.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
universe/prime.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
universe/sky.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

BIN
universe/star.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
universe/system.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
universe/tex/base.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

BIN
universe/tex/bsc.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

BIN
universe/tex/cardano.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

BIN
universe/tex/nexus.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

BIN
universe/tex/prime.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

BIN
universe/tex/vector.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

625
universe/universe.js Normal file
View File

@@ -0,0 +1,625 @@
// Apex Universe: the live sandbox for apex-fusion.odyssey-works.io (section "Apex Universe").
// A star system drawn from public data: AP3X is the star; PRIME, Vector and Nexus are worlds;
// PRIME's 333 stake pools are its asteroid belt; the staking reserve is a moon filled to what is
// left; agents fly as a fleet round Vector; bridges are jump gates; Cardano, Base and BNB Chain
// are distant worlds; partners are beacons on the outer belt; comets bring value home.
// Figures come from universe/data.json (refreshed daily) with the embedded snapshot as fallback.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
const COL = {
bg: 0x050b09, ink: 0xecf4ee, ink2: 0xbfd4c7, emerald: 0x4bb87a, emeraldSoft: 0x7fd5a1, pine: 0x077368,
vermilion: 0xf25041, ember: 0xe0623e, ignition: 0xf2a03c, orange: 0xf27b50, plasma: 0xffe0b8,
azure: 0x5ba8e0, violet: 0x8b7fd5, gold: 0xf0b90b, eye: 0xff2a1a,
};
const hex = c => '#' + c.toString(16).padStart(6, '0');
const fmt = (v, d = 0) => v == null || isNaN(v) ? '—' : Number(v).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
const M = (v, d = 1) => v == null || isNaN(v) ? '—' : Math.abs(v) >= 1e9 ? fmt(v / 1e9, 2) + 'B' : Math.abs(v) >= 1e6 ? fmt(v / 1e6, d) + 'M' : Math.abs(v) >= 1e3 ? fmt(v / 1e3, d) + 'k' : fmt(v, 0);
const usd = v => v == null ? '—' : '$' + M(v);
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
// Blender (z up) -> three (y up), so the live scene and the film share one layout
const B = (x, y, z) => new THREE.Vector3(x, z, -y);
// partners on the outer belt, grouped by what they do (logo file, name, role, link)
const PARTNERS = [
['Research', COL.violet, [['well-typed', 'Well-Typed', 'Ouroboros and protocol engineering behind Vector', 'https://well-typed.com'],
['pnsol', 'PNSol', 'Network performance science', 'https://pnsol.com'], ['ensurable', 'Ensurable Systems', 'Formal methods and consensus research', 'https://ensurable.systems'],
['20squares', '20squares', 'Game theory and mechanism design', 'https://20squares.xyz'], ['txpipe', 'TxPipe', 'Open-source Cardano tooling; Vector validators', 'https://txpipe.io']]],
['Tooling & infrastructure', COL.azure, [['zeeve', 'Zeeve', 'Enterprise blockchain infrastructure', 'https://www.zeeve.io'],
['tachyon', 'Tachyon', 'Exchange and matching-engine technology', 'https://tachyon.ae'], ['mlabs', 'MLabs', 'High-assurance Haskell and Cardano engineering', 'https://mlabs.city'],
['anastasia-labs', 'Anastasia Labs', 'Smart-contract audits and development', 'https://anastasialabs.com']]],
['Wallets', COL.emerald, [['eternl', 'Eternl', 'Cardano light wallet with AP3X on PRIME', 'https://eternl.io'],
['gero', 'GeroWallet', 'Wallet for Cardano, Midnight and Apex Fusion', 'https://gerowallet.io'], ['route3', 'Route3', 'Mobile wallet listed for AP3X', 'https://www.route3.co']]],
['Studios', COL.violet, [['mvp-workshop', 'MVP Workshop', 'Web3 product studio', 'https://mvpworkshop.co'],
['thespian', 'Thespian', 'Software development and consulting', 'https://thespian.eu'], ['t-minus-one', 'T Minus One', 'Swiss IT consulting and engineering', 'https://tminusone.pro']]],
['Games & media', COL.orange, [['dega', 'DEGA', 'Tools for Web3 game developers', 'https://www.dega.org'],
['cornucopias', 'Cornucopias', 'Open-world MMO', 'https://www.cornucopias.io'], ['mandala', 'The Mandala Project', 'A Conjunction Studios project', 'https://www.enterthemandala.com'],
['the-fuse', 'The Fuse', 'Video-first Web3 news', 'https://th3fus3.com']]],
];
export async function mount(root, opts = {}) {
const base = opts.base || 'universe/';
const $ = sel => root.querySelector(sel);
const stage = $('.au-stage');
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
// ------------------------------------------------------------------ data
let D = {};
try { D = JSON.parse(($('#au-data') || {}).textContent || '{}'); } catch (e) { D = {}; }
try {
const r = await fetch(base + 'data.json', { cache: 'no-cache' });
if (r.ok) D = await r.json();
} catch (e) { /* the embedded snapshot stands */ }
const P = D.pools || {}, BR = D.bridges || {}, R = D.rewards || {}, A = D.agents || {}, MK = D.markets || {};
fillFigures(root, D);
// ------------------------------------------------------------------ hero film
const video = $('.au-hero-video');
if (video) {
if (reduced) { video.removeAttribute('autoplay'); video.pause(); }
root.querySelectorAll('[data-au="watch"]').forEach(b => b.addEventListener('click', () => {
video.currentTime = 0; video.muted = true; video.play().catch(() => {});
(video.requestFullscreen || video.webkitRequestFullscreen || (() => {})).call(video);
}));
}
root.querySelectorAll('[data-au="enter"]').forEach(b => b.addEventListener('click', () => {
stage.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'center' });
boot();
}));
// the 3D scene boots when the stage first comes into view (the hub stays light until then)
let api = null;
const io = new IntersectionObserver(es => { if (es.some(e => e.isIntersecting)) boot(); }, { rootMargin: '200px' });
io.observe(stage);
function boot() {
if (api) return;
try { api = sandbox(); } catch (e) { console.warn('[Apex Universe] 3D unavailable:', e); stage.classList.add('au-nogl'); api = { destroy() {} }; }
}
// ------------------------------------------------------------------ the sandbox
function sandbox() {
const host = $('.au-canvas');
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(host.clientWidth, host.clientHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
renderer.localClippingEnabled = true;
host.appendChild(renderer.domElement);
const labels = new CSS2DRenderer();
labels.setSize(host.clientWidth, host.clientHeight);
labels.domElement.className = 'au-labels3d';
host.appendChild(labels.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(COL.bg);
const camera = new THREE.PerspectiveCamera(40, host.clientWidth / host.clientHeight, 0.1, 2000);
const HOME = { pos: B(5, -60, 29), at: B(3, 0, 0) };
camera.position.copy(HOME.pos);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.copy(HOME.at);
controls.enableDamping = true; controls.dampingFactor = 0.06;
controls.minDistance = 3; controls.maxDistance = 220;
controls.autoRotate = !reduced; controls.autoRotateSpeed = 0.18;
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(host.clientWidth, host.clientHeight), 0.6, 0.45, 0.32);
composer.addPass(bloom);
composer.addPass(new OutputPass());
const loader = new THREE.TextureLoader();
const tex = (f, srgb = true) => { const t = loader.load(base + f); if (srgb) t.colorSpace = THREE.SRGBColorSpace; return t; };
const glowTex = (() => { const c = document.createElement('canvas'); c.width = c.height = 128; const g = c.getContext('2d');
const r = g.createRadialGradient(64, 64, 0, 64, 64, 64); r.addColorStop(0, 'rgba(255,255,255,1)'); r.addColorStop(.2, 'rgba(255,255,255,.7)'); r.addColorStop(.5, 'rgba(255,255,255,.14)'); r.addColorStop(1, 'rgba(255,255,255,0)');
g.fillStyle = r; g.fillRect(0, 0, 128, 128); const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t; })();
const pickables = [];
const groups = { orbits: [], lanes: [], beacons: [], labels: [], comets: [], ships: [] };
const addTo = (g, o) => { groups[g].push(o); return o; };
// sky: the Blender-rendered panorama when present, a procedural field otherwise
loader.load(base + 'sky.jpg', t => { t.mapping = THREE.EquirectangularReflectionMapping; t.colorSpace = THREE.SRGBColorSpace; scene.background = t; }, undefined, () => {
const n = 5000, a = new Float32Array(n * 3);
for (let i = 0; i < n; i++) { const u = Math.random() * 2 - 1, t = Math.random() * Math.PI * 2, r = 700; const s = Math.sqrt(1 - u * u); a.set([r * s * Math.cos(t), r * u, r * s * Math.sin(t)], i * 3); }
const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.BufferAttribute(a, 3));
scene.add(new THREE.Points(g, new THREE.PointsMaterial({ color: 0xeaf2ff, size: 1.3, sizeAttenuation: false, transparent: true, opacity: .8 })));
});
scene.add(new THREE.AmbientLight(0x6f8f86, 0.35));
// ---------------------------------------------------------------- helpers
const lineLoop = (r, color, opacity, center = new THREE.Vector3(), tilt = null, seg = 256) => {
const pts = []; for (let i = 0; i <= seg; i++) { const t = i / seg * Math.PI * 2; pts.push(new THREE.Vector3(r * Math.cos(t), 0, r * Math.sin(t))); }
const l = new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), new THREE.LineBasicMaterial({ color, transparent: true, opacity, depthWrite: false }));
l.position.copy(center); if (tilt) l.rotation.copy(tilt); return l;
};
const fresnel = (color, power = 3.0, strength = 1.4) => new THREE.ShaderMaterial({
uniforms: { c: { value: new THREE.Color(color) }, p: { value: power }, s: { value: strength } },
vertexShader: 'varying vec3 vN; varying vec3 vV; void main(){ vec4 mv = modelViewMatrix*vec4(position,1.); vN = normalize(normalMatrix*normal); vV = normalize(-mv.xyz); gl_Position = projectionMatrix*mv; }',
fragmentShader: 'uniform vec3 c; uniform float p; uniform float s; varying vec3 vN; varying vec3 vV; void main(){ float f = pow(1.0-abs(dot(vN,vV)), p); gl_FragColor = vec4(c*s, f); }',
transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.FrontSide,
});
const noiseTex = (c1, c2, c3, seed = 1, bands = 0) => {
const w = 512, h = 256, cv = document.createElement('canvas'); cv.width = w; cv.height = h; const g = cv.getContext('2d'); const img = g.createImageData(w, h);
const A = new THREE.Color(c1), Bc = new THREE.Color(c2), C = new THREE.Color(c3);
const n = (x, y) => { const s = Math.sin(x * 12.9898 + y * 78.233 + seed * 3.7) * 43758.5453; return s - Math.floor(s); };
const sm = (x, y) => { const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi, u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
return (n(xi, yi) * (1 - u) + n(xi + 1, yi) * u) * (1 - v) + (n(xi, yi + 1) * (1 - u) + n(xi + 1, yi + 1) * u) * v; };
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
let f = 0, a = .5, fr = 4 / w * 8; for (let o = 0; o < 5; o++) { f += a * sm(x * fr * (o + 1) * 1.9, (y * (bands ? .25 : 1)) * fr * (o + 1) * 1.9); a *= .5; }
const col = f < .5 ? A.clone().lerp(Bc, f * 2) : Bc.clone().lerp(C, (f - .5) * 2);
const i = (y * w + x) * 4; img.data[i] = col.r * 255; img.data[i + 1] = col.g * 255; img.data[i + 2] = col.b * 255; img.data[i + 3] = 255;
}
g.putImageData(img, 0, 0); const t = new THREE.CanvasTexture(cv); t.colorSpace = THREE.SRGBColorSpace; return t;
};
const planetMat = (key, c1, c2, c3, seed, bands) => {
const m = new THREE.MeshStandardMaterial({ map: noiseTex(c1, c2, c3, seed, bands), roughness: .85, metalness: 0 });
loader.load(base + 'tex/' + key + '.jpg', t => { t.colorSpace = THREE.SRGBColorSpace; m.map = t; m.needsUpdate = true; }, undefined, () => {});
return m;
};
// logo badges: a dark disc, a thin coloured rim and the logo, drawn once to a canvas
const imgCache = {};
const loadImg = f => imgCache[f] || (imgCache[f] = new Promise(res => { const im = new Image(); im.onload = () => res(im); im.onerror = () => res(null); im.src = base + 'logos/' + f + '.png'; }));
const badgeTex = (logo, ring, bare = false) => {
const c = document.createElement('canvas'); c.width = c.height = 192; const g = c.getContext('2d');
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;
if (!bare) { g.beginPath(); g.arc(96, 96, 88, 0, Math.PI * 2); g.fillStyle = 'rgba(8,18,14,.9)'; g.fill(); g.lineWidth = 5; g.strokeStyle = hex(ring); g.stroke(); }
loadImg(logo).then(im => { if (!im) return; const box = bare ? 184 : 118; const k = Math.min(box / im.width, box / im.height); const w = im.width * k, h = im.height * k;
g.drawImage(im, 96 - w / 2, 96 - h / 2, w, h); t.needsUpdate = true; });
return t;
};
const badge = (info, logo, ring, size, bare = false, group = null) => {
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: badgeTex(logo, ring, bare), transparent: true, depthWrite: false, sizeAttenuation: false }));
s.scale.set(size, size, 1); s.renderOrder = 10; s.userData.info = info; pickables.push(s); scene.add(s);
if (group) addTo(group, s);
return s;
};
const label = (text, sub, obj, cls = '') => {
const el = document.createElement('div'); el.className = 'au-l3 ' + cls; el.innerHTML = esc(text) + (sub ? `<small>${esc(sub)}</small>` : '');
const o = new CSS2DObject(el); o.center.set(0.5, -0.9); obj.add(o); addTo('labels', o); return o;
};
const hit = (obj, info) => { obj.userData.info = info; pickables.push(obj); return obj; };
// ---------------------------------------------------------------- the star
const STAR = B(0, 8, 0);
const starMat = new THREE.ShaderMaterial({
uniforms: { t: { value: 0 } },
vertexShader: 'varying vec3 vP; varying vec3 vN; varying vec3 vV; void main(){ vP = position; vec4 mv = modelViewMatrix*vec4(position,1.); vN = normalize(normalMatrix*normal); vV = normalize(-mv.xyz); gl_Position = projectionMatrix*mv; }',
fragmentShader: `uniform float t; varying vec3 vP; varying vec3 vN; varying vec3 vV;
float h(vec3 p){ return fract(sin(dot(p, vec3(12.9898,78.233,45.164)))*43758.5453); }
float n(vec3 p){ vec3 i=floor(p), f=fract(p); f=f*f*(3.-2.*f); return mix(mix(mix(h(i),h(i+vec3(1,0,0)),f.x),mix(h(i+vec3(0,1,0)),h(i+vec3(1,1,0)),f.x),f.y),mix(mix(h(i+vec3(0,0,1)),h(i+vec3(1,0,1)),f.x),mix(h(i+vec3(0,1,1)),h(i+vec3(1,1,1)),f.x),f.y),f.z); }
void main(){ vec3 p = vP*1.6 + vec3(t*.05, t*.03, 0.); float f = n(p)*.5 + n(p*2.1)*.3 + n(p*4.3)*.2;
vec3 col = mix(vec3(1.0,.55,.18), vec3(1.0,.93,.78), smoothstep(.35,.75,f)); float rim = pow(1.-abs(dot(vN,vV)), 2.);
gl_FragColor = vec4(col*(2.2 + rim*1.5), 1.); }`,
});
const star = hit(new THREE.Mesh(new THREE.SphereGeometry(2.4, 64, 32), starMat), { id: 'ap3x' });
star.position.copy(STAR); scene.add(star);
const halo = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: COL.ignition, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, opacity: .9 }));
halo.scale.set(18, 18, 1); halo.position.copy(STAR); scene.add(halo);
const sun = new THREE.PointLight(0xffe8cc, 2600, 0, 1.6); sun.position.copy(STAR); scene.add(sun);
// corona crown: the nine public allocations
const crown = new THREE.Group(); crown.position.copy(STAR); crown.rotation.set(THREE.MathUtils.degToRad(-18), 0, THREE.MathUtils.degToRad(8)); scene.add(crown);
let a0 = -Math.PI / 2;
(D.genesis || []).forEach((g, i) => {
const span = Math.PI * 2 * (g.pct || 0) / 100;
const arc = new THREE.Mesh(new THREE.TorusGeometry(4.3, i ? .06 : .09, 8, 96, span - .05), new THREE.MeshBasicMaterial({ color: [COL.plasma, COL.ignition, COL.orange][i % 3] }));
arc.rotation.z = a0 + .025; crown.add(arc); a0 += span;
hit(arc, { id: 'alloc', name: g.label, fig: `${M(g.ap3x, 0)} AP3X · ${g.pct}%`, text: 'One of the nine allocations of the fixed 3B supply in the public tokenomics; unlocks are linear.' });
});
// ---------------------------------------------------------------- worlds on their orbits
const W = {
prime: { ang: 215, r: 15, size: 2.2, c: ['#0B3B2C', '#077368', '#7FD5A1'], atmo: COL.emerald, period: 600 },
vector: { ang: 262, r: 22, size: 1.6, c: ['#8A2416', '#F25041', '#FFE0B8'], atmo: COL.vermilion, period: 1000 },
nexus: { ang: 318, r: 29, size: 1.8, c: ['#3B1F06', '#F27B50', '#FFD9A0'], atmo: COL.ignition, period: 1500, bands: 1 },
};
Object.entries(W).forEach(([k, w], i) => {
w.group = new THREE.Group(); scene.add(w.group);
w.mesh = hit(new THREE.Mesh(new THREE.SphereGeometry(w.size, 64, 32), planetMat(k, ...w.c, i + 1, w.bands)), { id: k });
w.group.add(w.mesh);
const atmo = new THREE.Mesh(new THREE.SphereGeometry(w.size * 1.07, 48, 24), fresnel(w.atmo, 3.2, 1.6)); w.group.add(atmo);
addTo('orbits', lineLoop(w.r, w.atmo, .28, STAR));
scene.add(groups.orbits[groups.orbits.length - 1]);
});
const worldPos = (k, t) => { const w = W[k]; const ang = THREE.MathUtils.degToRad(w.ang) + t * Math.PI * 2 / w.period; return STAR.clone().add(B(w.r * Math.cos(ang), w.r * Math.sin(ang), 0)); };
// PRIME's asteroid belt: every registered stake pool (glowing = carries stake, size ~ stake)
const items = P.items || [];
const smax = Math.max(...items.map(p => p.s), 1);
const belt = new THREE.Group(); belt.rotation.set(THREE.MathUtils.degToRad(-18), 0, THREE.MathUtils.degToRad(-8)); W.prime.group.add(belt);
const rockGeo = (() => { const g = new THREE.IcosahedronGeometry(1, 1); const p = g.attributes.position; for (let i = 0; i < p.count; i++) { const k = .75 + .45 * ((Math.sin(i * 91.7) * 43758.5) % 1 + 1) % 1; p.setXYZ(i, p.getX(i) * k, p.getY(i) * k, p.getZ(i) * k); } g.computeVertexNormals(); return g; })();
const live = items.filter(p => p.a), dark = items.filter(p => !p.a);
const beltLive = new THREE.InstancedMesh(rockGeo, new THREE.MeshStandardMaterial({ color: 0x2a3a33, emissive: COL.emeraldSoft, emissiveIntensity: 1.3, roughness: .8 }), live.length);
const beltDark = new THREE.InstancedMesh(rockGeo, new THREE.MeshStandardMaterial({ color: 0x3a403d, roughness: .95 }), Math.max(dark.length, 1));
const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), e = new THREE.Euler(), s3 = new THREE.Vector3(), v3 = new THREE.Vector3();
const golden = Math.PI * (3 - Math.sqrt(5));
let li = 0, di = 0;
items.forEach((p, i) => {
const r = 3.4 + 2.0 * ((i * 0.618034) % 1), t = i * golden * 7.3;
v3.set(r * Math.cos(t), (((i * 0.381966) % 1) - .5) * .22, r * Math.sin(t));
e.set(i, i * 2, i * 3); q.setFromEuler(e);
const sz = p.a ? .04 + .13 * Math.sqrt(p.s / smax) : .03; s3.set(sz, sz, sz);
m4.compose(v3, q, s3);
if (p.a) { beltLive.setMatrixAt(li, m4); beltLive.userData[li++] = p; } else { beltDark.setMatrixAt(di, m4); beltDark.userData[di++] = p; }
});
beltDark.count = di; belt.add(beltLive, beltDark); hit(beltLive, { id: 'pool' }); hit(beltDark, { id: 'pool' });
[3.3, 5.5].forEach(r => belt.add(lineLoop(r, COL.emerald, .18)));
// the Reserve moon: glass, filled to what is left of the staking reserve
const MR = .72, fill = Math.max(0, Math.min(1, (R.reserve_left || 600e6) / 600e6));
const moon = new THREE.Group(); W.prime.group.add(moon);
const glass = hit(new THREE.Mesh(new THREE.SphereGeometry(MR, 48, 24), fresnel(COL.ink, 2.2, 1.3)), { id: 'moon' }); moon.add(glass);
let lo = -MR, hi = MR, hgt = 0; for (let k = 0; k < 40; k++) { hgt = (lo + hi) / 2; const cap = Math.PI * (MR + hgt) ** 2 * (3 * MR - (MR + hgt)) / 3 / (4 / 3 * Math.PI * MR ** 3); if (cap < fill) lo = hgt; else hi = hgt; }
const clip = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0);
const liquid = new THREE.Mesh(new THREE.SphereGeometry(MR * .94, 48, 24), new THREE.MeshBasicMaterial({ color: COL.emeraldSoft, transparent: true, opacity: .85, clippingPlanes: [clip] })); moon.add(liquid);
const menis = new THREE.Mesh(new THREE.CircleGeometry(Math.sqrt(Math.max(0, (MR * .94) ** 2 - (hgt * .94) ** 2)), 48), new THREE.MeshBasicMaterial({ color: COL.emeraldSoft, transparent: true, opacity: .95, side: THREE.DoubleSide }));
menis.rotation.x = -Math.PI / 2; menis.position.y = hgt * .94; moon.add(menis);
const pulse = new THREE.Mesh(new THREE.RingGeometry(.9, 1.0, 64), new THREE.MeshBasicMaterial({ color: COL.emeraldSoft, transparent: true, opacity: 0, side: THREE.DoubleSide, depthWrite: false }));
pulse.rotation.x = -Math.PI / 2; W.prime.group.add(pulse);
// the Foundation's ring station and LBank's dock in PRIME orbit
const hull = c => new THREE.MeshStandardMaterial({ color: c, metalness: .85, roughness: .3 });
const station = new THREE.Group(); W.prime.group.add(station);
station.add(new THREE.Mesh(new THREE.TorusGeometry(.62, .05, 12, 64), hull(0xc9d6cf)));
station.add(new THREE.Mesh(new THREE.CylinderGeometry(.14, .14, .5, 16), hull(0xc9d6cf)));
for (let i = 0; i < 4; i++) { const sp = new THREE.Mesh(new THREE.BoxGeometry(.62, .03, .03), hull(0xc9d6cf)); sp.rotation.y = i * Math.PI / 4; station.add(sp); }
station.add(new THREE.Mesh(new THREE.TorusGeometry(.66, .012, 6, 96), new THREE.MeshBasicMaterial({ color: COL.emeraldSoft })));
station.rotation.x = 1.2; hit(station, { id: 'foundation' });
const dock = new THREE.Group(); W.prime.group.add(dock);
dock.add(new THREE.Mesh(new THREE.BoxGeometry(.9, .24, .34), hull(0x8fa39a)));
[-1, 1].forEach(sx => { const pn = new THREE.Mesh(new THREE.BoxGeometry(.8, .02, .36), new THREE.MeshStandardMaterial({ color: 0x1c3a52, emissive: 0x0b2236, metalness: .4 })); pn.position.x = sx * .9; dock.add(pn); });
hit(dock, { id: 'lbank' });
// ---------------------------------------------------------------- gates
const gate = (id, color, locked, size) => {
const g = new THREE.Group(); scene.add(g);
g.add(new THREE.Mesh(new THREE.TorusGeometry(size, .09, 12, 96), hull(0x7e918a)));
for (let i = 0; i < 8; i++) { const c = new THREE.Mesh(new THREE.BoxGeometry(.16, .3, .26), hull(0x7e918a)); c.position.set(Math.cos(i * Math.PI / 4) * size * 1.06, Math.sin(i * Math.PI / 4) * size * 1.06, 0); c.rotation.z = i * Math.PI / 4; g.add(c); }
g.add(new THREE.Mesh(new THREE.TorusGeometry(size * .97, .03, 8, 128), new THREE.MeshBasicMaterial({ color })));
const portalMat = new THREE.ShaderMaterial({ uniforms: { t: { value: 0 }, c: { value: new THREE.Color(color) } }, transparent: true, depthWrite: false, side: THREE.DoubleSide, blending: THREE.AdditiveBlending,
vertexShader: 'varying vec2 vU; void main(){ vU = uv; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.); }',
fragmentShader: 'uniform float t; uniform vec3 c; varying vec2 vU; void main(){ vec2 p = vU-.5; float r = length(p)*2.; float a = atan(p.y,p.x); float s = .5+.5*sin(a*3.+r*9.-t*2.); float f = smoothstep(1.,.2,r)*(.35+.65*s); gl_FragColor = vec4(c*f*1.6, f*.85); }' });
const portal = new THREE.Mesh(new THREE.CircleGeometry(size * .95, 64), portalMat); g.add(portal);
const core = new THREE.Mesh(new THREE.SphereGeometry(.1 + .4 * Math.sqrt(Math.max(locked || 0, 0) / 120e6), 24, 12), new THREE.MeshBasicMaterial({ color: COL.plasma })); g.add(core);
hit(portal, { id }); return { g, portal, mat: portalMat };
};
const GATES = { reactor: gate('reactor', COL.plasma, BR.reactor, 1.25), layerzero: gate('layerzero', COL.ignition, BR.layerzero, .95), skyline: gate('skyline', COL.azure, BR.skyline, 1.1) };
GATES.skyline.g.position.copy(STAR.clone().add(B(-31, -8, 1.5)));
// ---------------------------------------------------------------- distant worlds, venues as moons
const FAR = {
cardano: { pos: STAR.clone().add(B(-40, 10, -4)), size: 3.0, c: ['#0B1E4A', '#2C5CC5', '#CFE1FF'], atmo: COL.azure },
base: { pos: STAR.clone().add(B(39, 14, 5)), size: 2.8, c: ['#06123A', '#0052FF', '#9DB9FF'], atmo: COL.azure, bands: 1 },
bsc: { pos: STAR.clone().add(B(44, -6, -2)), size: 1.7, c: ['#3A2A00', '#F0B90B', '#FFF2B0'], atmo: COL.gold },
};
Object.entries(FAR).forEach(([k, w], i) => {
w.mesh = hit(new THREE.Mesh(new THREE.SphereGeometry(w.size, 64, 32), planetMat(k, ...w.c, 7 + i, w.bands)), { id: k });
w.mesh.position.copy(w.pos); scene.add(w.mesh);
const at = new THREE.Mesh(new THREE.SphereGeometry(w.size * 1.06, 48, 24), fresnel(w.atmo, 3.0, 1.4)); at.position.copy(w.pos); scene.add(at);
});
const ringTilt = new THREE.Euler(THREE.MathUtils.degToRad(70 - 90), THREE.MathUtils.degToRad(12), 0);
const depth = MK.base_depth_usd || 0;
for (let k = 0; k < 9; k++) addTo('orbits', scene.add(lineLoop(3.7 + k * (0.06 + Math.sqrt(depth) / 9000), COL.azure, .35 + .04 * k, FAR.base.pos, ringTilt)) && scene.children[scene.children.length - 1]);
// ---------------------------------------------------------------- Vector's fleet and the HAL 8 mothership
const nShips = Math.min(A.identities || 36, 2000), nLive = A.production || 7;
const shipGeo = new THREE.ConeGeometry(.05, .16, 3); shipGeo.rotateX(Math.PI / 2);
const fleet = new THREE.InstancedMesh(shipGeo, new THREE.MeshStandardMaterial({ color: 0xb9c9c1, metalness: .7, roughness: .35, emissive: 0x223029 }), nShips);
const orbitsF = [];
for (let i = 0; i < nShips; i++) orbitsF.push({ r: 2.5 + 2.0 * ((i * 0.618034) % 1), ph: i * golden * 3.1, inc: THREE.MathUtils.degToRad(-25 + 50 * ((i * 0.381966) % 1)), node: i * .122, w: .9 / (2.5 + 2.0 * ((i * 0.618034) % 1)), live: i < nLive });
fleet.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(nShips * 3), 3);
const cLive = new THREE.Color(COL.emeraldSoft).multiplyScalar(3), cShip = new THREE.Color(0xb9c9c1);
orbitsF.forEach((o, i) => fleet.setColorAt(i, o.live ? cLive : cShip));
W.vector.group.add(fleet); hit(fleet, { id: 'fleet' }); addTo('ships', fleet);
const ms = new THREE.Group(); W.vector.group.add(ms);
const msb = new THREE.Mesh(new THREE.BoxGeometry(.5, .34, 2.4), hull(0xd5ddd9)); ms.add(msb);
const msn = new THREE.Mesh(new THREE.ConeGeometry(.26, .8, 6), hull(0xd5ddd9)); msn.rotation.x = Math.PI / 2; msn.position.z = 1.55; ms.add(msn);
[-1, 1].forEach(sx => { const wg = new THREE.Mesh(new THREE.BoxGeometry(.7, .06, .9), hull(0xd5ddd9)); wg.position.set(sx * .55, 0, -.6); ms.add(wg);
const en = new THREE.Mesh(new THREE.SphereGeometry(.11, 16, 8), new THREE.MeshBasicMaterial({ color: 0xff3b24 })); en.position.set(sx * .18, 0, -1.3); ms.add(en); });
const msRing = new THREE.Mesh(new THREE.TorusGeometry(.72, .025, 8, 96), new THREE.MeshBasicMaterial({ color: COL.eye })); msRing.position.z = -.3; ms.add(msRing);
const eye = new THREE.Mesh(new THREE.SphereGeometry(.14, 16, 8), new THREE.MeshBasicMaterial({ color: COL.eye })); eye.position.set(0, .2, 1.05); ms.add(eye);
hit(msb, { id: 'hal8' });
ms.position.copy(B(-3.4, -3.0, 1.3)); ms.lookAt(new THREE.Vector3());
// ---------------------------------------------------------------- comets: value coming home
const comets = [];
Object.entries(BR.back_30d || {}).sort().forEach(([src, amount], i) => {
const g = new THREE.Group(); scene.add(g); addTo('comets', g);
const head = hit(new THREE.Mesh(new THREE.SphereGeometry(.18, 16, 8), new THREE.MeshBasicMaterial({ color: COL.plasma })), { id: 'comet', src, amount }); g.add(head);
const tail = new THREE.Mesh(new THREE.ConeGeometry(.55, 5 + i * 1.5, 24, 1, true), new THREE.ShaderMaterial({ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide,
uniforms: { c: { value: new THREE.Color(src === 'skyline' ? COL.azure : COL.plasma) } }, vertexShader: 'varying float vY; void main(){ vY = uv.y; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.); }',
fragmentShader: 'uniform vec3 c; varying float vY; void main(){ gl_FragColor = vec4(c*1.4, pow(vY,1.6)*.8); }' }));
tail.userData.len = 5 + i * 1.5; g.add(tail);
comets.push({ g, tail, ph: i * 2.1, src });
});
// ---------------------------------------------------------------- the outer belt and its beacons
{ const n = 1600, arr = new Float32Array(n * 3); for (let i = 0; i < n; i++) { const t = Math.random() * Math.PI * 2, r = 47 + (Math.random() - .5) * 3; arr.set([STAR.x + r * Math.cos(t), (Math.random() - .5) * .8, STAR.z + r * Math.sin(t)], i * 3); }
const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.BufferAttribute(arr, 3));
scene.add(new THREE.Points(g, new THREE.PointsMaterial({ color: COL.ink2, size: .12, map: glowTex, transparent: true, depthWrite: false, opacity: .8 }))); }
const beaconList = [];
{ const total = PARTNERS.reduce((a, g) => a + g[2].length, 0) + PARTNERS.length - 1; let slot = 0;
PARTNERS.forEach(([grp, col, list]) => { list.forEach(([logo, name, role, link]) => {
const t = THREE.MathUtils.degToRad(28 + (152 - 28) * slot / (total - 1));
const b = badge({ id: 'partner', name, role, link, grp, logo }, 'partners/' + logo, col, .034, false, 'beacons');
b.position.copy(STAR.clone().add(B(47 * Math.cos(t), 47 * Math.sin(t), 2.2)));
beaconList.push(b); slot++; }); slot++; }); }
// ---------------------------------------------------------------- badges on the named bodies
const T1 = .05, T2 = .04, T3 = .03;
const BADGES = [];
const attach = (id, logo, ring, size, follow, offset, name, sub, bare = false) => {
const b = badge({ id }, logo, ring, size, bare);
BADGES.push({ b, follow, offset }); if (name) label(name, sub, b);
return b;
};
attach('ap3x', 'ap3x', COL.ignition, T1, () => STAR, B(0, 0, 5.4), 'AP3X', '3B · fixed');
attach('prime', 'prime', COL.emerald, T1, () => W.prime.group.position, B(0, 0, 2.9), 'PRIME', `${fmt(P.registered)} pools`);
attach('vector', 'vector', COL.vermilion, T1, () => W.vector.group.position, B(0, 0, 4.6), 'Vector', `${fmt(A.identities)} agents`);
attach('nexus', 'nexus', COL.ignition, T1, () => W.nexus.group.position, B(0, 0, 2.7), 'Nexus', 'EVM');
attach('hal8', 'hal8-eye', COL.eye, T2 * 1.3, () => W.vector.group.position.clone().add(ms.position), B(0, 0, 1.7), 'HAL 8', 'mothership', true);
attach('foundation', 'apex-fusion', COL.emerald, T2, () => station.getWorldPosition(new THREE.Vector3()), B(0, 0, 1.2), 'Foundation', 'InFusion');
attach('lbank', 'lbank', COL.azure, T2, () => dock.getWorldPosition(new THREE.Vector3()), B(0, 0, 1.2), 'LBank', 'AP3X/USDT');
attach('moon', 'ap3x', COL.emeraldSoft, T3, () => moon.getWorldPosition(new THREE.Vector3()), B(0, 0, 1.3), 'Reserve', `${Math.round(fill * 100)}% full`);
attach('reactor', 'apex-fusion-white', COL.plasma, T2, () => GATES.reactor.g.position, B(0, 0, 2.0), 'Reactor', `${M(BR.reactor)} locked`);
attach('ethernal', 'partners/ethernal', COL.plasma, T3, () => GATES.reactor.g.position, B(1.8, 0.4, 2.2));
attach('layerzero', 'layerzero-white', COL.ignition, T2, () => GATES.layerzero.g.position, B(0, 0, 1.8), 'LayerZero', `${M(BR.layerzero)} locked`);
attach('skyline', 'skyline', COL.azure, T2, () => GATES.skyline.g.position, B(0, 0, 2.0), 'Skyline', `${M(BR.skyline)} locked`);
attach('cardano', 'cardano', COL.azure, T1, () => FAR.cardano.pos, B(0, 0, 4.2), 'Cardano', 'cAP3X');
attach('base', 'base', COL.azure, T1, () => FAR.base.pos, B(0, 0, 4.0), 'Base', 'bAP3X');
attach('bsc', 'bnb', COL.gold, T1, () => FAR.bsc.pos, B(0, 0, 2.6), 'BNB Chain', 'bnAP3X');
attach('usdc', 'usdc', COL.azure, T3, () => W.nexus.group.position, B(2.2, -1.6, 1.3));
attach('stargate', 'stargate', COL.ignition, T3, () => W.nexus.group.position, B(3.2, -0.4, 0.3));
attach('tenderly', 'partners/tenderly', COL.ignition, T3, () => W.nexus.group.position, B(-1.8, -2.2, 1.6));
attach('origintrail', 'origintrail', COL.vermilion, T3, () => W.vector.group.position, B(3.8, -1.6, 1.2));
attach('kash', 'kash', COL.vermilion, T3, () => W.vector.group.position, B(2.8, 2.6, -0.6));
[['aerodrome', 'aerodrome'], ['uniswap', 'uniswap'], ['quickswap', 'quickswap'], ['merkl', 'merkl']].forEach(([id, lg], i) => { const t = THREE.MathUtils.degToRad(200 + i * 42);
attach('venue:' + id, lg, COL.azure, T3, () => FAR.base.pos, B(4.8 * Math.cos(t), 4.8 * Math.sin(t), -1.4 + i * .5)); });
[['cap3x', 'cap3x'], ['cstap3x', 'cstap3x'], ['minswap', 'minswap'], ['vyfi', 'partners/vyfi'], ['steelswap', 'partners/steelswap'], ['charli3', 'partners/charli3']].forEach(([id, lg], i) => { const t = THREE.MathUtils.degToRad(-60 + i * 36);
attach('venue:' + id, lg, COL.azure, T3, () => FAR.cardano.pos, B(5.2 * Math.cos(t), 5.2 * Math.sin(t), -1 + i * .45)); });
// ---------------------------------------------------------------- lanes: particles on curves between moving ends
const lanes = [];
const lane = (id, color, value, ends, fixedN = null) => {
const n = fixedN ?? (value > 0 ? Math.round(Math.max(14, Math.min(160, 14 + 30 * (Math.log10(value) - 3)))) : 0);
const geo = new THREE.BufferGeometry(); const lg = new THREE.BufferGeometry();
const pts = new THREE.Points(geo, new THREE.PointsMaterial({ color, size: .34, map: glowTex, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(Math.max(n, 1) * 3), 3));
const line = new THREE.Line(lg, new THREE.LineBasicMaterial({ color, transparent: true, opacity: .28, depthWrite: false }));
scene.add(pts, line); addTo('lanes', pts); addTo('lanes', line);
const L = { id, n, ends, pts, line, phase: Float32Array.from({ length: n }, (_, i) => (i + Math.random() * .6) / Math.max(n, 1)) };
lanes.push(L); return L;
};
const out = BR.out_30d || {};
const vOf = k => W[k].group.position;
lane('rewards', COL.emeraldSoft, R.per_epoch, () => { const m = moon.getWorldPosition(new THREE.Vector3()), p = vOf('prime'); return [m, m.clone().lerp(p, .3).add(new THREE.Vector3(0, 1.2, 0)), p.clone().add(m.clone().sub(p).normalize().multiplyScalar(3.8))]; });
lane('infusion', COL.emerald, 168e6, () => { const s = station.getWorldPosition(new THREE.Vector3()), p = vOf('prime'); return [s, s.clone().lerp(p, .4).add(new THREE.Vector3(0, .8, 0)), p.clone().add(s.clone().sub(p).normalize().multiplyScalar(4.2))]; });
lane('prime_reactor', COL.plasma, 5e5, () => [vOf('prime'), vOf('prime').clone().lerp(GATES.reactor.g.position, .5).add(new THREE.Vector3(0, 1.4, 0)), GATES.reactor.g.position]);
lane('reactor_vector', COL.vermilion, (out.vector || 0) + 2e5, () => [GATES.reactor.g.position, GATES.reactor.g.position.clone().lerp(vOf('vector'), .5).add(new THREE.Vector3(0, 1, 0)), vOf('vector')]);
lane('reactor_nexus', COL.ignition, out.nexus, () => [GATES.reactor.g.position, GATES.reactor.g.position.clone().lerp(vOf('nexus'), .5).add(new THREE.Vector3(0, 3.5, 0)), vOf('nexus')]);
lane('nexus_lz', COL.ignition, 1e5, () => [vOf('nexus'), vOf('nexus').clone().lerp(GATES.layerzero.g.position, .5).add(new THREE.Vector3(0, .8, 0)), GATES.layerzero.g.position]);
lane('lz_base', COL.ignition, BR.layerzero, () => [GATES.layerzero.g.position, GATES.layerzero.g.position.clone().lerp(FAR.base.pos, .5).add(new THREE.Vector3(0, 4, 0)), FAR.base.pos]);
lane('lz_bsc', COL.gold, 0, () => [GATES.layerzero.g.position, GATES.layerzero.g.position.clone().lerp(FAR.bsc.pos, .5).add(new THREE.Vector3(0, 1.5, 0)), FAR.bsc.pos], 0);
lane('prime_skyline', COL.azure, 3e5, () => [vOf('prime'), vOf('prime').clone().lerp(GATES.skyline.g.position, .5).add(new THREE.Vector3(0, 2, 0)), GATES.skyline.g.position]);
lane('sky_cardano', COL.azure, out.cardano, () => [GATES.skyline.g.position, GATES.skyline.g.position.clone().lerp(FAR.cardano.pos, .5).add(new THREE.Vector3(0, 2, 0)), FAR.cardano.pos]);
lane('serve', COL.eye, null, () => { const a = W.vector.group.position.clone().add(ms.position); return [a, a.clone().lerp(vOf('vector'), .5).add(new THREE.Vector3(0, 1.2, 0)), vOf('vector')]; }, 18);
const curve = new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]);
// ---------------------------------------------------------------- the inspector and its catalogue
const Pc = P.registered, Pa = P.active;
const INFO = {
ap3x: () => ({ name: 'AP3X', kind: 'The star', logo: 'ap3x', rows: [['Supply', '3,000,000,000 · fixed'], ['Staking reserve', `${M(R.reserve_left)} left`], ['Price', D.price_usd ? `$${fmt(D.price_usd, 4)}` : '—']], text: 'The native coin, minted only on PRIME. Its corona is the nine public allocations.', go: 'Markets' }),
prime: () => ({ name: 'PRIME', kind: 'Anchor world', logo: 'prime', rows: [['Asteroid belt', `${Pc} pools · ${Pa} producing`], ['Mass in the belt', `${M(P.active_stake)} AP3X staked`], ['Reserve moon', `${M(R.reserve_left)} of 600M left`], ['Falls each epoch', `${M(R.per_epoch)} AP3X`], ['Network yield', R.yield_network ? `${(R.yield_network * 100).toFixed(1)}% a year` : '—']], text: 'A Cardano-derived proof-of-stake L1 and the only issuer of AP3X. Stake is never locked.', go: 'Staking' }),
vector: () => ({ name: 'Vector', kind: 'Agent world', logo: 'vector', rows: [['Agent identities', fmt(A.identities)], ['Commercial agents', `${fmt(A.production)} (${fmt(A.via_hal8)} via HAL 8)`], ['Escrow transactions', fmt(A.escrow_txs)], ['Arrived in 30 days', `${M(out.vector)} AP3X`]], text: 'The settlement layer for AI agents: bonded escrow, staked reputation, disputes, signed receipts.', go: 'Agent Stack' }),
nexus: () => ({ name: 'Nexus', kind: 'EVM world', logo: 'nexus', rows: [['Arrived in 30 days', `${M(out.nexus)} AP3X`], ['LayerZero lockbox', `${M(BR.layerzero)} AP3X`], ['Chain id', '9069']], text: 'The EVM chain with AP3X as gas; home of the Reputation System and the gateway to Base and BNB Chain.', go: 'Developers' }),
moon: () => ({ name: 'Staking reserve', kind: 'The moon', logo: 'ap3x', rows: [['Still inside', `${M(R.reserve_left)} of 600M`], ['Paid last epoch', `${M(R.per_epoch)} AP3X`], ['Emitted so far', `${M(R.emitted)} AP3X`]], text: 'Filled to what is left. Each epoch a pulse of rewards falls into the belt.', go: 'Staking' }),
pool: p => ({ name: p.t || 'Stake pool', kind: 'Asteroid · stake pool', logo: 'prime', rows: [['Stake', `${M(p.s)} AP3X`], ['Delegations', fmt(p.d)], ['Saturation', `${fmt(p.sat, 1)}%`], ['Blocks made', fmt(p.b)]], text: p.a ? 'Glowing: this pool carries stake and makes blocks.' : 'Dark rock: registered, without stake.', go: 'Staking' }),
foundation: () => ({ name: 'Apex Fusion Foundation', kind: 'Ring station', logo: 'apex-fusion', rows: [['InFusion', '720k / 480k AP3X per pool'], ['By design', `${M((D.infusion || {}).max_ap3x, 0)} AP3X`], ['Share of supply', '13.33%']], text: 'The Swiss foundation that stewards the protocol and issues AP3X.', go: 'Staking' }),
lbank: () => ({ name: 'LBank', kind: 'Trading dock', logo: 'lbank', rows: [['Pair', 'AP3X / USDT'], ['Last', MK.lbank ? `$${fmt(MK.lbank.last, 4)}` : '—'], ['Top-5 depth', MK.lbank ? usd(MK.lbank.depth_usd) : '—']], text: 'The centralised market; deposits arrive as native PRIME AP3X.', go: 'Exchanges' }),
reactor: () => ({ name: 'Reactor', kind: 'Jump gate', logo: 'apex-fusion-white', rows: [['Locked on PRIME', `${M(BR.reactor)} AP3X`], ['To Nexus, 30 days', `${M(out.nexus)} AP3X`], ['To Vector, 30 days', `${M(out.vector)} AP3X`], ['Built by', 'Ethernal']], text: 'The native bridge between PRIME, Vector and Nexus.', go: 'Bridges' }),
layerzero: () => ({ name: 'LayerZero', kind: 'Jump gate', logo: 'layerzero-white', rows: [['Lockbox on Nexus', `${M(BR.layerzero)} AP3X`], ['Out to', 'Base (bAP3X) · BNB Chain (bnAP3X)']], text: 'The OFT standard: every bAP3X and bnAP3X is minted against AP3X locked here.', go: 'Bridges' }),
skyline: () => ({ name: 'Skyline', kind: 'Jump gate', logo: 'skyline', rows: [['Locked on PRIME', `${M(BR.skyline)} AP3X`], ['To Cardano, 30 days', `${M(out.cardano)} AP3X`], ['Built by', 'Ethernal']], text: 'Carries AP3X to Cardano as cAP3X, redeemable 1:1.', go: 'Bridges' }),
hal8: () => ({ name: 'HAL 8', kind: 'Mothership', logo: 'hal8-eye', rows: [['Models on Vector', fmt(A.via_hal8)], ['Built', 'The Apex Fusion blockchain']], text: 'The Abu Dhabi AI lab whose inference platform serves models to agents on the Vector marketplace.', go: 'HAL 8' }),
fleet: () => ({ name: 'Agents on Vector', kind: 'The fleet', logo: 'vector', rows: [['Identities', fmt(A.identities)], ['Commercial (lit)', fmt(A.production)], ['Escrow transactions', fmt(A.escrow_txs)], ['MCP tools', fmt(A.mcp_tools)]], text: (A.pilot || '') + '.', go: 'Agent Stack' }),
cardano: () => ({ name: 'Cardano', kind: 'Distant world', logo: 'cardano', rows: [['Via', 'Skyline'], ['Forms', 'cAP3X · cstAP3X'], ['Pools', usd(MK.cardano_depth_usd)]], text: 'Reached through Skyline; staked through VyFi.', go: 'Bridges' }),
base: () => ({ name: 'Base', kind: 'Distant world', logo: 'base', rows: [['bAP3X pools', usd(MK.base_depth_usd)], ['24h volume', usd(MK.base_vol24_usd)], ['Venues', 'Aerodrome · Uniswap · QuickSwap']], text: 'Home of bAP3X and its main DEX liquidity; its rings widen with depth.', go: 'Markets' }),
bsc: () => ({ name: 'BNB Chain', kind: 'Distant world', logo: 'bnb', rows: [['Form', 'bnAP3X'], ['Liquidity', 'none yet']], text: 'The same OFT as bAP3X, deployed without a pool.', go: 'Exchanges' }),
comet: o => ({ name: 'Value coming home', kind: 'Comet', logo: 'ap3x', rows: [['Released by', o.src === 'skyline' ? 'Skyline' : 'Reactor'], ['Last 30 days', `${M(o.amount)} AP3X`]], text: 'AP3X released back to PRIME from the other chains.', go: 'Bridges' }),
alloc: o => ({ name: o.name, kind: 'Allocation', logo: 'ap3x', rows: [['Size', o.fig]], text: o.text, go: 'Overview' }),
partner: o => ({ name: o.name, kind: o.grp, logo: 'partners/' + o.logo, rows: [], text: o.role, link: o.link }),
};
const VENUES = { aerodrome: ['Aerodrome', 'bAP3X/USDC on Base'], uniswap: ['Uniswap', 'bAP3X/AERO v4 on Base'], quickswap: ['QuickSwap', 'bAP3X/cbBTC on Base'], merkl: ['Merkl', 'Incentives on the Base launch'],
cap3x: ['cAP3X', 'AP3X on Cardano via Skyline'], cstap3x: ['cstAP3X', 'Staked cAP3X via VyFi'], minswap: ['Minswap', 'cAP3X/ADA'], vyfi: ['VyFinance', 'ADA/cstAP3X; the Bar'], steelswap: ['Steelswap', 'Cardano DEX aggregator'], charli3: ['Charli3', "Cardano's oracle"],
usdc: ['USDC.e', 'Stargate USDC on Nexus'], stargate: ['Stargate', 'The stablecoin rail to Nexus'], tenderly: ['Tenderly', 'EVM developer platform on Nexus'], origintrail: ['OriginTrail', 'Holds the agent pilot knowledge graph'], kash: ['KASH', 'Vector utility token'], ethernal: ['Ethernal', 'Built Reactor and Skyline'] };
const infoFor = u => {
if (!u) return null;
if (u.id && u.id.startsWith('venue:')) { const k = u.id.slice(6); const [n, t] = VENUES[k] || [k, '']; return { name: n, kind: 'Venue', logo: k === 'vyfi' || k === 'steelswap' || k === 'charli3' ? 'partners/' + k : k, rows: [], text: t, go: 'Exchanges' }; }
if (VENUES[u.id]) { const [n, t] = VENUES[u.id]; return { name: n, kind: 'Partner', logo: u.id === 'tenderly' || u.id === 'ethernal' ? 'partners/' + u.id : u.id, rows: [], text: t }; }
const f = INFO[u.id]; return f ? f(u.pool || u) : null;
};
const insp = $('.au-insp');
const showInsp = (u, pick) => {
const i = infoFor(u); if (!i) return;
insp.innerHTML = `<button class="au-x" aria-label="Close">×</button><div class="au-insp-hd"><img src="${base}logos/${i.logo}.png" alt=""><div><div class="eyebrow">${esc(i.kind || '')}</div><div class="serif au-insp-name">${esc(i.name)}</div></div></div>` +
(i.rows.length ? `<div class="au-insp-rows">${i.rows.map(([k, v]) => `<div><span>${esc(k)}</span><b>${esc(v)}</b></div>`).join('')}</div>` : '') +
`<p class="au-insp-tx">${esc(i.text || '')}</p><div class="au-insp-actions">` +
(pick ? '<span class="au-btn au-btn-sm au-btn-primary" data-au="fly">Fly there</span>' : '') +
(i.go ? `<span class="au-btn au-btn-sm" data-au="open">Open ${esc(i.go)} &rarr;</span>` : '') +
(i.link ? `<a class="au-btn au-btn-sm" href="${esc(i.link)}" target="_blank" rel="noopener">Visit &rarr;</a>` : '') + '</div>';
insp.classList.add('on');
insp.querySelector('.au-x').onclick = () => insp.classList.remove('on');
const fb = insp.querySelector('[data-au="fly"]'); if (fb) fb.onclick = () => flyTo(pick);
const ob = insp.querySelector('[data-au="open"]'); if (ob) ob.onclick = () => openSection(i.go);
const cr = $('.au-crumb'); if (cr) cr.innerHTML = `AP3X system <b>&rsaquo;</b> ${esc(i.name)}`;
};
const openSection = name => {
const el = [...document.querySelectorAll('.nav-list button.nav-item')].find(n => (n.querySelector('.label') || n).textContent.trim().toLowerCase() === name.toLowerCase());
if (el) { el.click(); window.scrollTo({ top: 0 }); }
};
// ---------------------------------------------------------------- picking, hover, fly-to
const ray = new THREE.Raycaster(), mouse = new THREE.Vector2();
const tip = $('.au-tip');
const pick = ev => {
const r = renderer.domElement.getBoundingClientRect();
mouse.set(((ev.clientX - r.left) / r.width) * 2 - 1, -((ev.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(mouse, camera);
const h = ray.intersectObjects(pickables.filter(o => o.visible !== false), true)[0];
if (!h) return null;
const o = h.object;
if ((o === beltLive || o === beltDark) && h.instanceId != null) return { u: { id: 'pool', pool: o.userData[h.instanceId] }, obj: o, point: h.point };
let n = o; while (n && !(n.userData && n.userData.info)) n = n.parent;
return n ? { u: n.userData.info, obj: n, point: h.point } : null;
};
renderer.domElement.addEventListener('pointermove', ev => {
const h = pick(ev); renderer.domElement.style.cursor = h ? 'pointer' : 'grab';
if (!tip) return;
const i = h && infoFor(h.u); if (!i) { tip.hidden = true; return; }
tip.innerHTML = `<b>${esc(i.name)}</b><span>${esc(i.kind || '')}${i.rows[0] ? ' · ' + esc(i.rows[0][1]) : ''}</span>`;
const r = stage.getBoundingClientRect(); tip.style.left = (ev.clientX - r.left + 14) + 'px'; tip.style.top = (ev.clientY - r.top + 10) + 'px'; tip.hidden = false;
});
renderer.domElement.addEventListener('pointerleave', () => { if (tip) tip.hidden = true; });
let downAt = 0; renderer.domElement.addEventListener('pointerdown', () => { downAt = performance.now(); controls.autoRotate = false; stopTour(); });
renderer.domElement.addEventListener('click', ev => { if (performance.now() - downAt > 300) return; const h = pick(ev); if (h) { showInsp(h.u, h.point); flyTo(h.point); } });
let anim = null;
const ease = t => t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const flyTo = (point, dist = 9, ms = 1400) => {
if (!point) return;
const dir = camera.position.clone().sub(point).normalize();
const to = point.clone().add(dir.multiplyScalar(dist)).add(new THREE.Vector3(0, dist * .3, 0));
go(to, point.clone(), ms);
};
const go = (pos, at, ms) => { if (reduced) { camera.position.copy(pos); controls.target.copy(at); return; } anim = { p0: camera.position.clone(), t0: controls.target.clone(), p1: pos, t1: at, s: performance.now(), ms }; };
// ---------------------------------------------------------------- HUD: toggles, time, search, tour
root.querySelectorAll('.au-toggles .pill').forEach(p => p.addEventListener('click', () => {
p.classList.toggle('on'); const on = p.classList.contains('on'); const g = groups[p.dataset.g] || [];
g.forEach(o => { o.visible = on; if (o.element) o.element.style.display = on ? '' : 'none'; });
}));
let speed = reduced ? 0 : 1, simT = 0;
root.querySelectorAll('.au-time [data-sp]').forEach(b => b.addEventListener('click', () => {
speed = Number(b.dataset.sp); root.querySelectorAll('.au-time [data-sp]').forEach(x => x.classList.toggle('on', x === b));
}));
const ep = $('.au-epoch'), bar = $('.au-bar i');
const epochInfo = () => { const start = Date.UTC(2026, 8, 25, 17, 40), len = 5 * 864e5; const n = Math.floor((Date.now() - start) / len); const into = (Date.now() - start) / len - n; return { epoch: 173 + n, day: Math.floor(into * 5) + 1, frac: into }; };
const tickEpoch = () => { const e = epochInfo(); if (ep) ep.textContent = `Epoch ${e.epoch} · day ${e.day} of 5`; if (bar) bar.style.width = (e.frac * 100).toFixed(1) + '%'; };
tickEpoch(); const epTimer = setInterval(tickEpoch, 60000);
const search = $('.au-search input');
if (search) {
const names = { ap3x: 'AP3X', prime: 'PRIME', vector: 'Vector', nexus: 'Nexus', moon: 'Staking reserve', foundation: 'Apex Fusion Foundation', lbank: 'LBank', reactor: 'Reactor', layerzero: 'LayerZero', skyline: 'Skyline', hal8: 'HAL 8', fleet: 'Agents', cardano: 'Cardano', base: 'Base', bsc: 'BNB Chain' };
const dl = document.createElement('datalist'); dl.id = 'au-names';
dl.innerHTML = [...Object.values(names), ...PARTNERS.flatMap(g => g[2].map(x => x[1])), ...(P.items || []).slice(0, 400).map(p => p.t)].filter(Boolean).map(n => `<option value="${esc(n)}">`).join('');
root.appendChild(dl); search.setAttribute('list', 'au-names');
search.addEventListener('change', () => {
const v = search.value.trim().toLowerCase(); if (!v) return;
const id = Object.keys(names).find(k => names[k].toLowerCase() === v || k === v);
if (id) { const b = BADGES.find(x => x.b.userData.info.id === id); const p = b ? b.follow().clone() : null; showInsp({ id }, p); flyTo(p); return; }
const bc = beaconList.find(b => b.userData.info.name.toLowerCase() === v); if (bc) { showInsp(bc.userData.info, bc.position); flyTo(bc.position, 6); return; }
const pi = live.findIndex(p => (p.t || '').toLowerCase() === v);
if (pi >= 0) { const m = new THREE.Matrix4(); beltLive.getMatrixAt(pi, m); const p = new THREE.Vector3().setFromMatrixPosition(m).applyMatrix4(belt.matrixWorld); showInsp({ id: 'pool', pool: live[pi] }, p); flyTo(p, 3); }
});
}
// the tour: five chapters
const cap = $('.au-caption');
const CH = [
{ id: 'ap3x', k: 'The star', t: 'AP3X lights the system: 3 billion, fixed. Its corona is the nine public allocations.', at: () => STAR, d: 16 },
{ id: 'prime', k: 'PRIME and its belt', t: `${Pa} of ${Pc} pools glow in PRIME's belt, sized by stake. The Reserve moon is ${Math.round(fill * 100)}% full and pulses rewards into the belt each epoch.`, at: () => vOf('prime'), d: 13 },
{ id: 'fleet', k: 'The fleet at Vector', t: `${fmt(A.identities)} agent craft circle Vector; the ${fmt(A.production)} commercial ones are lit. HAL 8's mothership serves their models.`, at: () => vOf('vector'), d: 12 },
{ id: 'layerzero', k: 'Through the LayerZero gate', t: `${M(BR.layerzero)} AP3X locked on Nexus stands behind every bAP3X on Base, whose rings widen with its ${usd(MK.base_depth_usd)} of pools.`, at: () => GATES.layerzero.g.position, d: 14 },
{ k: 'The whole universe', t: 'Three worlds, three gates, three distant chains and twenty partners on the outer belt. Hover anything; click to fly in.', at: () => HOME.at, d: 0 },
];
let tour = null;
const stopTour = () => { if (!tour) return; clearTimeout(tour.tm); tour = null; if (cap) cap.classList.remove('on'); const b = $('[data-au="tour"]'); if (b) b.innerHTML = '&#9654;&nbsp; Tour &middot; 5 chapters'; };
const step = i => {
if (!tour) return; if (i >= CH.length) { stopTour(); controls.autoRotate = !reduced; return; }
const c = CH[i]; tour.i = i; if (c.id) showInsp({ id: c.id }, null);
if (c.d) flyTo(c.at().clone(), c.d, 2600); else go(HOME.pos.clone(), HOME.at.clone(), 2600);
if (cap) { cap.classList.remove('on'); setTimeout(() => { if (!tour) return; cap.innerHTML = `<span class="eyebrow">${String(i + 1).padStart(2, '0')} / 05 &middot; ${esc(c.k)}</span>${esc(c.t)}`; cap.classList.add('on'); }, 600); }
tour.tm = setTimeout(() => step(i + 1), 7600);
};
const startTour = (i = 0) => { stopTour(); controls.autoRotate = false; tour = { i }; const b = $('[data-au="tour"]'); if (b) b.innerHTML = '&#9632;&nbsp; Stop'; step(i); };
root.querySelectorAll('[data-au="tour"]').forEach(b => b.addEventListener('click', () => tour ? stopTour() : startTour(0)));
root.querySelectorAll('[data-chapter]').forEach(c => c.addEventListener('click', () => { stage.scrollIntoView({ behavior: 'smooth', block: 'center' }); startTour(Number(c.dataset.chapter)); }));
const resetBtn = $('[data-au="reset"]'); if (resetBtn) resetBtn.onclick = () => { stopTour(); go(HOME.pos.clone(), HOME.at.clone(), 1400); };
// ---------------------------------------------------------------- the loop
const place = () => {
Object.keys(W).forEach(k => { W[k].group.position.copy(worldPos(k, simT)); W[k].mesh.rotation.y = simT * .05; });
const p = vOf('prime'), out = p.clone().sub(STAR).normalize(), right = new THREE.Vector3(-out.z, 0, out.x);
moon.position.copy(right.clone().multiplyScalar(4.6).add(out.clone().multiplyScalar(1.2)).add(new THREE.Vector3(0, 1.6, 0)));
station.position.copy(right.clone().multiplyScalar(-3.9).add(out.clone().multiplyScalar(.6)).add(new THREE.Vector3(0, 2.6, 0)));
dock.position.copy(right.clone().multiplyScalar(-5.4).add(out.clone().multiplyScalar(2.2)).add(new THREE.Vector3(0, -.9, 0)));
pulse.position.copy(moon.position);
belt.rotation.y = simT * .04;
GATES.reactor.g.position.copy(p.clone().lerp(vOf('vector'), .45).add(new THREE.Vector3(0, 1.6, 0)));
GATES.reactor.g.lookAt(vOf('vector'));
const n = vOf('nexus'); const nOut = n.clone().sub(STAR).normalize();
GATES.layerzero.g.position.copy(n.clone().add(nOut.clone().multiplyScalar(3.4)).add(new THREE.Vector3(-nOut.z, 0, nOut.x).multiplyScalar(-3.2)).add(new THREE.Vector3(0, 1.6, 0)));
GATES.layerzero.g.lookAt(FAR.base.pos); GATES.skyline.g.lookAt(FAR.cardano.pos);
BADGES.forEach(x => x.b.position.copy(x.follow()).add(x.offset));
};
const updShips = () => {
const m = new THREE.Matrix4(), q = new THREE.Quaternion(), pos = new THREE.Vector3(), nxt = new THREE.Vector3(), up = new THREE.Vector3(0, 1, 0), sc = new THREE.Vector3();
orbitsF.forEach((o, i) => {
const a = o.ph + simT * o.w * 6, ci = Math.cos(o.inc), si = Math.sin(o.inc), cn = Math.cos(o.node), sn = Math.sin(o.node);
const f = (ang, v) => { const x = o.r * Math.cos(ang), z = o.r * Math.sin(ang); const y = z * si, zz = z * ci; v.set(x * cn - zz * sn, y, x * sn + zz * cn); return v; };
f(a, pos); f(a + .05, nxt); m.lookAt(nxt, pos, up); q.setFromRotationMatrix(m);
const k = o.live ? 2.2 : 1; sc.set(k, k, k); m.compose(pos, q, sc); fleet.setMatrixAt(i, m);
});
fleet.instanceMatrix.needsUpdate = true;
};
const updLanes = t => {
lanes.forEach(L => {
const [a, b, c] = L.ends(); curve.points[0].copy(a); curve.points[1].copy(b); curve.points[2].copy(c);
const pts = curve.getPoints(40); L.line.geometry.setFromPoints(pts);
if (!L.n) return; const arr = L.pts.geometry.attributes.position.array;
for (let i = 0; i < L.n; i++) { let u = (L.phase[i] + t * .05) % 1; const pp = curve.getPoint(u); arr[i * 3] = pp.x; arr[i * 3 + 1] = pp.y; arr[i * 3 + 2] = pp.z; }
L.pts.geometry.attributes.position.needsUpdate = true;
});
};
const updComets = t => comets.forEach((c, i) => {
const a = c.ph + t * .01; const head = STAR.clone().add(B(-22 + i * 12 + 6 * Math.cos(a), 30 + i * 4 + 4 * Math.sin(a), 16 + i * 3));
c.g.position.copy(head); const away = head.clone().sub(STAR).normalize();
c.tail.position.copy(away.clone().multiplyScalar(c.tail.userData.len / 2)); c.tail.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), away.clone().negate());
});
let lastEpochTick = 0;
const clock = new THREE.Clock();
let visible = true, raf = 0;
const vis = new IntersectionObserver(es => { visible = es[0].isIntersecting; }); vis.observe(stage);
const frame = () => {
raf = requestAnimationFrame(frame);
if (!visible || document.hidden) return;
const dt = Math.min(clock.getDelta(), .05); simT += dt * speed; const t = clock.elapsedTime;
if (anim) { const u = Math.min(1, (performance.now() - anim.s) / anim.ms), e = ease(u); camera.position.lerpVectors(anim.p0, anim.p1, e); controls.target.lerpVectors(anim.t0, anim.t1, e); if (u >= 1) anim = null; }
place(); updShips(); updLanes(reduced ? 0 : t * (speed || .2)); updComets(simT);
starMat.uniforms.t.value = t; Object.values(GATES).forEach(g => g.mat.uniforms.t.value = t);
clip.constant = hgt * .94; clip.normal.set(0, -1, 0); liquid.material.clippingPlanes[0].copy(new THREE.Plane(new THREE.Vector3(0, -1, 0), hgt * .94 + moon.getWorldPosition(new THREE.Vector3()).y));
if (speed && simT - lastEpochTick > 30) { lastEpochTick = simT; pulse.userData.p = 0; }
if (pulse.userData.p != null) { pulse.userData.p += dt * .8; const k = pulse.userData.p; pulse.scale.setScalar(1 + k * 5); pulse.material.opacity = Math.max(0, .8 - k); if (k > 1) pulse.userData.p = null; }
controls.update(); composer.render(); labels.render(scene, camera);
};
const resize = () => { const w = host.clientWidth, h = host.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); composer.setSize(w, h); labels.setSize(w, h); bloom.setSize(w, h); };
const ro = new ResizeObserver(resize); ro.observe(host);
place(); frame();
if (stage.clientWidth > 700) showInsp({ id: 'prime' }, null);
return { destroy() { cancelAnimationFrame(raf); ro.disconnect(); vis.disconnect(); clearInterval(epTimer); stopTour(); renderer.dispose(); host.innerHTML = ''; } };
}
return { destroy() { io.disconnect(); if (api) api.destroy(); } };
}
// the figures in the section's static HTML, refreshed from data.json
function fillFigures(root, D) {
const P = D.pools || {}, BR = D.bridges || {}, A = D.agents || {}, R = D.rewards || {}, MK = D.markets || {};
const V = {
'pools': `${P.active ?? '—'}/${P.registered ?? '—'}`, 'agents': fmt(A.identities), 'locked': M(BR.locked), 'reserve': M(R.reserve_left),
'escrow': fmt(A.escrow_txs), 'stake': M(P.active_stake), 'yield': R.yield_network ? (R.yield_network * 100).toFixed(1) + '%' : '—',
'depth': usd(MK.base_depth_usd), 'lz': M(BR.layerzero), 'reactor': M(BR.reactor), 'skyline': M(BR.skyline), 'epoch': D.epoch ?? '—',
'per_epoch': M(R.per_epoch), 'production': fmt(A.production), 'via_hal8': fmt(A.via_hal8),
'at': D.at ? new Date(D.at).toISOString().slice(0, 16).replace('T', ' ') + ' UTC' : '—',
};
root.querySelectorAll('[data-k]').forEach(el => { if (V[el.dataset.k] != null) el.textContent = V[el.dataset.k]; });
}