Raiff1982's picture
Upload 120 files
ed1b365 verified
/* ============================================================
Spiderweb Visualization β€” Canvas-based Agent Network
Shows the QuantumSpiderweb as an animated node graph.
Zero dependencies. Pure Canvas API.
Always visually alive: ambient breathing, orbital drift,
dim connections at rest, full glow when agents are active.
============================================================ */
class SpiderwebViz {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.nodes = {};
this.attractors = [];
this.coherence = 0;
this.animFrame = null;
this.time = 0;
// Agent positions (circular layout)
this.agents = [
'newton', 'davinci', 'empathy', 'philosophy',
'quantum', 'consciousness', 'multi_perspective', 'systems_architecture'
];
this.colors = {
newton: '#3b82f6', davinci: '#f59e0b', empathy: '#a855f7',
philosophy: '#10b981', quantum: '#ef4444', consciousness: '#e2e8f0',
multi_perspective: '#f97316', systems_architecture: '#06b6d4',
};
this.labels = {
newton: 'N', davinci: 'D', empathy: 'E', philosophy: 'P',
quantum: 'Q', consciousness: 'C', multi_perspective: 'M',
systems_architecture: 'S',
};
// Initialize with default state
this._initDefaultState();
this._resize();
this._animate();
// Handle resize
new ResizeObserver(() => this._resize()).observe(canvas.parentElement);
}
_initDefaultState() {
this.agents.forEach((name, i) => {
this.nodes[name] = {
state: [0.5, 0, 0.5, 0, 0.5], // psi, tau, chi, phi, lam
tension: 0,
active: false,
energy: 0.25,
// Each node gets a unique phase offset for ambient animation
phaseOffset: (i / this.agents.length) * Math.PI * 2,
};
});
}
_resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
this.canvas.width = rect.width * dpr;
this.canvas.height = 200 * dpr;
this.canvas.style.width = rect.width + 'px';
this.canvas.style.height = '200px';
// Reset transform before scaling β€” prevents DPR compounding on repeated resizes
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.w = rect.width;
this.h = 200;
this.cx = this.w / 2;
this.cy = this.h / 2;
this.radius = Math.min(this.w, this.h) * 0.35;
}
update(spiderwebState) {
if (!spiderwebState || !spiderwebState.nodes) return;
// Update node states
for (const [name, data] of Object.entries(spiderwebState.nodes)) {
if (this.nodes[name]) {
this.nodes[name].state = data.state || [0.5, 0, 0.5, 0, 0.5];
const tensions = data.tension_history || [];
this.nodes[name].tension = tensions.length > 0 ?
tensions[tensions.length - 1] : 0;
this.nodes[name].energy = data.state ?
data.state.reduce((s, v) => s + v * v, 0) : 0.25;
this.nodes[name].active = (data.state[0] || 0) > 0.6;
}
}
this.attractors = spiderwebState.attractors || [];
this.coherence = spiderwebState.phase_coherence || 0;
}
_getNodePos(index) {
const angle = (index / this.agents.length) * Math.PI * 2 - Math.PI / 2;
// Add gentle orbital drift
const drift = Math.sin(this.time * 0.3 + index * 0.8) * 2;
const driftY = Math.cos(this.time * 0.25 + index * 1.1) * 1.5;
return {
x: this.cx + Math.cos(angle) * this.radius + drift,
y: this.cy + Math.sin(angle) * this.radius + driftY,
};
}
_animate() {
this.time += 0.016;
this._draw();
this.animFrame = requestAnimationFrame(() => this._animate());
}
_draw() {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.w, this.h);
// ── Ambient center glow (always visible, brighter with coherence) ──
const ambientAlpha = 0.02 + (this.coherence > 0.5 ? this.coherence * 0.05 : 0);
const centerGlow = ctx.createRadialGradient(
this.cx, this.cy, 0, this.cx, this.cy, this.radius * 1.3
);
centerGlow.addColorStop(0, `rgba(59, 130, 246, ${ambientAlpha + Math.sin(this.time * 0.5) * 0.01})`);
centerGlow.addColorStop(0.6, `rgba(168, 85, 247, ${ambientAlpha * 0.5})`);
centerGlow.addColorStop(1, 'transparent');
ctx.fillStyle = centerGlow;
ctx.fillRect(0, 0, this.w, this.h);
// ── Draw edges (always visible, brighter when active/tense) ──
this.agents.forEach((nameA, i) => {
const posA = this._getNodePos(i);
this.agents.forEach((nameB, j) => {
if (j <= i) return;
const posB = this._getNodePos(j);
const nodeA = this.nodes[nameA];
const nodeB = this.nodes[nameB];
const tension = Math.abs((nodeA?.tension || 0) - (nodeB?.tension || 0));
ctx.beginPath();
ctx.moveTo(posA.x, posA.y);
ctx.lineTo(posB.x, posB.y);
const bothActive = nodeA?.active && nodeB?.active;
const eitherActive = nodeA?.active || nodeB?.active;
// Base alpha: always visible (0.12), more when active
let alpha;
if (bothActive) {
alpha = 0.25 + Math.sin(this.time * 3 + i + j) * 0.08;
} else if (eitherActive) {
alpha = 0.15 + Math.sin(this.time * 2 + i) * 0.04;
} else {
// Ambient: gentle breathing pulse on each edge
alpha = 0.08 + Math.sin(this.time * 0.8 + i * 0.7 + j * 0.5) * 0.03;
}
// Tension boosts visibility
alpha += Math.min(tension * 0.3, 0.15);
if (bothActive) {
ctx.strokeStyle = `rgba(168, 85, 247, ${alpha})`;
ctx.lineWidth = 1.5;
} else if (eitherActive) {
ctx.strokeStyle = `rgba(139, 92, 246, ${alpha})`;
ctx.lineWidth = 1;
} else {
ctx.strokeStyle = `rgba(100, 116, 139, ${alpha})`;
ctx.lineWidth = 0.5;
}
ctx.stroke();
});
});
// ── Draw attractor regions ──
this.attractors.forEach((att, ai) => {
if (!att.members || att.members.length < 2) return;
let cx = 0, cy = 0, count = 0;
att.members.forEach(name => {
const idx = this.agents.indexOf(name);
if (idx >= 0) {
const pos = this._getNodePos(idx);
cx += pos.x;
cy += pos.y;
count++;
}
});
if (count < 2) return;
cx /= count;
cy /= count;
const attRadius = 20 + count * 8;
const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, attRadius);
gradient.addColorStop(0, `rgba(168, 85, 247, ${0.08 + Math.sin(this.time * 2 + ai) * 0.03})`);
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(cx, cy, attRadius, 0, Math.PI * 2);
ctx.fill();
});
// ── Draw nodes (always visible with ambient breathing) ──
this.agents.forEach((name, i) => {
const pos = this._getNodePos(i);
const node = this.nodes[name];
const color = this.colors[name] || '#94a3b8';
const energy = node?.energy || 0.25;
const isActive = node?.active || false;
const phase = node?.phaseOffset || 0;
// Breathing pulse β€” all nodes gently pulse even at rest
const breathe = Math.sin(this.time * 1.2 + phase) * 0.3 + 0.7;
// Node glow β€” always present, stronger when active
const glowAlpha = isActive ? 0.35 : (0.08 * breathe);
const glowRadius = isActive
? 14 + Math.sin(this.time * 2 + phase) * 4
: 10 + breathe * 2;
const glow = ctx.createRadialGradient(
pos.x, pos.y, 0, pos.x, pos.y, glowRadius
);
glow.addColorStop(0, color + (isActive ? '60' : '25'));
glow.addColorStop(1, 'transparent');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pos.x, pos.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
// Node circle
const nodeRadius = isActive
? 7 + energy * 4
: 5 + breathe * 1.5;
ctx.beginPath();
ctx.arc(pos.x, pos.y, nodeRadius, 0, Math.PI * 2);
ctx.fillStyle = isActive ? color : color + '80';
ctx.fill();
// Border ring
ctx.strokeStyle = isActive ? color : color + '40';
ctx.lineWidth = isActive ? 1.5 : 0.8;
ctx.stroke();
// Label
ctx.fillStyle = isActive ? '#e2e8f0' : '#94a3b8';
ctx.font = `${isActive ? 'bold ' : ''}9px system-ui`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.labels[name], pos.x, pos.y + nodeRadius + 10);
});
// ── Coherence ring (always show a faint ring, solid when coherent) ──
const ringAlpha = this.coherence > 0
? 0.2 + this.coherence * 0.4
: 0.06 + Math.sin(this.time * 0.6) * 0.02;
const ringProgress = this.coherence > 0
? this.coherence
: 0.15 + Math.sin(this.time * 0.3) * 0.05;
ctx.beginPath();
ctx.arc(this.cx, this.cy, this.radius + 15,
-Math.PI / 2,
-Math.PI / 2 + Math.PI * 2 * ringProgress);
ctx.strokeStyle = this.coherence > 0.5
? `rgba(16, 185, 129, ${ringAlpha})`
: `rgba(100, 116, 139, ${ringAlpha})`;
ctx.lineWidth = this.coherence > 0.5 ? 2.5 : 1.5;
ctx.lineCap = 'round';
ctx.stroke();
// Coherence label
if (this.coherence > 0) {
ctx.fillStyle = '#94a3b8';
ctx.font = '9px system-ui';
ctx.textAlign = 'center';
ctx.fillText(`\u0393 ${this.coherence.toFixed(2)}`, this.cx, this.h - 8);
} else {
ctx.fillStyle = '#475569';
ctx.font = '9px system-ui';
ctx.textAlign = 'center';
ctx.fillText('idle', this.cx, this.h - 8);
}
}
destroy() {
if (this.animFrame) cancelAnimationFrame(this.animFrame);
}
}