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:
625
universe/universe.js
Normal file
625
universe/universe.js
Normal 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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)} →</span>` : '') +
|
||||
(i.link ? `<a class="au-btn au-btn-sm" href="${esc(i.link)}" target="_blank" rel="noopener">Visit →</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>›</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 = '▶ Tour · 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 · ${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 = '■ 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]; });
|
||||
}
|
||||
Reference in New Issue
Block a user