Merge pull request 'Apex Universe: the ecosystem as a live star system, below Overview' (#1) from feat/apex-universe into main

This commit is contained in:
2026-09-26 23:02:50 +00:00
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
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
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]; });
}