feat: add memory graph & sovereignty loop visualization (#18)
- Memory graph: 9 typed nodes (user/feedback/project/reference) as glowing spheres with halo rings and labels, positioned at (-15,0,-10) with edge connections between related nodes and floating animation - Sovereignty loop: EXPORT→COMPRESS→TRAIN→EVAL ring at (0,8,-22) with arc segments, central soul icosahedron, traveling pulse sphere that changes color per stage, slow Y-axis rotation - HUD legend (top-right) with color key for both memory types and sovereignty stages - Animation: memory nodes float/pulse independently; sovereignty group rotates; pulse sphere travels the ring with per-stage color shifts Fixes #18 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
253
app.js
253
app.js
@@ -34,6 +34,8 @@ let debugOverlay;
|
||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||
let chatOpen = true;
|
||||
let loadProgress = 0;
|
||||
let memoryNodes = [];
|
||||
let sovereigntyGroup, sovereigntyPulse, sovereigntyPulseT = 0;
|
||||
|
||||
// ═══ INIT ═══
|
||||
function init() {
|
||||
@@ -77,6 +79,8 @@ function init() {
|
||||
createDustParticles();
|
||||
updateLoad(85);
|
||||
createAmbientStructures();
|
||||
createMemoryGraph();
|
||||
createSovereigntyLoop();
|
||||
updateLoad(90);
|
||||
|
||||
// Post-processing
|
||||
@@ -787,6 +791,214 @@ function createAmbientStructures() {
|
||||
scene.add(pedestal);
|
||||
}
|
||||
|
||||
// ═══ MEMORY GRAPH ═══
|
||||
function createMemoryGraph() {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(-15, 0, -10);
|
||||
group.name = 'memory-graph';
|
||||
scene.add(group);
|
||||
|
||||
// Memory type color map
|
||||
const typeColors = {
|
||||
user: 0x4af0c0,
|
||||
feedback: 0xff8844,
|
||||
project: 0x7b5cff,
|
||||
reference: 0xffd700,
|
||||
};
|
||||
|
||||
// Node definitions: label, type, local position
|
||||
const nodeDefs = [
|
||||
{ label: 'role', type: 'user', pos: [-2.5, 4, 0.5] },
|
||||
{ label: 'prefs', type: 'user', pos: [-1, 5.5, -1 ] },
|
||||
{ label: 'expertise', type: 'user', pos: [-3, 3, -1.5] },
|
||||
{ label: 'corrections', type: 'feedback', pos: [ 1.5, 4, -0.5] },
|
||||
{ label: 'confirmed', type: 'feedback', pos: [ 2.5, 5.5, 1 ] },
|
||||
{ label: 'milestone', type: 'project', pos: [ 0.5, 2.5, -2 ] },
|
||||
{ label: 'decisions', type: 'project', pos: [-1, 2, 1 ] },
|
||||
{ label: 'gitea', type: 'reference', pos: [ 2, 2.8, 2 ] },
|
||||
{ label: 'grafana', type: 'reference', pos: [-2, 5, 2 ] },
|
||||
];
|
||||
|
||||
// Edges (index pairs)
|
||||
const edges = [
|
||||
[0, 1], [0, 2], [1, 2],
|
||||
[3, 4],
|
||||
[5, 6],
|
||||
[7, 8],
|
||||
[0, 3], [2, 6], [4, 5], [6, 7],
|
||||
];
|
||||
|
||||
// Create node spheres
|
||||
nodeDefs.forEach((def, i) => {
|
||||
const color = typeColors[def.type];
|
||||
const geo = new THREE.SphereGeometry(0.22, 16, 16);
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color,
|
||||
emissive: color,
|
||||
emissiveIntensity: 1.2,
|
||||
roughness: 0.2,
|
||||
metalness: 0.5,
|
||||
});
|
||||
const sphere = new THREE.Mesh(geo, mat);
|
||||
sphere.position.set(...def.pos);
|
||||
sphere.userData = { baseY: def.pos[1], phase: i * 0.7 };
|
||||
sphere.name = 'memnode_' + i;
|
||||
group.add(sphere);
|
||||
|
||||
// Halo ring
|
||||
const haloGeo = new THREE.TorusGeometry(0.32, 0.02, 8, 32);
|
||||
const haloMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5 });
|
||||
const halo = new THREE.Mesh(haloGeo, haloMat);
|
||||
halo.rotation.x = Math.PI / 2;
|
||||
sphere.add(halo);
|
||||
|
||||
// Canvas label
|
||||
const lc = document.createElement('canvas');
|
||||
lc.width = 256; lc.height = 48;
|
||||
const lctx = lc.getContext('2d');
|
||||
lctx.font = 'bold 22px "JetBrains Mono", monospace';
|
||||
lctx.fillStyle = '#' + new THREE.Color(color).getHexString();
|
||||
lctx.textAlign = 'center';
|
||||
lctx.fillText(def.label, 128, 32);
|
||||
const ltex = new THREE.CanvasTexture(lc);
|
||||
const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide, depthWrite: false });
|
||||
const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(1.0, 0.2), lmat);
|
||||
lmesh.position.y = 0.42;
|
||||
sphere.add(lmesh);
|
||||
|
||||
memoryNodes.push(sphere);
|
||||
});
|
||||
|
||||
// Create edge lines
|
||||
edges.forEach(([a, b]) => {
|
||||
const posA = new THREE.Vector3(...nodeDefs[a].pos);
|
||||
const posB = new THREE.Vector3(...nodeDefs[b].pos);
|
||||
const points = [posA, posB];
|
||||
const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const lineMat = new THREE.LineBasicMaterial({
|
||||
color: 0x334466,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
});
|
||||
group.add(new THREE.Line(lineGeo, lineMat));
|
||||
});
|
||||
|
||||
// Section label
|
||||
const lc = document.createElement('canvas');
|
||||
lc.width = 512; lc.height = 56;
|
||||
const lctx = lc.getContext('2d');
|
||||
lctx.font = 'bold 28px "Orbitron", sans-serif';
|
||||
lctx.fillStyle = '#4af0c0';
|
||||
lctx.textAlign = 'center';
|
||||
lctx.fillText('◈ MEMORY BANKS', 256, 38);
|
||||
const ltex = new THREE.CanvasTexture(lc);
|
||||
const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide });
|
||||
const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.45), lmat);
|
||||
lmesh.position.set(0, 7, 0);
|
||||
group.add(lmesh);
|
||||
}
|
||||
|
||||
// ═══ SOVEREIGNTY LOOP ═══
|
||||
function createSovereigntyLoop() {
|
||||
sovereigntyGroup = new THREE.Group();
|
||||
sovereigntyGroup.position.set(0, 8, -22);
|
||||
sovereigntyGroup.name = 'sovereignty-loop';
|
||||
scene.add(sovereigntyGroup);
|
||||
|
||||
const stages = [
|
||||
{ label: 'EXPORT', color: 0x4af0c0, angle: 0 },
|
||||
{ label: 'COMPRESS', color: 0x7b5cff, angle: Math.PI / 2 },
|
||||
{ label: 'TRAIN', color: 0xff8844, angle: Math.PI },
|
||||
{ label: 'EVAL', color: 0xffd700, angle: Math.PI * 3 / 2 },
|
||||
];
|
||||
|
||||
const ringRadius = 3.5;
|
||||
|
||||
stages.forEach((s, i) => {
|
||||
const x = Math.cos(s.angle) * ringRadius;
|
||||
const z = Math.sin(s.angle) * ringRadius;
|
||||
|
||||
// Stage sphere
|
||||
const geo = new THREE.SphereGeometry(0.38, 20, 20);
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: s.color,
|
||||
emissive: s.color,
|
||||
emissiveIntensity: 1.5,
|
||||
roughness: 0.1,
|
||||
metalness: 0.7,
|
||||
});
|
||||
const sphere = new THREE.Mesh(geo, mat);
|
||||
sphere.position.set(x, 0, z);
|
||||
sphere.name = 'stage_' + i;
|
||||
sovereigntyGroup.add(sphere);
|
||||
|
||||
// Stage label
|
||||
const lc = document.createElement('canvas');
|
||||
lc.width = 256; lc.height = 48;
|
||||
const lctx = lc.getContext('2d');
|
||||
lctx.font = 'bold 24px "Orbitron", sans-serif';
|
||||
lctx.fillStyle = '#' + new THREE.Color(s.color).getHexString();
|
||||
lctx.textAlign = 'center';
|
||||
lctx.fillText(s.label, 128, 32);
|
||||
const ltex = new THREE.CanvasTexture(lc);
|
||||
const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide, depthWrite: false });
|
||||
const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 0.26), lmat);
|
||||
lmesh.position.set(x, 0.7, z);
|
||||
sovereigntyGroup.add(lmesh);
|
||||
|
||||
// Arc segment to next stage
|
||||
const nextAngle = stages[(i + 1) % stages.length].angle;
|
||||
const pts = [];
|
||||
const steps = 24;
|
||||
for (let k = 0; k <= steps; k++) {
|
||||
const a = s.angle + (nextAngle - s.angle + (i === 3 ? Math.PI * 2 : 0)) * (k / steps);
|
||||
pts.push(new THREE.Vector3(Math.cos(a) * ringRadius, 0, Math.sin(a) * ringRadius));
|
||||
}
|
||||
const arcGeo = new THREE.BufferGeometry().setFromPoints(pts);
|
||||
const arcMat = new THREE.LineBasicMaterial({ color: s.color, transparent: true, opacity: 0.35 });
|
||||
sovereigntyGroup.add(new THREE.Line(arcGeo, arcMat));
|
||||
});
|
||||
|
||||
// Central soul sphere
|
||||
const soulGeo = new THREE.IcosahedronGeometry(0.55, 2);
|
||||
const soulMat = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
emissive: 0x4af0c0,
|
||||
emissiveIntensity: 2,
|
||||
roughness: 0,
|
||||
metalness: 1,
|
||||
transmission: 0.4,
|
||||
});
|
||||
const soul = new THREE.Mesh(soulGeo, soulMat);
|
||||
soul.name = 'soul-core';
|
||||
sovereigntyGroup.add(soul);
|
||||
|
||||
// Traveling pulse sphere
|
||||
const pulseGeo = new THREE.SphereGeometry(0.15, 12, 12);
|
||||
const pulseMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x4af0c0,
|
||||
emissive: 0x4af0c0,
|
||||
emissiveIntensity: 3,
|
||||
roughness: 0,
|
||||
});
|
||||
sovereigntyPulse = new THREE.Mesh(pulseGeo, pulseMat);
|
||||
sovereigntyGroup.add(sovereigntyPulse);
|
||||
|
||||
// Section label
|
||||
const lc = document.createElement('canvas');
|
||||
lc.width = 640; lc.height = 56;
|
||||
const lctx = lc.getContext('2d');
|
||||
lctx.font = 'bold 28px "Orbitron", sans-serif';
|
||||
lctx.fillStyle = '#ffd700';
|
||||
lctx.textAlign = 'center';
|
||||
lctx.fillText('◈ SOVEREIGNTY LOOP', 320, 38);
|
||||
const ltex = new THREE.CanvasTexture(lc);
|
||||
const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide });
|
||||
const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(5, 0.45), lmat);
|
||||
lmesh.position.set(0, 2.2, 0);
|
||||
sovereigntyGroup.add(lmesh);
|
||||
}
|
||||
|
||||
// ═══ CONTROLS ═══
|
||||
function setupControls() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -941,6 +1153,47 @@ function gameLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Animate memory nodes (float & pulse)
|
||||
memoryNodes.forEach((node, i) => {
|
||||
node.position.y = node.userData.baseY + Math.sin(elapsed * 0.9 + node.userData.phase) * 0.25;
|
||||
node.material.emissiveIntensity = 1.0 + Math.sin(elapsed * 1.5 + node.userData.phase) * 0.4;
|
||||
});
|
||||
|
||||
// Animate sovereignty loop
|
||||
if (sovereigntyGroup) {
|
||||
sovereigntyGroup.rotation.y = elapsed * 0.12;
|
||||
|
||||
// Soul core pulse
|
||||
const soul = scene.getObjectByName('soul-core');
|
||||
if (soul) {
|
||||
soul.rotation.y = elapsed * 0.8;
|
||||
soul.rotation.x = elapsed * 0.4;
|
||||
soul.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2.5) * 0.5;
|
||||
}
|
||||
|
||||
// Traveling pulse around the ring
|
||||
if (sovereigntyPulse) {
|
||||
sovereigntyPulseT = (elapsed * 0.4) % 1; // full lap period ~2.5s
|
||||
const stageAngles = [0, Math.PI / 2, Math.PI, Math.PI * 3 / 2];
|
||||
const stageColors = [0x4af0c0, 0x7b5cff, 0xff8844, 0xffd700];
|
||||
const seg = sovereigntyPulseT * 4;
|
||||
const segIdx = Math.floor(seg) % 4;
|
||||
const segT = seg - Math.floor(seg);
|
||||
const aFrom = stageAngles[segIdx];
|
||||
const aTo = stageAngles[(segIdx + 1) % 4];
|
||||
// handle wrap-around from EVAL (270°) back to EXPORT (0°/360°)
|
||||
let diff = aTo - aFrom;
|
||||
if (diff < 0) diff += Math.PI * 2;
|
||||
const angle = aFrom + diff * segT;
|
||||
const ringRadius = 3.5;
|
||||
sovereigntyPulse.position.set(Math.cos(angle) * ringRadius, 0, Math.sin(angle) * ringRadius);
|
||||
const col = new THREE.Color(stageColors[segIdx]);
|
||||
sovereigntyPulse.material.color.copy(col);
|
||||
sovereigntyPulse.material.emissive.copy(col);
|
||||
sovereigntyPulse.material.emissiveIntensity = 2.5 + Math.sin(elapsed * 8) * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Animate nexus core
|
||||
const core = scene.getObjectByName('nexus-core');
|
||||
if (core) {
|
||||
|
||||
14
index.html
14
index.html
@@ -95,6 +95,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory legend -->
|
||||
<div class="hud-memory-legend">
|
||||
<div class="legend-title">MEMORY BANKS</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#4af0c0"></span>USER</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#ff8844"></span>FEEDBACK</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#7b5cff"></span>PROJECT</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#ffd700"></span>REFERENCE</div>
|
||||
<div class="legend-title" style="margin-top:8px">SOVEREIGNTY LOOP</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#4af0c0"></span>EXPORT</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#7b5cff"></span>COMPRESS</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#ff8844"></span>TRAIN</div>
|
||||
<div class="legend-row"><span class="legend-dot" style="background:#ffd700"></span>EVAL</div>
|
||||
</div>
|
||||
|
||||
<!-- Minimap / Controls hint -->
|
||||
<div class="hud-controls">
|
||||
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
||||
|
||||
39
style.css
39
style.css
@@ -359,3 +359,42 @@ canvas#nexus-canvas {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* === MEMORY LEGEND === */
|
||||
.hud-memory-legend {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: var(--space-4);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--panel-radius);
|
||||
padding: var(--space-3);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
min-width: 140px;
|
||||
font-size: var(--text-xs);
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.legend-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--space-1);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.legend-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: 3px 0;
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 6px currentColor;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user