feat: add dual-brain holographic panel with brain pulse visualization (#407)
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s

Co-authored-by: Perplexity Computer <perplexity@tower.local>
Co-committed-by: Perplexity Computer <perplexity@tower.local>
This commit was merged in pull request #407.
This commit is contained in:
2026-03-24 16:41:13 +00:00
committed by Timmy Time
parent 0a49e6e75d
commit e29b6ff0a8

410
app.js
View File

@@ -32,11 +32,18 @@ loadingManager.onProgress = (url, itemsLoaded, itemsTotal) => {
document.getElementById('loading-bar').style.width = `${progress}%`;
};
// Simulate loading a texture for demonstration
const textureLoader = new THREE.TextureLoader(loadingManager);
textureLoader.load('placeholder-texture.jpg', (texture) => {
loadedAssets.set('placeholder-texture', texture);
});
// Procedural placeholder texture — avoids 404 for missing placeholder-texture.jpg
const _placeholderCanvas = document.createElement('canvas');
_placeholderCanvas.width = 64;
_placeholderCanvas.height = 64;
const _placeholderCtx = _placeholderCanvas.getContext('2d');
_placeholderCtx.fillStyle = '#0a0a18';
_placeholderCtx.fillRect(0, 0, 64, 64);
const placeholderTexture = new THREE.CanvasTexture(_placeholderCanvas);
loadedAssets.set('placeholder-texture', placeholderTexture);
// Notify loading manager so it still counts one asset
loadingManager.itemStart('placeholder-texture');
loadingManager.itemEnd('placeholder-texture');
// === MATRIX RAIN ===
// 2D canvas layer rendered behind the Three.js scene.
@@ -1219,10 +1226,6 @@ function animateEnergyBeam() {
energyBeamMaterial.opacity = 0.3 + pulseEffect * 0.4;
}
// Update energy beam pulse
beamPulse += 0.02;
energyBeam.material.opacity = 0.6 + Math.sin(beamPulse) * 0.2;
// === RESIZE HANDLER ===
window.addEventListener('resize', () => {
@@ -1234,65 +1237,6 @@ window.addEventListener('resize', () => {
// === SOVEREIGNTY METER ===
// === BATCAVE TERMINAL ENERGY BEAM ===
// Vertical energy beam from Batcave terminal area to the sky with animated opacity for a subtle pulse effect.
const beamGeometry = new THREE.CylinderGeometry(0.2, 0.5, 50, 32);
const beamMaterial = new THREE.MeshBasicMaterial({
color: NEXUS.colors.energy,
emissive: NEXUS.colors.energy,
emissiveIntensity: 0.8,
transparent: true,
opacity: 0.6
});
const energyBeam = new THREE.Mesh(beamGeometry, beamMaterial);
energyBeam.position.set(-10, 25, -10); // Positioned at Batcave terminal area
energyBeam.rotation.x = Math.PI / 2;
NEXUS.scene.add(energyBeam);
// Animate beam opacity for subtle pulse effect
function animateBeam() {
beamMaterial.opacity = 0.6 + 0.2 * Math.sin(Date.now() * 0.002);
requestAnimationFrame(animateBeam);
}
animateBeam();
// === BATCAVE TERMINAL ENERGY BEAM ===
// Vertical energy beam representing connection between Timmy and the outside world
const beamGeometry = new THREE.CylinderGeometry(0.2, 0.5, 100, 32);
const beamMaterial = new THREE.MeshBasicMaterial({
color: NEXUS.colors.accent,
emissive: NEXUS.colors.accent,
emissiveIntensity: 0.8,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false
});
const energyBeam = new THREE.Mesh(beamGeometry, beamMaterial);
energyBeam.position.set(10, 50, 10); // Positioned at Batcave terminal area
scene.add(energyBeam);
// Energy beam from Batcave terminal to sky, representing Timmy's connection
const beamGeometry2 = new THREE.CylinderGeometry(0.5, 2, 1000, 32);
const beamMaterial2 = new THREE.MeshBasicMaterial({
color: NEXUS.colors.accent,
emissive: NEXUS.colors.accent,
emissiveIntensity: 0.8,
transparent: true,
opacity: 0.6
});
const energyBeam2 = new THREE.Mesh(beamGeometry2, beamMaterial2);
energyBeam2.position.set(-40, 500, -40); // Centered above Batcave terminal area
energyBeam2.rotation.x = Math.PI / 2;
scene.add(energyBeam2);
NEXUS.animations = NEXUS.animations || [];
NEXUS.animations.push(() => {
beamMaterial2.opacity = 0.6 + 0.2 * Math.sin(Date.now() * 0.002);
});
// Animation variable for beam pulse effect
let beamPulse = 0;
// Holographic arc gauge floating above the platform; reads from sovereignty-status.json
const sovereigntyGroup = new THREE.Group();
sovereigntyGroup.position.set(0, 3.8, 0);
@@ -2119,6 +2063,266 @@ batcaveGroup.traverse(obj => {
// Probe state — timestamp of last reflection capture (seconds)
let batcaveProbeLastUpdate = -999;
// === DUAL-BRAIN HOLOGRAPHIC PANEL ===
// Floating panel showing Brain Gap Scorecard with two glowing brain orbs
// connected by an animated particle stream representing knowledge transfer.
const DUAL_BRAIN_ORIGIN = new THREE.Vector3(10, 3, -8);
const dualBrainGroup = new THREE.Group();
dualBrainGroup.position.copy(DUAL_BRAIN_ORIGIN);
// Face toward the centre platform
dualBrainGroup.lookAt(0, 3, 0);
scene.add(dualBrainGroup);
// --- Canvas texture for the scorecard panel ---
function createDualBrainTexture() {
const W = 512, H = 512;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
// Dark background
ctx.fillStyle = 'rgba(0, 6, 20, 0.90)';
ctx.fillRect(0, 0, W, H);
// Outer neon border
ctx.strokeStyle = '#4488ff';
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
// Inner subtle border
ctx.strokeStyle = '#223366';
ctx.lineWidth = 1;
ctx.strokeRect(5, 5, W - 10, H - 10);
// Title
ctx.font = 'bold 22px "Courier New", monospace';
ctx.fillStyle = '#88ccff';
ctx.textAlign = 'center';
ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40);
// Separator under title
ctx.strokeStyle = '#1a3a6a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(20, 52);
ctx.lineTo(W - 20, 52);
ctx.stroke();
// Section header
ctx.font = '11px "Courier New", monospace';
ctx.fillStyle = '#556688';
ctx.textAlign = 'left';
ctx.fillText('BRAIN GAP SCORECARD', 20, 74);
// Categories
const categories = [
{ name: 'Triage', score: 0.87, status: 'GRADUATED', color: '#00ff88' },
{ name: 'Tool Use', score: 0.78, status: 'PROBATION', color: '#ffcc00' },
{ name: 'Code Gen', score: 0.62, status: 'SHADOW', color: '#4488ff' },
{ name: 'Planning', score: 0.71, status: 'SHADOW', color: '#4488ff' },
{ name: 'Communication', score: 0.83, status: 'PROBATION', color: '#ffcc00' },
{ name: 'Reasoning', score: 0.55, status: 'CLOUD ONLY', color: '#ff4444' },
];
const barX = 20;
const barW = W - 130;
const barH = 20;
let y = 90;
for (const cat of categories) {
// Category label
ctx.font = '13px "Courier New", monospace';
ctx.fillStyle = '#ccd6f6';
ctx.textAlign = 'left';
ctx.fillText(cat.name, barX, y + 14);
// Score value
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = cat.color;
ctx.textAlign = 'right';
ctx.fillText(cat.score.toFixed(2), W - 20, y + 14);
y += 22;
// Bar background
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
ctx.fillRect(barX, y, barW, barH);
// Bar fill
ctx.fillStyle = cat.color;
ctx.globalAlpha = 0.7;
ctx.fillRect(barX, y, barW * cat.score, barH);
ctx.globalAlpha = 1.0;
// Status label on bar
ctx.font = '10px "Courier New", monospace';
ctx.fillStyle = '#000000';
ctx.textAlign = 'left';
ctx.fillText(cat.status, barX + 6, y + 14);
y += barH + 12;
}
// Separator
ctx.strokeStyle = '#1a3a6a';
ctx.beginPath();
ctx.moveTo(20, y + 4);
ctx.lineTo(W - 20, y + 4);
ctx.stroke();
y += 22;
// Overall score
ctx.font = '12px "Courier New", monospace';
ctx.fillStyle = '#556688';
ctx.textAlign = 'left';
ctx.fillText('OVERALL CONVERGENCE', 20, y);
ctx.font = 'bold 36px "Courier New", monospace';
ctx.fillStyle = '#88ccff';
ctx.textAlign = 'center';
ctx.fillText('0.73', W / 2, y + 44);
// Brain indicators at bottom
y += 60;
// Cloud brain indicator
ctx.beginPath();
ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = '#00ddff';
ctx.fill();
ctx.font = '11px "Courier New", monospace';
ctx.fillStyle = '#00ddff';
ctx.textAlign = 'left';
ctx.fillText('CLOUD', W / 2 - 48, y + 12);
// Local brain indicator
ctx.beginPath();
ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = '#ffaa22';
ctx.fill();
ctx.fillStyle = '#ffaa22';
ctx.fillText('LOCAL', W / 2 + 42, y + 12);
return new THREE.CanvasTexture(canvas);
}
// Panel sprite
const dualBrainTexture = createDualBrainTexture();
const dualBrainMaterial = new THREE.SpriteMaterial({
map: dualBrainTexture,
transparent: true,
opacity: 0.92,
depthWrite: false,
});
const dualBrainSprite = new THREE.Sprite(dualBrainMaterial);
dualBrainSprite.scale.set(5.0, 5.0, 1);
dualBrainSprite.position.set(0, 0, 0); // local to group
dualBrainSprite.userData = {
baseY: 0,
floatPhase: 0,
floatSpeed: 0.22,
zoomLabel: 'Dual-Brain Status',
};
dualBrainGroup.add(dualBrainSprite);
// Accent light for the panel
const dualBrainLight = new THREE.PointLight(0x4488ff, 0.6, 10);
dualBrainLight.position.set(0, 0.5, 1);
dualBrainGroup.add(dualBrainLight);
// --- Brain Orbs ---
// Cloud brain orb (cyan) — positioned left of panel
const CLOUD_ORB_COLOR = 0x00ddff;
const cloudOrbGeo = new THREE.SphereGeometry(0.35, 32, 32);
const cloudOrbMat = new THREE.MeshStandardMaterial({
color: CLOUD_ORB_COLOR,
emissive: new THREE.Color(CLOUD_ORB_COLOR),
emissiveIntensity: 1.5,
metalness: 0.3,
roughness: 0.2,
transparent: true,
opacity: 0.85,
});
const cloudOrb = new THREE.Mesh(cloudOrbGeo, cloudOrbMat);
cloudOrb.position.set(-2.0, 3.0, 0);
cloudOrb.userData.zoomLabel = 'Cloud Brain';
dualBrainGroup.add(cloudOrb);
const cloudOrbLight = new THREE.PointLight(CLOUD_ORB_COLOR, 0.8, 5);
cloudOrbLight.position.copy(cloudOrb.position);
dualBrainGroup.add(cloudOrbLight);
// Local brain orb (amber) — positioned right of panel
const LOCAL_ORB_COLOR = 0xffaa22;
const localOrbGeo = new THREE.SphereGeometry(0.35, 32, 32);
const localOrbMat = new THREE.MeshStandardMaterial({
color: LOCAL_ORB_COLOR,
emissive: new THREE.Color(LOCAL_ORB_COLOR),
emissiveIntensity: 1.5,
metalness: 0.3,
roughness: 0.2,
transparent: true,
opacity: 0.85,
});
const localOrb = new THREE.Mesh(localOrbGeo, localOrbMat);
localOrb.position.set(2.0, 3.0, 0);
localOrb.userData.zoomLabel = 'Local Brain';
dualBrainGroup.add(localOrb);
const localOrbLight = new THREE.PointLight(LOCAL_ORB_COLOR, 0.8, 5);
localOrbLight.position.copy(localOrb.position);
dualBrainGroup.add(localOrbLight);
// --- Brain Pulse Particle Stream ---
// Particles flow from cloud orb → local orb along a curved arc
const BRAIN_PARTICLE_COUNT = 120;
const brainParticlePositions = new Float32Array(BRAIN_PARTICLE_COUNT * 3);
const brainParticlePhases = new Float32Array(BRAIN_PARTICLE_COUNT); // 0..1 progress along arc
const brainParticleSpeeds = new Float32Array(BRAIN_PARTICLE_COUNT);
for (let i = 0; i < BRAIN_PARTICLE_COUNT; i++) {
brainParticlePhases[i] = Math.random();
brainParticleSpeeds[i] = 0.15 + Math.random() * 0.2;
// Initial positions will be set in animate()
brainParticlePositions[i * 3] = 0;
brainParticlePositions[i * 3 + 1] = 0;
brainParticlePositions[i * 3 + 2] = 0;
}
const brainParticleGeo = new THREE.BufferGeometry();
brainParticleGeo.setAttribute('position', new THREE.BufferAttribute(brainParticlePositions, 3));
const brainParticleMat = new THREE.PointsMaterial({
color: 0x44ddff,
size: 0.08,
sizeAttenuation: true,
transparent: true,
opacity: 0.8,
depthWrite: false,
});
const brainParticles = new THREE.Points(brainParticleGeo, brainParticleMat);
dualBrainGroup.add(brainParticles);
// Scanning line overlay canvas — redrawn each frame in animate()
const _scanCanvas = document.createElement('canvas');
_scanCanvas.width = 512;
_scanCanvas.height = 512;
const _scanCtx = _scanCanvas.getContext('2d');
const dualBrainScanTexture = new THREE.CanvasTexture(_scanCanvas);
const dualBrainScanMat = new THREE.SpriteMaterial({
map: dualBrainScanTexture,
transparent: true,
opacity: 0.18,
depthWrite: false,
});
const dualBrainScanSprite = new THREE.Sprite(dualBrainScanMat);
dualBrainScanSprite.scale.set(5.0, 5.0, 1);
dualBrainScanSprite.position.set(0, 0, 0.01);
dualBrainGroup.add(dualBrainScanSprite);
// === ANIMATION LOOP ===
const clock = new THREE.Clock();
@@ -2393,6 +2597,76 @@ function animate() {
gz.discMat.opacity = 0.02 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.02;
}
// === DUAL-BRAIN ANIMATION ===
// Panel float
dualBrainSprite.position.y = dualBrainSprite.userData.baseY +
Math.sin(elapsed * dualBrainSprite.userData.floatSpeed + dualBrainSprite.userData.floatPhase) * 0.22;
dualBrainScanSprite.position.y = dualBrainSprite.position.y;
// Orb glow pulse
const cloudPulse = 1.2 + Math.sin(elapsed * 1.8) * 0.4;
const localPulse = 1.2 + Math.sin(elapsed * 1.8 + Math.PI) * 0.4;
cloudOrbMat.emissiveIntensity = cloudPulse;
localOrbMat.emissiveIntensity = localPulse;
cloudOrbLight.intensity = 0.5 + Math.sin(elapsed * 1.8) * 0.3;
localOrbLight.intensity = 0.5 + Math.sin(elapsed * 1.8 + Math.PI) * 0.3;
// Orb hover
cloudOrb.position.y = 3.0 + Math.sin(elapsed * 0.9) * 0.15;
localOrb.position.y = 3.0 + Math.sin(elapsed * 0.9 + 1.0) * 0.15;
cloudOrbLight.position.y = cloudOrb.position.y;
localOrbLight.position.y = localOrb.position.y;
// Brain pulse particles — flow along a curved arc from cloud → local orb
{
const pos = brainParticleGeo.attributes.position.array;
const startX = cloudOrb.position.x;
const endX = localOrb.position.x;
const arcHeight = 1.2; // peak height of arc above orbs
const simRate = 0.73; // simulated learning rate tied to overall score
for (let i = 0; i < BRAIN_PARTICLE_COUNT; i++) {
brainParticlePhases[i] += brainParticleSpeeds[i] * simRate * 0.016;
if (brainParticlePhases[i] > 1.0) brainParticlePhases[i] -= 1.0;
const t = brainParticlePhases[i];
// Lerp X between orbs
pos[i * 3] = startX + (endX - startX) * t;
// Arc Y: parabolic curve peaking at midpoint
const midY = (cloudOrb.position.y + localOrb.position.y) / 2 + arcHeight;
pos[i * 3 + 1] = cloudOrb.position.y + (midY - cloudOrb.position.y) * 4 * t * (1 - t)
+ (localOrb.position.y - cloudOrb.position.y) * t;
// Slight Z wobble for volume
pos[i * 3 + 2] = Math.sin(t * Math.PI * 4 + elapsed * 2 + i) * 0.12;
}
brainParticleGeo.attributes.position.needsUpdate = true;
// Colour lerp from cyan → amber based on progress (approximated via hue shift)
const pulseIntensity = 0.6 + Math.sin(elapsed * 2.0) * 0.2;
brainParticleMat.opacity = pulseIntensity;
}
// Scanning line effect — thin horizontal line sweeps down the panel
{
const W = 512, H = 512;
_scanCtx.clearRect(0, 0, W, H);
const scanY = ((elapsed * 60) % H);
_scanCtx.fillStyle = 'rgba(68, 136, 255, 0.5)';
_scanCtx.fillRect(0, scanY, W, 2);
// Faint glow around scan line
const grad = _scanCtx.createLinearGradient(0, scanY - 8, 0, scanY + 10);
grad.addColorStop(0, 'rgba(68, 136, 255, 0)');
grad.addColorStop(0.4, 'rgba(68, 136, 255, 0.15)');
grad.addColorStop(0.6, 'rgba(68, 136, 255, 0.15)');
grad.addColorStop(1, 'rgba(68, 136, 255, 0)');
_scanCtx.fillStyle = grad;
_scanCtx.fillRect(0, scanY - 8, W, 18);
dualBrainScanTexture.needsUpdate = true;
}
// Panel accent light pulse
dualBrainLight.intensity = 0.4 + Math.sin(elapsed * 1.1) * 0.2;
// Portal collision detection
forwardVector.set(0, 0, -1).applyQuaternion(camera.quaternion);
raycaster.set(camera.position, forwardVector);