Files
nibiru-framework.com/docs/public/js/nibiru-scene.js
stephan 48c839d927 Initial public push: docs cosmos v4 + AI module + framework groundwork
This is the snapshot the production landing site (nibiru-framework.com) is
deployed from. Brings together the recent splash + docs migration to the v4
"Cosmos" design system, the new in-framework AI module, and the framework
groundwork that backs the framework-reference extraction.

What lands:
- docs/: Astro + Starlight site with the v4 dark cosmic palette, GalaxyHero
  canvas constellation, Mission Control chat (wired to /api/oracle →
  api.neuronetz.ai via providers.mjs Ollama), 5-panel MMVC stage
  (Model · AI · Module · Controller · View), translated EN/DE/JA/ES/FR
  content, PWA + sitemap + llms.txt + Umami analytics.
- docs/design-system/: canonical mockup bundle (source/index-v2.html for
  splash, source/docs-system.html + preview/ for docs, SPEC.md, tokens).
- docs/scripts/extraction/framework-reference-v2.md: deep framework
  reference (~1.6k lines, file:line citations, every public factory and
  idiom — basis for the LoRA training corpus.
- application/module/ai/: AI module with chat / embed / RAG / agent
  plugins, plus pdoQuery / httpGet / fileRead tools and Modelfile +
  smoke-test in training/.
- application/module/users/: user / ACL / form-factory traits used as the
  reference plugin pattern for the framework docs.
- application/settings/config/database/: schema + seed migrations
  including the AI module tables (200–203).
- Form factory + autogenerator changes the framework-reference-v2 covers.

Production secrets stay out: docs/.env, settings.production.ini and
ai.production.ini are all gitignored (.example files are in tree).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:22:18 +02:00

1001 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
Nibiru v2 — site script
------------------------------------------------------------
- Hero constellation (Three.js)
- Comet-trail cursor
- MMVC pinned narrative (scroll-driven camera focus + panel sync)
- Mission Control chat (window.claude.complete + telemetry)
- Launch-sequence typewriter + live system canvas
- Footer wide-shot constellation
- Nav condense / back-to-top
============================================================ */
(() => {
'use strict';
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const hasThree = typeof THREE !== 'undefined';
/* ====================== NAV CONDENSE ====================== */
const nav = document.getElementById('nav');
const toTop = document.getElementById('toTop');
const onScroll = () => {
const y = window.scrollY || 0;
nav.classList.toggle('condensed', y > 40);
toTop.classList.toggle('visible', y > window.innerHeight * 0.8);
};
window.addEventListener('scroll', onScroll, { passive: true });
toTop.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
onScroll();
/* ====================== CURSOR GLOW (subtle) ====================== */
// A small soft star-glow follows the cursor — much subtler than before.
// Disabled on touch / small screens.
if (!reducedMotion && window.matchMedia('(hover: hover)').matches && innerWidth > 900) {
const trail = document.getElementById('comet-trail');
const tctx = trail.getContext('2d');
let mx = -100, my = -100, tx = -100, ty = -100;
const resize = () => {
trail.width = innerWidth * devicePixelRatio;
trail.height = innerHeight * devicePixelRatio;
trail.style.width = innerWidth + 'px';
trail.style.height = innerHeight + 'px';
tctx.setTransform(1, 0, 0, 1, 0, 0);
tctx.scale(devicePixelRatio, devicePixelRatio);
};
resize();
window.addEventListener('resize', resize);
window.addEventListener('mousemove', (e) => { mx = e.clientX; my = e.clientY; });
const tick = () => {
// ease the rendered position so it lags slightly
tx += (mx - tx) * 0.18;
ty += (my - ty) * 0.18;
tctx.clearRect(0, 0, innerWidth, innerHeight);
const r = 18;
const g = tctx.createRadialGradient(tx, ty, 0, tx, ty, r);
g.addColorStop(0, 'rgba(244,238,219,0.22)');
g.addColorStop(0.4, 'rgba(184,107,255,0.10)');
g.addColorStop(1, 'rgba(91,141,255,0)');
tctx.fillStyle = g;
tctx.beginPath();
tctx.arc(tx, ty, r, 0, Math.PI * 2);
tctx.fill();
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
/* ====================== HERO GALAXY ====================== */
const heroCanvas = document.getElementById('constellation');
let heroSystem = null;
if (hasThree && heroCanvas && !reducedMotion) {
heroSystem = makeGalaxy(heroCanvas);
}
/* ====================== MMVC PINNED NARRATIVE ====================== */
const mmvcTrack = document.getElementById('mmvcTrack');
const mmvcPanels = document.querySelectorAll('.mmvc-copy .panel');
const mmvcSteps = document.querySelectorAll('.mmvc-progress .step');
const mmvcCanvas = document.getElementById('mmvc-canvas');
// Dedicated MMVC system (separate canvas inside .mmvc-visual)
let mmvcSystem = null;
if (hasThree && mmvcCanvas && !reducedMotion) {
mmvcSystem = makeNibiruSystem(mmvcCanvas, {
withStarfield: true,
labels: false,
autoRotate: true,
cameraDistance: 6.5,
modules: [
{ name: 'Retriever', sig: '', angle: 0, radius: 2.2, color: 0xb86bff, size: 0.16 },
{ name: 'ReActPlanner', sig: '', angle: 1.3, radius: 2.6, color: 0xffb574, size: 0.18 },
{ name: 'Vision', sig: '', angle: 2.5, radius: 2.0, color: 0x5b8dff, size: 0.14 },
{ name: 'Tool::calc', sig: '', angle: 3.7, radius: 2.8, color: 0x7ad6a3, size: 0.12 },
{ name: 'Memory', sig: '', angle: 4.9, radius: 2.3, color: 0xff8fb1, size: 0.14 },
]
});
}
let currentMmvcStep = -1;
const setMmvcStep = (n) => {
if (n === currentMmvcStep) return;
currentMmvcStep = n;
mmvcPanels.forEach((p, i) => p.classList.toggle('active', i === n));
mmvcSteps.forEach((p, i) => p.classList.toggle('active', i === n));
if (mmvcSystem) mmvcSystem.focus(n); // 0=Model, 1=AI, 2=Module, 3=Controller, 4=View
};
const PANEL_COUNT = 5;
const stickyCanvas = () => {
if (!mmvcTrack) return;
const trackRect = mmvcTrack.getBoundingClientRect();
const trackTop = trackRect.top;
const total = mmvcTrack.offsetHeight - window.innerHeight;
const progress = Math.max(0, Math.min(1, -trackTop / total));
let step = Math.floor(progress * PANEL_COUNT);
if (step > PANEL_COUNT - 1) step = PANEL_COUNT - 1;
if (progress >= 0 && trackTop <= 0) {
setMmvcStep(step);
} else if (trackTop > 0) {
setMmvcStep(0);
}
};
window.addEventListener('scroll', stickyCanvas, { passive: true });
window.addEventListener('resize', stickyCanvas);
stickyCanvas();
/* ====================== MISSION CONTROL CHAT ====================== */
const mcBody = document.getElementById('mcBody');
const mcInput = document.getElementById('mcInput');
const mcSend = document.getElementById('mcSend');
const mcReset = document.getElementById('mcReset');
const mcTele = document.getElementById('mcTele');
let totalTokens = 0;
let currentModel = 'nibiru-base-7B';
const teleUpdate = (latency, tokens, model) => {
if (model) currentModel = model;
mcTele.innerHTML = `
<span>MODEL <strong>${currentModel}</strong></span>
<span>LATENCY <strong>${latency != null ? latency + ' ms' : '—'}</strong></span>
<span>TOKENS <strong>${tokens}</strong></span>
`;
};
const mcAddLine = (cls, text) => {
const l = document.createElement('div');
l.className = 'mc-line ' + cls;
l.textContent = text;
mcBody.appendChild(l);
mcBody.scrollTop = mcBody.scrollHeight;
return l;
};
const moduleTrace = [
' · Controller::dispatch(prompt)',
' · Module<Retriever>::query(prompt) → 5 docs',
' · Module<ReActPlanner>::run(prompt, ctx)',
' · Model::stream(messages) → tokens',
];
const askAgent = async (prompt) => {
if (!prompt.trim()) return;
mcInput.value = '';
mcInput.disabled = true; mcSend.disabled = true;
mcAddLine('usr', prompt);
// Trace lines (cosmetic — the "modules lighting up")
for (const t of moduleTrace) {
await sleep(160 + Math.random() * 100);
mcAddLine('trc', t);
}
mcAddLine('asst-label', '◇ assistant');
const out = document.createElement('div');
out.className = 'mc-line asst-stream';
mcBody.appendChild(out);
const cursor = document.createElement('span');
cursor.className = 'mc-cursor';
out.appendChild(cursor);
const start = performance.now();
let text = '';
let modelLabel = null;
try {
const res = await fetch('/api/oracle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [{ role: 'user', content: prompt }]
})
});
if (!res.ok) throw new Error('oracle ' + res.status);
const data = await res.json();
text = (data.answer || '').trim();
modelLabel = data.model || null;
} catch (e) {
text = 'Link interrupted. Re-establishing controller… try again in a moment.';
}
// Stream-render the response by chunks of 12 chars
out.textContent = '';
out.appendChild(cursor);
for (let i = 0; i < text.length; i++) {
out.insertBefore(document.createTextNode(text[i]), cursor);
totalTokens += 0.25; // pseudo-token counter
if (i % 4 === 0) {
teleUpdate(Math.round(performance.now() - start), Math.floor(totalTokens));
mcBody.scrollTop = mcBody.scrollHeight;
}
await sleep(8 + Math.random() * 18);
}
cursor.remove();
const elapsed = Math.round(performance.now() - start);
teleUpdate(elapsed, Math.floor(totalTokens), modelLabel);
mcAddLine('done', `✓ done · ${elapsed} ms · ${Math.floor(totalTokens)} tok`);
mcInput.disabled = false; mcSend.disabled = false;
mcInput.focus();
};
mcSend.addEventListener('click', () => askAgent(mcInput.value));
mcInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') askAgent(mcInput.value); });
mcReset.addEventListener('click', () => {
mcBody.innerHTML = '';
mcAddLine('sys', ' session reset · controller online · 4 modules in orbit');
totalTokens = 0;
teleUpdate(null, 0);
});
document.querySelectorAll('.mc-suggestion').forEach(b => {
b.addEventListener('click', () => {
const map = {
'explain mmvc': 'Explain the MMVC pattern in two sentences. Why split Model into Model and Module?',
'rag': 'Show a tiny PHP snippet that builds a RAG agent with Nibiru: a Retriever module wrapping a model, plus a ReActPlanner.',
'tools': "How do I add a custom 'calculator' tool to a Nibiru agent? Give a short PHP snippet using tool('calc', fn).",
'haiku': 'Write a haiku about a small AI agent orbiting a PHP server.'
};
askAgent(map[b.dataset.q] || b.textContent);
});
});
/* ====================== LAUNCH SEQUENCE ====================== */
const launchBody = document.getElementById('launchBody');
const launchPlay = document.getElementById('launchPlay');
const launchCanvas = document.getElementById('launch-canvas');
const lsModules = document.getElementById('lsModules');
const lsTokens = document.getElementById('lsTokens');
const lsLatency = document.getElementById('lsLatency');
// Build a small system canvas dedicated to the launch demo
let launchSys = null;
if (hasThree && launchCanvas && !reducedMotion) {
launchSys = makeNibiruSystem(launchCanvas, {
withStarfield: false,
labels: false,
autoRotate: true,
modules: [] // start empty; launch sequence adds them
});
}
// Tokenized PHP code script. Each step writes a chunk and may register a module.
const LAUNCH_SCRIPT = [
{ code: '<?php\n', delay: 80 },
{ code: '<span class="tk-c">// Nibiru — Model, Module, View, Controller</span>\n', delay: 160 },
{ code: '<span class="tk-k">use</span> Nibiru\\{Model, Module, Controller, View};\n', delay: 220 },
{ code: '<span class="tk-k">use</span> Nibiru\\Std\\{Retriever, ReActPlanner, ChatView};\n\n', delay: 300 },
{ code: '<span class="tk-v">$model</span> = <span class="tk-cls">Model</span>::<span class="tk-fn">load</span>(<span class="tk-s">\'nibiru-base-7B\'</span>);\n', delay: 360, fx: () => addLaunchSatellite('Model', 0xffb574, 0, true) },
{ code: '<span class="tk-v">$retriever</span> = <span class="tk-k">new</span> <span class="tk-cls">Retriever</span>(<span class="tk-attr">index</span>: <span class="tk-s">\'./docs\'</span>, <span class="tk-attr">k</span>: <span class="tk-n">5</span>);\n', delay: 380, fx: () => addLaunchSatellite('Retriever', 0xb86bff, 1) },
{ code: '<span class="tk-v">$planner</span> = <span class="tk-k">new</span> <span class="tk-cls">ReActPlanner</span>(\n', delay: 240 },
{ code: ' <span class="tk-attr">model</span>: <span class="tk-v">$model</span>,\n', delay: 200 },
{ code: ' <span class="tk-attr">tools</span>: [<span class="tk-v">$retriever</span>],\n', delay: 200 },
{ code: ');\n\n', delay: 200, fx: () => addLaunchSatellite('ReActPlanner', 0x5b8dff, 2) },
{ code: '<span class="tk-v">$agent</span> = <span class="tk-k">new</span> <span class="tk-cls">Controller</span>(\n', delay: 240 },
{ code: ' <span class="tk-v">$planner</span>,\n', delay: 200 },
{ code: ' <span class="tk-attr">view</span>: <span class="tk-k">new</span> <span class="tk-cls">ChatView</span>(),\n', delay: 200 },
{ code: ');\n\n', delay: 220, fx: () => addLaunchSatellite('ChatView', 0x7ad6a3, 3) },
{ code: '<span class="tk-k">foreach</span> (<span class="tk-v">$agent</span>-><span class="tk-fn">stream</span>(<span class="tk-s">\'hello\'</span>) <span class="tk-k">as</span> <span class="tk-v">$tok</span>) {\n', delay: 280, fx: () => simulateTokens() },
{ code: ' <span class="tk-k">echo</span> <span class="tk-v">$tok</span>;\n', delay: 200 },
{ code: '}\n', delay: 220 },
];
let satelliteCount = 0;
function addLaunchSatellite(name, color, idx, isCenter = false) {
if (!launchSys) return;
if (isCenter) {
// model already exists at center; just blink it
launchSys.flashCenter();
return;
}
const angle = (idx - 1) * (Math.PI * 2 / 4);
launchSys.addModule({ name, angle, radius: 2.2 + (idx % 2) * 0.4, color, size: 0.15 });
satelliteCount++;
if (lsModules) lsModules.textContent = satelliteCount;
}
async function simulateTokens() {
if (!lsTokens || !lsLatency) return;
const start = performance.now();
let n = 0;
const total = 87;
const step = () => {
n++;
lsTokens.textContent = n;
lsLatency.textContent = Math.round(performance.now() - start) + ' ms';
if (launchSys) launchSys.pulseGravity();
if (n < total) setTimeout(step, 18 + Math.random() * 24);
};
step();
}
let launchRunning = false;
async function runLaunch() {
if (launchRunning) return;
launchRunning = true;
launchPlay.textContent = '▶ Replay';
launchBody.innerHTML = '';
if (launchSys) launchSys.reset();
satelliteCount = 0;
if (lsModules) lsModules.textContent = '0';
if (lsTokens) lsTokens.textContent = '0';
if (lsLatency) lsLatency.textContent = '— ms';
const caret = '<span class="caret"></span>';
let html = '';
for (const part of LAUNCH_SCRIPT) {
if (part.fx) part.fx();
// type out gradually for nicer effect
const raw = part.code;
// For the typewriter feel we render full chunk then a small delay
html += raw;
launchBody.innerHTML = html + caret;
await sleep(part.delay);
}
launchBody.innerHTML = html;
launchRunning = false;
}
if (launchPlay) launchPlay.addEventListener('click', runLaunch);
// Auto-launch when the section enters view
if ('IntersectionObserver' in window && launchPlay) {
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
runLaunch();
io.disconnect();
}
});
}, { threshold: 0.4 });
io.observe(document.getElementById('code'));
}
/* ====================== FOOTER WIDE-SHOT ====================== */
const footerCanvas = document.getElementById('footer-canvas');
if (hasThree && footerCanvas && !reducedMotion) {
makeNibiruSystem(footerCanvas, {
withStarfield: true,
labels: false,
autoRotate: true,
cameraDistance: 9,
modules: [
{ name: '', angle: 0, radius: 2.2, color: 0xb86bff, size: 0.10 },
{ name: '', angle: 1.4, radius: 2.6, color: 0xffb574, size: 0.10 },
{ name: '', angle: 2.7, radius: 2.0, color: 0x5b8dff, size: 0.08 },
{ name: '', angle: 4.0, radius: 2.8, color: 0x7ad6a3, size: 0.08 },
]
});
}
/* ====================== HELPERS ====================== */
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function makeRadialTexture(stops) {
const c = document.createElement('canvas');
c.width = c.height = 128;
const ctx = c.getContext('2d');
const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
stops.forEach((s, i) => g.addColorStop(i / (stops.length - 1), s));
ctx.fillStyle = g;
ctx.fillRect(0, 0, 128, 128);
const tex = new THREE.CanvasTexture(c);
tex.needsUpdate = true;
return tex;
}
/* ====================== GALAXY (hero) ====================== */
function makeGalaxy(canvas) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 500);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(2.5, devicePixelRatio));
renderer.setClearColor(0x000000, 0);
// Camera spherical coords
const cam = {
radius: 11,
minRadius: 5.5,
maxRadius: 28,
theta: Math.PI * 0.25,
phi: 1.05,
target: new THREE.Vector3(0, 0, 0),
// Visual offset: shift the rendered scene UP so the galaxy sits in the
// upper portion of the hero, above the headline.
yOffset: 1.2,
};
function applyCamera() {
const r = cam.radius;
const sp = Math.sin(cam.phi);
camera.position.set(
r * sp * Math.sin(cam.theta),
r * Math.cos(cam.phi) + cam.yOffset,
r * sp * Math.cos(cam.theta)
);
const look = cam.target.clone();
look.y += cam.yOffset;
camera.lookAt(look);
}
applyCamera();
const resize = () => {
const parent = canvas.parentElement;
const rect = (parent || canvas).getBoundingClientRect();
const w = Math.max(2, rect.width), h = Math.max(2, rect.height);
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h, true);
};
resize();
window.addEventListener('resize', resize);
if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(resize).observe(canvas.parentElement || canvas);
}
// ---------- Galaxy particles ----------
const PARTICLES = 18000;
const ARMS = 4;
const positions = new Float32Array(PARTICLES * 3);
const colors = new Float32Array(PARTICLES * 3);
const sizes = new Float32Array(PARTICLES);
const baseAngle = new Float32Array(PARTICLES);
const radii = new Float32Array(PARTICLES);
const heights = new Float32Array(PARTICLES);
const COLOR_CORE = new THREE.Color('#ffe6b8');
const COLOR_HOT = new THREE.Color('#ff9d6c');
const COLOR_MID = new THREE.Color('#b86bff');
const COLOR_OUT = new THREE.Color('#6d9bff');
const COLOR_DUST = new THREE.Color('#3a1f5a');
for (let i = 0; i < PARTICLES; i++) {
const r = Math.pow(Math.random(), 1.8) * 6.4 + 0.05;
const arm = Math.floor(Math.random() * ARMS);
const armOffset = (arm / ARMS) * Math.PI * 2;
const curl = r * 0.9;
// Tighter arm spread near outer
const armSpread = (Math.random() - 0.5) * 0.55 * Math.exp(-r * 0.12);
// Background "haze" stars between arms
const isArmStar = Math.random() < 0.78;
const angleNoise = isArmStar ? armSpread : (Math.random() - 0.5) * Math.PI * 0.9;
const angle = armOffset + curl + angleNoise;
const verticalScale = Math.exp(-r * 0.45) * 0.45 + 0.06;
const yJitter = (Math.random() - 0.5) * verticalScale;
const x = Math.cos(angle) * r;
const z = Math.sin(angle) * r;
const y = yJitter;
positions[i * 3] = x;
positions[i * 3 + 1] = y;
positions[i * 3 + 2] = z;
baseAngle[i] = angle;
radii[i] = r;
heights[i] = y;
// Color blend: core warm → hot orange → magenta → outer blue
let c;
if (r < 1.0) {
c = COLOR_CORE.clone().lerp(COLOR_HOT, r);
} else if (r < 3.0) {
c = COLOR_HOT.clone().lerp(COLOR_MID, (r - 1.0) / 2.0);
} else {
c = COLOR_MID.clone().lerp(COLOR_OUT, Math.min(1, (r - 3.0) / 3.0));
}
// Dust-lane darkening — slight chance to be a "dust" particle
if (!isArmStar && Math.random() < 0.35 && r > 1.2 && r < 5.0) {
c.lerp(COLOR_DUST, 0.55);
}
const v = 0.6 + Math.random() * 0.7;
colors[i * 3] = c.r * v;
colors[i * 3 + 1] = c.g * v;
colors[i * 3 + 2] = c.b * v;
// Size: bigger near core, occasional bright stars
const coreBoost = (1 - Math.min(1, r / 6)) * 1.4;
const bright = Math.random() < 0.025 ? 2.2 : (Math.random() < 0.08 ? 1.4 : 1);
sizes[i] = (0.4 + Math.random() * 0.5 + coreBoost) * bright;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1));
// Soft star sprite (radial gradient on a canvas) — gives points a real glow
const sprite = makeRadialTexture([
'rgba(255,255,255,1)',
'rgba(255,255,255,0.55)',
'rgba(255,255,255,0.12)',
'rgba(255,255,255,0)'
]);
// Custom shader so we can drive per-particle size + soft sprite
const mat = new THREE.ShaderMaterial({
uniforms: {
uTex: { value: sprite },
uPixelRatio: { value: renderer.getPixelRatio() },
uBaseSize: { value: 18.0 },
},
vertexShader: `
attribute float aSize;
varying vec3 vColor;
uniform float uBaseSize;
uniform float uPixelRatio;
void main() {
vColor = color;
vec4 mv = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mv;
// perspective-attenuated size
gl_PointSize = aSize * uBaseSize * uPixelRatio * (1.0 / -mv.z);
}
`,
fragmentShader: `
uniform sampler2D uTex;
varying vec3 vColor;
void main() {
vec4 t = texture2D(uTex, gl_PointCoord);
if (t.a < 0.02) discard;
gl_FragColor = vec4(vColor, 1.0) * t;
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
});
const points = new THREE.Points(geo, mat);
scene.add(points);
// ---------- Bright galactic core ----------
const coreSprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: makeRadialTexture(['rgba(255,240,200,1)','rgba(255,170,100,0.7)','rgba(184,107,255,0.15)','rgba(0,0,0,0)']),
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
}));
coreSprite.scale.set(3.4, 3.4, 1);
scene.add(coreSprite);
const coreOuter = new THREE.Sprite(new THREE.SpriteMaterial({
map: makeRadialTexture(['rgba(184,107,255,0.45)','rgba(91,141,255,0.12)','rgba(0,0,0,0)']),
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
}));
coreOuter.scale.set(9, 9, 1);
scene.add(coreOuter);
// ---------- Far starfield (parallax background) ----------
const bgCount = 1200;
const bgPos = new Float32Array(bgCount * 3);
for (let i = 0; i < bgCount; i++) {
const r = 60 + Math.random() * 80;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
bgPos[i*3] = r * Math.sin(phi) * Math.cos(theta);
bgPos[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
bgPos[i*3+2] = r * Math.cos(phi);
}
const bgGeo = new THREE.BufferGeometry();
bgGeo.setAttribute('position', new THREE.BufferAttribute(bgPos, 3));
const bgStars = new THREE.Points(bgGeo, new THREE.PointsMaterial({
color: 0xf4eedb, size: 0.18, transparent: true, opacity: 0.7, depthWrite: false,
}));
scene.add(bgStars);
// ---------- Interaction: drag rotate, wheel zoom ----------
let dragging = false, lastX = 0, lastY = 0;
let userInteracted = false;
let interactTimeout = null;
const markInteract = () => {
userInteracted = true;
clearTimeout(interactTimeout);
interactTimeout = setTimeout(() => { userInteracted = false; }, 3000);
};
canvas.addEventListener('pointerdown', (e) => {
dragging = true; lastX = e.clientX; lastY = e.clientY;
canvas.setPointerCapture(e.pointerId);
canvas.style.cursor = 'grabbing';
markInteract();
});
canvas.addEventListener('pointermove', (e) => {
if (!dragging) return;
const dx = e.clientX - lastX; const dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
cam.theta -= dx * 0.005;
cam.phi = Math.max(0.25, Math.min(Math.PI - 0.25, cam.phi - dy * 0.005));
markInteract();
});
const endDrag = () => { dragging = false; canvas.style.cursor = 'grab'; };
canvas.addEventListener('pointerup', endDrag);
canvas.addEventListener('pointercancel', endDrag);
canvas.addEventListener('pointerleave', endDrag);
canvas.style.cursor = 'grab';
canvas.addEventListener('wheel', (e) => {
// Only zoom when hero canvas is in foreground (top of page)
e.preventDefault();
const factor = Math.exp(e.deltaY * 0.0015);
cam.radius = Math.max(cam.minRadius, Math.min(cam.maxRadius, cam.radius * factor));
markInteract();
}, { passive: false });
// ---------- Animate ----------
let last = performance.now();
function frame() {
const now = performance.now();
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
// Differential rotation: inner faster, outer slower
const pos = geo.attributes.position.array;
for (let i = 0; i < PARTICLES; i++) {
const r = radii[i];
const speed = 0.08 / (0.4 + r * 0.18);
baseAngle[i] += speed * dt;
const a = baseAngle[i];
pos[i*3] = Math.cos(a) * r;
pos[i*3 + 1] = heights[i];
pos[i*3 + 2] = Math.sin(a) * r;
}
geo.attributes.position.needsUpdate = true;
// Auto-orbit camera if user idle
if (!userInteracted && !dragging) {
cam.theta += dt * 0.04;
}
applyCamera();
// Core breathing
const breath = 1 + Math.sin(now / 1400) * 0.04;
coreSprite.scale.set(3.4 * breath, 3.4 * breath, 1);
// Position the core sprites at the visual offset so they ride with the galaxy
coreSprite.position.set(0, cam.yOffset, 0);
coreOuter.position.set(0, cam.yOffset, 0);
// Move the points cloud up too
points.position.y = cam.yOffset;
bgStars.position.y = cam.yOffset;
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
return { resize };
}
/* ====================== NIBIRU SYSTEM (Three.js) ====================== */
function makeNibiruSystem(canvas, opts) {
const o = Object.assign({
withStarfield: true,
labels: false,
autoRotate: true,
cameraDistance: 6.5,
modules: []
}, opts || {});
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 200);
camera.position.set(0, 1.8, o.cameraDistance);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(2, devicePixelRatio));
renderer.setClearColor(0x000000, 0);
// Sizing
const resize = () => {
const r = canvas.getBoundingClientRect();
const w = Math.max(2, r.width);
const h = Math.max(2, r.height);
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h, false);
};
resize();
// Starfield
if (o.withStarfield) {
const starGeo = new THREE.BufferGeometry();
const starCount = 1200;
const positions = new Float32Array(starCount * 3);
const sizes = new Float32Array(starCount);
for (let i = 0; i < starCount; i++) {
const r = 30 + Math.random() * 50;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i*3] = r * Math.sin(phi) * Math.cos(theta);
positions[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
positions[i*3+2] = r * Math.cos(phi);
sizes[i] = Math.random() < 0.05 ? 0.06 : 0.02 + Math.random() * 0.02;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
starGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const starMat = new THREE.PointsMaterial({
color: 0xf4eedb, size: 0.04, sizeAttenuation: true,
transparent: true, opacity: 0.85, depthWrite: false
});
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
}
// === Center: the Model star ===
// Layered glow: bright core + soft halo
const coreGeo = new THREE.SphereGeometry(0.32, 48, 48);
const coreMat = new THREE.MeshBasicMaterial({ color: 0xfff2d6 });
const core = new THREE.Mesh(coreGeo, coreMat);
scene.add(core);
// Halo (sprite)
const haloTex = makeRadialTexture(['rgba(255,228,170,1)','rgba(255,150,90,0.6)','rgba(184,107,255,0.0)']);
const haloMat = new THREE.SpriteMaterial({ map: haloTex, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending });
const halo = new THREE.Sprite(haloMat);
halo.scale.set(2.2, 2.2, 1);
scene.add(halo);
// Outer corona
const corona = new THREE.Sprite(new THREE.SpriteMaterial({
map: makeRadialTexture(['rgba(184,107,255,0.5)','rgba(91,141,255,0.15)','rgba(0,0,0,0)']),
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
}));
corona.scale.set(5.2, 5.2, 1);
scene.add(corona);
// Orbits group
const orbits = new THREE.Group();
scene.add(orbits);
// Modules
const moduleObjs = [];
function buildOrbit(radius) {
const seg = 128;
const pts = [];
for (let i = 0; i <= seg; i++) {
const a = (i / seg) * Math.PI * 2;
pts.push(new THREE.Vector3(Math.cos(a) * radius, 0, Math.sin(a) * radius));
}
const g = new THREE.BufferGeometry().setFromPoints(pts);
const m = new THREE.LineBasicMaterial({ color: 0xb86bff, transparent: true, opacity: 0.18 });
return new THREE.Line(g, m);
}
function addModule(spec) {
const orbit = buildOrbit(spec.radius);
orbits.add(orbit);
const mGeo = new THREE.SphereGeometry(spec.size || 0.14, 24, 24);
const mMat = new THREE.MeshBasicMaterial({ color: spec.color });
const mesh = new THREE.Mesh(mGeo, mMat);
// glow sprite
const glow = new THREE.Sprite(new THREE.SpriteMaterial({
map: makeRadialTexture([
`rgba(${rgb(spec.color)},0.8)`,
`rgba(${rgb(spec.color)},0.2)`,
`rgba(0,0,0,0)`
]),
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
}));
glow.scale.set(spec.size * 5, spec.size * 5, 1);
const node = new THREE.Group();
node.add(mesh); node.add(glow);
// initial position (animated to orbit on add)
node.position.set(Math.cos(spec.angle) * spec.radius, 0, Math.sin(spec.angle) * spec.radius);
// arrival animation
node.scale.setScalar(0.001);
scene.add(node);
const obj = {
spec, node, mesh, glow, orbit,
angle: spec.angle, radius: spec.radius,
speed: 0.12 + Math.random() * 0.08,
arriveStart: performance.now(),
arriveDur: 700,
};
moduleObjs.push(obj);
return obj;
}
o.modules.forEach(addModule);
// Gravity lines (controller "field")
const linesGroup = new THREE.Group();
scene.add(linesGroup);
function rebuildLines() {
while (linesGroup.children.length) linesGroup.remove(linesGroup.children[0]);
moduleObjs.forEach(m => {
const g = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0,0,0),
m.node.position.clone()
]);
const mat = new THREE.LineBasicMaterial({ color: 0xb86bff, transparent: true, opacity: 0.0 });
const line = new THREE.Line(g, mat);
line._target = m;
linesGroup.add(line);
});
}
rebuildLines();
// Tooltip
const tip = document.getElementById('moduleTip');
const raycaster = hasThree ? new THREE.Raycaster() : null;
const pointer = new THREE.Vector2();
let hovered = null;
function onMove(e) {
if (!o.labels || !tip) return;
const r = canvas.getBoundingClientRect();
pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
pointer.y = -((e.clientY - r.top) / r.height) * 2 + 1;
}
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseleave', () => {
hovered = null;
if (tip) tip.classList.remove('visible');
});
// === Focus / camera presets ===
let cameraTarget = { pos: camera.position.clone(), look: new THREE.Vector3(0,0,0) };
let cameraEase = 0.06;
function focus(step) {
// step: 0=Model, 1=AI, 2=Module, 3=Controller (lines on), 4=View (pull back)
switch (step) {
case 0:
// MODEL — close on the centre
cameraTarget = { pos: new THREE.Vector3(0, 1.0, 3.5), look: new THREE.Vector3(0,0,0) };
linesVisible = 0;
break;
case 1:
// AI — slight orbit, slightly elevated, gravity lines start to flicker on
cameraTarget = { pos: new THREE.Vector3(0.8, 1.6, 3.8), look: new THREE.Vector3(0, 0.2, 0) };
linesVisible = 0.25;
break;
case 2:
// MODULE — offset right, looking at orbiting module nodes
cameraTarget = { pos: new THREE.Vector3(2.6, 1.2, 3.6), look: new THREE.Vector3(1.6, 0, 0) };
linesVisible = 0;
break;
case 3:
// CONTROLLER — high up, looking down, all gravity lines visible
cameraTarget = { pos: new THREE.Vector3(0, 4.8, 6.0), look: new THREE.Vector3(0,0,0) };
linesVisible = 1;
break;
case 4:
default:
// VIEW — pulled back, full system in frame
cameraTarget = { pos: new THREE.Vector3(0, 1.8, o.cameraDistance), look: new THREE.Vector3(0,0,0) };
linesVisible = 0.6;
break;
}
}
let linesVisible = 0;
function pulseGravity() {
linesGroup.children.forEach(l => {
l.material.opacity = Math.min(1, l.material.opacity + 0.4);
});
}
function flashCenter() {
core.scale.setScalar(1.4);
halo.scale.set(3.0, 3.0, 1);
}
function reset() {
while (moduleObjs.length) {
const m = moduleObjs.pop();
scene.remove(m.node);
orbits.remove(m.orbit);
}
rebuildLines();
}
// Animate
let t0 = performance.now();
function frame() {
const t = performance.now();
const dt = Math.min(0.05, (t - t0) / 1000);
t0 = t;
// Module orbit + arrival animation
moduleObjs.forEach(m => {
m.angle += m.speed * dt;
const x = Math.cos(m.angle) * m.radius;
const z = Math.sin(m.angle) * m.radius;
m.node.position.set(x, 0, z);
// arrival ease
const p = Math.min(1, (t - m.arriveStart) / m.arriveDur);
const e = 1 - Math.pow(1 - p, 3);
m.node.scale.setScalar(e);
});
// Core breathing
const breath = 1 + Math.sin(t / 700) * 0.04;
core.scale.lerp(new THREE.Vector3(breath, breath, breath), 0.1);
halo.scale.lerp(new THREE.Vector3(2.2 * breath, 2.2 * breath, 1), 0.1);
// Camera ease
camera.position.lerp(cameraTarget.pos, cameraEase);
const lookCurrent = new THREE.Vector3();
camera.getWorldDirection(lookCurrent);
const desiredDir = cameraTarget.look.clone().sub(camera.position).normalize();
// Smooth lookAt by easing the camera's quaternion toward a temp lookAt camera
const tmp = new THREE.Object3D();
tmp.position.copy(camera.position);
tmp.lookAt(cameraTarget.look);
camera.quaternion.slerp(tmp.quaternion, cameraEase);
// Auto rotate around Y by orbiting target slightly
if (o.autoRotate) {
const ang = t / 12000;
const offset = new THREE.Vector3(Math.sin(ang) * 0.4, 0, 0);
camera.position.add(offset.multiplyScalar(0.001));
}
// Gravity lines opacity
linesGroup.children.forEach(l => {
const target = linesVisible;
l.material.opacity += (target - l.material.opacity) * 0.05;
// refresh geometry to current module pos
if (l._target) {
const pts = [new THREE.Vector3(0,0,0), l._target.node.position.clone()];
l.geometry.setFromPoints(pts);
}
});
// Hover detection
if (o.labels && tip && raycaster) {
raycaster.setFromCamera(pointer, camera);
const meshes = moduleObjs.map(m => m.mesh);
const hits = raycaster.intersectObjects(meshes, false);
if (hits.length) {
const m = moduleObjs.find(m => m.mesh === hits[0].object);
if (m && m !== hovered) {
hovered = m;
tip.querySelector('.name').textContent = m.spec.name;
tip.querySelector('.sig').textContent = m.spec.sig || '';
tip.classList.add('visible');
}
if (hovered) {
// project module position to screen
const v = hovered.node.position.clone().project(camera);
const r = canvas.getBoundingClientRect();
const sx = (v.x * 0.5 + 0.5) * r.width + r.left;
const sy = (-v.y * 0.5 + 0.5) * r.height + r.top;
tip.style.left = sx + 'px';
tip.style.top = sy + 'px';
canvas.style.cursor = 'pointer';
}
} else if (hovered) {
hovered = null;
tip.classList.remove('visible');
canvas.style.cursor = '';
}
}
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
window.addEventListener('resize', resize);
return { focus, addModule, reset, resize, pulseGravity, flashCenter };
}
function rgb(hex) {
const r = (hex >> 16) & 255;
const g = (hex >> 8) & 255;
const b = hex & 255;
return `${r},${g},${b}`;
}
function makeRadialTexture(stops) {
const c = document.createElement('canvas');
c.width = c.height = 128;
const ctx = c.getContext('2d');
const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
stops.forEach((s, i) => g.addColorStop(i / (stops.length - 1), s));
ctx.fillStyle = g;
ctx.fillRect(0, 0, 128, 128);
const tex = new THREE.CanvasTexture(c);
tex.needsUpdate = true;
return tex;
}
})();