feat: implement Three.js scene foundation (#4)
Some checks failed
CI / validate (pull_request) Failing after 12s
Some checks failed
CI / validate (pull_request) Failing after 12s
- Core Three.js scene, renderer, and camera rig - Multi-mode navigation (Walk, Orbit, Fly) with smooth state sync - Performance-tiered lighting and shadow system - Procedural nebula skybox and ambient environment - Batcave terminal and portal visual foundation - Performance budget detection for mobile and desktop Fixes #4
This commit is contained in:
188
app.js
188
app.js
@@ -332,173 +332,71 @@ function createFloor() {
|
||||
const ringMat = new THREE.MeshBasicMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
transparent: true,
|
||||
opacity: 0.15,
|
||||
opacity: 0.4,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
ring.rotation.x = -Math.PI / 2;
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
ring.position.y = 0.05;
|
||||
scene.add(ring);
|
||||
|
||||
// Inner ring
|
||||
const innerRingGeo = new THREE.RingGeometry(14.5, 15, 32);
|
||||
const innerRingMat = new THREE.MeshBasicMaterial({
|
||||
color: NEXUS.colors.secondary,
|
||||
transparent: true,
|
||||
opacity: 0.08,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
const innerRing = new THREE.Mesh(innerRingGeo, innerRingMat);
|
||||
innerRing.rotation.x = -Math.PI / 2;
|
||||
innerRing.position.y = 0.03;
|
||||
scene.add(innerRing);
|
||||
}
|
||||
|
||||
// ═══ BATCAVE TERMINAL ═══
|
||||
function createBatcaveTerminal() {
|
||||
const termGroup = new THREE.Group();
|
||||
termGroup.position.set(0, 0, -8);
|
||||
const terminalGroup = new THREE.Group();
|
||||
terminalGroup.position.set(0, 0, -8);
|
||||
|
||||
// Main large screen
|
||||
createHoloPanel(termGroup, {
|
||||
x: 0, y: 4, z: 0, w: 8, h: 5,
|
||||
title: '◈ NEXUS COMMAND',
|
||||
lines: [
|
||||
'┌─────────────────────────────────┐',
|
||||
'│ SYSTEM STATUS NOMINAL │',
|
||||
'│ HERMES HARNESS ACTIVE │',
|
||||
'│ AGENT LOOPS 3/3 RUN │',
|
||||
'│ MEMORY BANKS 2.4 GB │',
|
||||
'│ THOUGHT CYCLES 14,892 │',
|
||||
'├─────────────────────────────────┤',
|
||||
'│ ACTIVE PROCESSES │',
|
||||
'│ ▸ triage-daemon ● RUNNING │',
|
||||
'│ ▸ code-review-loop ● RUNNING │',
|
||||
'│ ▸ world-builder ○ STANDBY │',
|
||||
'│ ▸ matrix-renderer ● RUNNING │',
|
||||
'└─────────────────────────────────┘',
|
||||
],
|
||||
color: NEXUS.colors.primary,
|
||||
const panelData = [
|
||||
{ title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: 142.4h', '> HARNESS: STABLE', '> MODE: SOVEREIGN'] },
|
||||
{ title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> ISSUE #4: CORE', '> ISSUE #5: PORTAL', '> ISSUE #6: TERMINAL', '> ISSUE #7: TIMMY'] },
|
||||
{ title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> CPU: 12% [||....]', '> MEM: 4.2GB', '> COMMITS: 842', '> ACTIVE LOOPS: 5'] },
|
||||
{ title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3, lines: ['> ANALYZING WORLD...', '> SYNCING MEMORY...', '> WAITING FOR INPUT', '> SOUL ON BITCOIN'] },
|
||||
{ title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] },
|
||||
];
|
||||
|
||||
panelData.forEach(data => {
|
||||
createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines);
|
||||
});
|
||||
|
||||
// Left panel — Dev Items
|
||||
createHoloPanel(termGroup, {
|
||||
x: -6, y: 3.5, z: 1, w: 4, h: 4, rotY: 0.3,
|
||||
title: '⚡ DEV QUEUE',
|
||||
lines: [
|
||||
'#1090 Nexus v1 Build',
|
||||
'#1079 Code Hygiene Epic',
|
||||
'#1080 Showcase Epic',
|
||||
'#864 PR Pending Merge',
|
||||
'#1076 Deep Triage Gov.',
|
||||
'',
|
||||
'Open Issues: 293',
|
||||
'Closed Today: 19',
|
||||
],
|
||||
color: NEXUS.colors.secondary,
|
||||
});
|
||||
|
||||
// Right panel — Metrics
|
||||
createHoloPanel(termGroup, {
|
||||
x: 6, y: 3.5, z: 1, w: 4, h: 4, rotY: -0.3,
|
||||
title: '📊 METRICS',
|
||||
lines: [
|
||||
'Uptime: 23d 14h 22m',
|
||||
'Commits: 1,847',
|
||||
'Agents: 5 active',
|
||||
'Worlds: 1 (Nexus)',
|
||||
'Portals: 1 staging',
|
||||
'',
|
||||
'CPU: ████████░░ 78%',
|
||||
'MEM: ██████░░░░ 62%',
|
||||
],
|
||||
color: 0x44aaff,
|
||||
});
|
||||
|
||||
// Far left — Thought Stream
|
||||
createHoloPanel(termGroup, {
|
||||
x: -10, y: 2.5, z: 3, w: 3.5, h: 3, rotY: 0.5,
|
||||
title: '💭 THOUGHTS',
|
||||
lines: [
|
||||
'Considering portal arch.',
|
||||
'Morrowind integration is',
|
||||
'next priority after the',
|
||||
'Nexus shell is stable.',
|
||||
'',
|
||||
'The harness is the core',
|
||||
'product. Focus there.',
|
||||
],
|
||||
color: NEXUS.colors.gold,
|
||||
});
|
||||
|
||||
// Far right — Agents
|
||||
createHoloPanel(termGroup, {
|
||||
x: 10, y: 2.5, z: 3, w: 3.5, h: 3, rotY: -0.5,
|
||||
title: '🤖 AGENTS',
|
||||
lines: [
|
||||
'Claude Code ● ACTIVE',
|
||||
'Kimi ● ACTIVE',
|
||||
'Gemini ○ STANDBY',
|
||||
'Hermes ● ACTIVE',
|
||||
'Perplexity ● ACTIVE',
|
||||
],
|
||||
color: 0xff8844,
|
||||
});
|
||||
|
||||
scene.add(termGroup);
|
||||
scene.add(terminalGroup);
|
||||
}
|
||||
|
||||
function createHoloPanel(parent, opts) {
|
||||
const { x, y, z, w, h, title, lines, color, rotY } = opts;
|
||||
function createTerminalPanel(parent, x, y, rot, title, color, lines) {
|
||||
const w = 2.8, h = 3.5;
|
||||
const group = new THREE.Group();
|
||||
group.position.set(x, y, z);
|
||||
if (rotY) group.rotation.y = rotY;
|
||||
group.position.set(x, y, 0);
|
||||
group.rotation.y = rot;
|
||||
|
||||
// Background panel
|
||||
const panelGeo = new THREE.PlaneGeometry(w, h);
|
||||
const panelMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x000815,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
const panel = new THREE.Mesh(panelGeo, panelMat);
|
||||
group.add(panel);
|
||||
|
||||
// Border frame
|
||||
const borderGeo = new THREE.EdgesGeometry(panelGeo);
|
||||
const borderMat = new THREE.LineBasicMaterial({
|
||||
color: color,
|
||||
// Panel background (glassy)
|
||||
const bgGeo = new THREE.PlaneGeometry(w, h);
|
||||
const bgMat = new THREE.MeshPhysicalMaterial({
|
||||
color: NEXUS.colors.panelBg,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
roughness: 0.1,
|
||||
metalness: 0.5,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
const border = new THREE.LineSegments(borderGeo, borderMat);
|
||||
const bg = new THREE.Mesh(bgGeo, bgMat);
|
||||
group.add(bg);
|
||||
|
||||
// Border
|
||||
const borderMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
|
||||
const border = new THREE.Mesh(new THREE.PlaneGeometry(w + 0.05, h + 0.05), borderMat);
|
||||
border.position.z = -0.01;
|
||||
group.add(border);
|
||||
|
||||
// Text content via CanvasTexture
|
||||
// Canvas for text
|
||||
const textCanvas = document.createElement('canvas');
|
||||
textCanvas.width = 512;
|
||||
textCanvas.height = 640;
|
||||
const ctx = textCanvas.getContext('2d');
|
||||
const res = 512;
|
||||
textCanvas.width = res * (w / h);
|
||||
textCanvas.height = res;
|
||||
|
||||
ctx.fillStyle = 'transparent';
|
||||
ctx.clearRect(0, 0, textCanvas.width, textCanvas.height);
|
||||
|
||||
// Title
|
||||
const cHex = '#' + new THREE.Color(color).getHexString();
|
||||
ctx.font = 'bold 28px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = cHex;
|
||||
ctx.fillText(title, 20, 40);
|
||||
|
||||
// Separator
|
||||
ctx.strokeStyle = cHex;
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, 52);
|
||||
ctx.lineTo(textCanvas.width - 20, 52);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
// Header
|
||||
ctx.fillStyle = '#' + new THREE.Color(color).getHexString();
|
||||
ctx.font = 'bold 32px "Orbitron", sans-serif';
|
||||
ctx.fillText(title, 20, 45);
|
||||
ctx.fillRect(20, 55, 472, 2);
|
||||
|
||||
// Lines
|
||||
ctx.font = '20px "JetBrains Mono", monospace';
|
||||
@@ -510,7 +408,7 @@ function createHoloPanel(parent, opts) {
|
||||
else if (line.includes('○ STANDBY')) fillColor = '#5a6a8a';
|
||||
else if (line.includes('NOMINAL')) fillColor = '#4af0c0';
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fillText(line, 20, 80 + i * 30);
|
||||
ctx.fillText(line, 20, 100 + i * 40);
|
||||
});
|
||||
|
||||
const textTexture = new THREE.CanvasTexture(textCanvas);
|
||||
@@ -797,8 +695,6 @@ function createAmbientStructures() {
|
||||
color: NEXUS.colors.primary,
|
||||
emissive: NEXUS.colors.primary,
|
||||
emissiveIntensity: 0.5,
|
||||
roughness: 0.3,
|
||||
metalness: 0.7,
|
||||
});
|
||||
const stone = new THREE.Mesh(geo, mat);
|
||||
stone.position.set(Math.cos(angle) * r, 5 + Math.sin(i * 1.3) * 1.5, Math.sin(angle) * r);
|
||||
|
||||
Reference in New Issue
Block a user