feat: dashboard v2.1 glassmorphism + graph decomposition + fix flaky macOS vector test

Dashboard v2.1 "Nuclear" upgrade:
- Dark glassmorphism UI system (4-tier glass utilities, ambient orbs, nav glow)
- Graph3D decomposed from 806-line monolith into 10 focused modules
- Custom GLSL shaders (nebula FBM background, chromatic aberration, film grain, vignette)
- Enhanced dream mode with smooth 2s lerped transitions and aurora cycling
- Cognitive pipeline visualizer (7-stage search cascade animation)
- Temporal playback slider (scrub through memory evolution over time)
- Bioluminescent color palette for node types and events

Fix flaky CI test on macOS:
- vector::tests::test_add_and_search used near-identical test vectors (additive phase shift)
- Changed to multiplicative frequency so each seed produces a distinct vector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-03-01 21:24:10 -06:00
parent 2c1f499a8b
commit d98cf6136a
241 changed files with 6262 additions and 4884 deletions

View file

@ -0,0 +1,102 @@
import * as THREE from 'three';
import type { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import type { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export interface DreamConfig {
bloomStrength: number;
rotateSpeed: number;
fogColor: number;
fogDensity: number;
nebulaIntensity: number;
chromaticIntensity: number;
vignetteRadius: number;
breatheAmplitude: number;
}
const NORMAL_CONFIG: DreamConfig = {
bloomStrength: 0.8,
rotateSpeed: 0.3,
fogColor: 0x050510,
fogDensity: 0.008,
nebulaIntensity: 0,
chromaticIntensity: 0.002,
vignetteRadius: 0.9,
breatheAmplitude: 1.0,
};
const DREAM_CONFIG: DreamConfig = {
bloomStrength: 1.8,
rotateSpeed: 0.08,
fogColor: 0x0a0520,
fogDensity: 0.006,
nebulaIntensity: 1.0,
chromaticIntensity: 0.005,
vignetteRadius: 0.7,
breatheAmplitude: 2.0,
};
export class DreamMode {
active = false;
private transition = 0; // 0 = normal, 1 = dream
private transitionSpeed = 0.008; // ~2 seconds at 60fps
current: DreamConfig;
private auroraHue = 0;
constructor() {
this.current = { ...NORMAL_CONFIG };
}
setActive(active: boolean) {
this.active = active;
}
update(
scene: THREE.Scene,
bloomPass: UnrealBloomPass,
controls: OrbitControls,
lights: { point1: THREE.PointLight; point2: THREE.PointLight },
_time: number
) {
// Smooth transition
const target = this.active ? 1 : 0;
this.transition += (target - this.transition) * this.transitionSpeed * 60 * (1 / 60);
this.transition = Math.max(0, Math.min(1, this.transition));
const t = this.transition;
// Lerp all config values
this.current.bloomStrength = this.lerp(NORMAL_CONFIG.bloomStrength, DREAM_CONFIG.bloomStrength, t);
this.current.rotateSpeed = this.lerp(NORMAL_CONFIG.rotateSpeed, DREAM_CONFIG.rotateSpeed, t);
this.current.fogDensity = this.lerp(NORMAL_CONFIG.fogDensity, DREAM_CONFIG.fogDensity, t);
this.current.nebulaIntensity = this.lerp(NORMAL_CONFIG.nebulaIntensity, DREAM_CONFIG.nebulaIntensity, t);
this.current.chromaticIntensity = this.lerp(NORMAL_CONFIG.chromaticIntensity, DREAM_CONFIG.chromaticIntensity, t);
this.current.vignetteRadius = this.lerp(NORMAL_CONFIG.vignetteRadius, DREAM_CONFIG.vignetteRadius, t);
this.current.breatheAmplitude = this.lerp(NORMAL_CONFIG.breatheAmplitude, DREAM_CONFIG.breatheAmplitude, t);
// Apply
bloomPass.strength = this.current.bloomStrength;
controls.autoRotateSpeed = this.current.rotateSpeed;
// Fog color lerp
const normalFog = new THREE.Color(NORMAL_CONFIG.fogColor);
const dreamFog = new THREE.Color(DREAM_CONFIG.fogColor);
const fogColor = normalFog.clone().lerp(dreamFog, t);
scene.fog = new THREE.FogExp2(fogColor, this.current.fogDensity);
// Aurora color cycling during dream
if (t > 0.01) {
this.auroraHue = (_time * 0.1) % 1;
const auroraColor1 = new THREE.Color().setHSL(0.75 + this.auroraHue * 0.15, 0.8, 0.5);
const auroraColor2 = new THREE.Color().setHSL(0.55 + this.auroraHue * 0.2, 0.7, 0.4);
lights.point1.color.lerp(auroraColor1, t * 0.3);
lights.point2.color.lerp(auroraColor2, t * 0.3);
} else {
lights.point1.color.set(0x6366f1);
lights.point2.color.set(0xa855f7);
}
}
private lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
}

View file

@ -0,0 +1,53 @@
import * as THREE from 'three';
import type { GraphEdge } from '$types';
export class EdgeManager {
group: THREE.Group;
constructor() {
this.group = new THREE.Group();
}
createEdges(edges: GraphEdge[], positions: Map<string, THREE.Vector3>) {
for (const edge of edges) {
const sourcePos = positions.get(edge.source);
const targetPos = positions.get(edge.target);
if (!sourcePos || !targetPos) continue;
const points = [sourcePos, targetPos];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: 0x4a4a7a,
transparent: true,
opacity: Math.min(0.1 + edge.weight * 0.5, 0.6),
blending: THREE.AdditiveBlending,
});
const line = new THREE.Line(geometry, material);
line.userData = { source: edge.source, target: edge.target };
this.group.add(line);
}
}
updatePositions(positions: Map<string, THREE.Vector3>) {
this.group.children.forEach((child) => {
const line = child as THREE.Line;
const sourcePos = positions.get(line.userData.source);
const targetPos = positions.get(line.userData.target);
if (sourcePos && targetPos) {
const attrs = line.geometry.attributes.position as THREE.BufferAttribute;
attrs.setXYZ(0, sourcePos.x, sourcePos.y, sourcePos.z);
attrs.setXYZ(1, targetPos.x, targetPos.y, targetPos.z);
attrs.needsUpdate = true;
}
});
}
dispose() {
this.group.children.forEach((child) => {
const line = child as THREE.Line;
line.geometry?.dispose();
(line.material as THREE.Material)?.dispose();
});
}
}

View file

@ -0,0 +1,204 @@
import * as THREE from 'three';
export interface PulseEffect {
nodeId: string;
intensity: number;
color: THREE.Color;
decay: number;
}
interface SpawnBurst {
position: THREE.Vector3;
age: number;
particles: THREE.Points;
}
interface Shockwave {
mesh: THREE.Mesh;
age: number;
maxAge: number;
}
interface ConnectionFlash {
line: THREE.Line;
intensity: number;
}
export class EffectManager {
pulseEffects: PulseEffect[] = [];
private spawnBursts: SpawnBurst[] = [];
private shockwaves: Shockwave[] = [];
private connectionFlashes: ConnectionFlash[] = [];
private scene: THREE.Scene;
constructor(scene: THREE.Scene) {
this.scene = scene;
}
addPulse(nodeId: string, intensity: number, color: THREE.Color, decay: number) {
this.pulseEffects.push({ nodeId, intensity, color, decay });
}
createSpawnBurst(position: THREE.Vector3, color: THREE.Color) {
const count = 60;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
positions[i * 3] = position.x;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const speed = 0.3 + Math.random() * 0.5;
velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
velocities[i * 3 + 1] = Math.sin(phi) * Math.sin(theta) * speed;
velocities[i * 3 + 2] = Math.cos(phi) * speed;
}
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
const mat = new THREE.PointsMaterial({
color,
size: 0.6,
transparent: true,
opacity: 1.0,
blending: THREE.AdditiveBlending,
sizeAttenuation: true,
});
const pts = new THREE.Points(geo, mat);
this.scene.add(pts);
this.spawnBursts.push({ position: position.clone(), age: 0, particles: pts });
}
createShockwave(position: THREE.Vector3, color: THREE.Color, camera: THREE.Camera) {
const geo = new THREE.RingGeometry(0.1, 0.5, 64);
const mat = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending,
});
const ring = new THREE.Mesh(geo, mat);
ring.position.copy(position);
ring.lookAt(camera.position);
this.scene.add(ring);
this.shockwaves.push({ mesh: ring, age: 0, maxAge: 60 });
}
createConnectionFlash(from: THREE.Vector3, to: THREE.Vector3, color: THREE.Color) {
const points = [from.clone(), to.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({
color,
transparent: true,
opacity: 1.0,
blending: THREE.AdditiveBlending,
});
const line = new THREE.Line(geo, mat);
this.scene.add(line);
this.connectionFlashes.push({ line, intensity: 1.0 });
}
update(nodeMeshMap: Map<string, THREE.Mesh>, camera: THREE.Camera) {
// Pulse effects
for (let i = this.pulseEffects.length - 1; i >= 0; i--) {
const pulse = this.pulseEffects[i];
pulse.intensity -= pulse.decay;
if (pulse.intensity <= 0) {
this.pulseEffects.splice(i, 1);
continue;
}
const mesh = nodeMeshMap.get(pulse.nodeId);
if (mesh) {
const mat = mesh.material as THREE.MeshStandardMaterial;
mat.emissive.lerp(pulse.color, pulse.intensity * 0.3);
mat.emissiveIntensity = Math.max(mat.emissiveIntensity, pulse.intensity);
}
}
// Spawn bursts
for (let i = this.spawnBursts.length - 1; i >= 0; i--) {
const burst = this.spawnBursts[i];
burst.age++;
if (burst.age > 120) {
this.scene.remove(burst.particles);
burst.particles.geometry.dispose();
(burst.particles.material as THREE.Material).dispose();
this.spawnBursts.splice(i, 1);
continue;
}
const positions = burst.particles.geometry.attributes.position as THREE.BufferAttribute;
const vels = burst.particles.geometry.attributes.velocity as THREE.BufferAttribute;
for (let j = 0; j < positions.count; j++) {
positions.setX(j, positions.getX(j) + vels.getX(j));
positions.setY(j, positions.getY(j) + vels.getY(j));
positions.setZ(j, positions.getZ(j) + vels.getZ(j));
vels.setX(j, vels.getX(j) * 0.97);
vels.setY(j, vels.getY(j) * 0.97);
vels.setZ(j, vels.getZ(j) * 0.97);
}
positions.needsUpdate = true;
const mat = burst.particles.material as THREE.PointsMaterial;
mat.opacity = Math.max(0, 1 - burst.age / 120);
mat.size = 0.6 * (1 - burst.age / 200);
}
// Shockwaves
for (let i = this.shockwaves.length - 1; i >= 0; i--) {
const sw = this.shockwaves[i];
sw.age++;
if (sw.age > sw.maxAge) {
this.scene.remove(sw.mesh);
sw.mesh.geometry.dispose();
(sw.mesh.material as THREE.Material).dispose();
this.shockwaves.splice(i, 1);
continue;
}
const progress = sw.age / sw.maxAge;
sw.mesh.scale.setScalar(1 + progress * 20);
(sw.mesh.material as THREE.MeshBasicMaterial).opacity = 0.8 * (1 - progress);
sw.mesh.lookAt(camera.position);
}
// Connection flashes
for (let i = this.connectionFlashes.length - 1; i >= 0; i--) {
const flash = this.connectionFlashes[i];
flash.intensity -= 0.015;
if (flash.intensity <= 0) {
this.scene.remove(flash.line);
flash.line.geometry.dispose();
(flash.line.material as THREE.Material).dispose();
this.connectionFlashes.splice(i, 1);
continue;
}
(flash.line.material as THREE.LineBasicMaterial).opacity = flash.intensity;
}
}
dispose() {
for (const burst of this.spawnBursts) {
this.scene.remove(burst.particles);
burst.particles.geometry.dispose();
(burst.particles.material as THREE.Material).dispose();
}
for (const sw of this.shockwaves) {
this.scene.remove(sw.mesh);
sw.mesh.geometry.dispose();
(sw.mesh.material as THREE.Material).dispose();
}
for (const flash of this.connectionFlashes) {
this.scene.remove(flash.line);
flash.line.geometry.dispose();
(flash.line.material as THREE.Material).dispose();
}
this.pulseEffects = [];
this.spawnBursts = [];
this.shockwaves = [];
this.connectionFlashes = [];
}
}

View file

@ -0,0 +1,98 @@
import * as THREE from 'three';
import type { VestigeEvent } from '$types';
import type { EffectManager } from './effects';
export function mapEventToEffects(
event: VestigeEvent,
effects: EffectManager,
nodePositions: Map<string, THREE.Vector3>,
nodeMeshMap: Map<string, THREE.Mesh>,
camera: THREE.Camera
) {
switch (event.type) {
case 'MemoryCreated': {
const nodeId = (event.data as { id?: string })?.id;
const pos = nodeId ? nodePositions.get(nodeId) : null;
const burstPos =
pos?.clone() ??
new THREE.Vector3(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40
);
effects.createSpawnBurst(burstPos, new THREE.Color(0x00ffd1));
effects.createShockwave(burstPos, new THREE.Color(0x00ffd1), camera);
break;
}
case 'SearchPerformed': {
nodeMeshMap.forEach((_, id) => {
effects.addPulse(id, 0.6 + Math.random() * 0.4, new THREE.Color(0x818cf8), 0.02);
});
break;
}
case 'DreamStarted': {
nodeMeshMap.forEach((_, id) => {
effects.addPulse(id, 1.0, new THREE.Color(0xa855f7), 0.005);
});
break;
}
case 'DreamProgress': {
const memoryId = (event.data as { memory_id?: string })?.memory_id;
if (memoryId && nodeMeshMap.has(memoryId)) {
effects.addPulse(memoryId, 1.5, new THREE.Color(0xc084fc), 0.01);
}
break;
}
case 'DreamCompleted': {
effects.createSpawnBurst(new THREE.Vector3(0, 0, 0), new THREE.Color(0xa855f7));
effects.createShockwave(new THREE.Vector3(0, 0, 0), new THREE.Color(0xa855f7), camera);
break;
}
case 'ConnectionDiscovered': {
const data = event.data as { source_id?: string; target_id?: string };
const srcPos = data.source_id ? nodePositions.get(data.source_id) : null;
const tgtPos = data.target_id ? nodePositions.get(data.target_id) : null;
if (srcPos && tgtPos) {
effects.createConnectionFlash(srcPos, tgtPos, new THREE.Color(0x00d4ff));
}
break;
}
case 'RetentionDecayed': {
const decayId = (event.data as { id?: string })?.id;
if (decayId && nodeMeshMap.has(decayId)) {
effects.addPulse(decayId, 0.8, new THREE.Color(0xff4757), 0.03);
}
break;
}
case 'MemoryPromoted': {
const promoId = (event.data as { id?: string })?.id;
if (promoId && nodeMeshMap.has(promoId)) {
effects.addPulse(promoId, 1.2, new THREE.Color(0x00ff88), 0.01);
const promoPos = nodePositions.get(promoId);
if (promoPos) effects.createShockwave(promoPos, new THREE.Color(0x00ff88), camera);
}
break;
}
case 'ConsolidationCompleted': {
nodeMeshMap.forEach((_, id) => {
effects.addPulse(id, 0.4 + Math.random() * 0.3, new THREE.Color(0xffb800), 0.015);
});
break;
}
case 'ActivationSpread': {
const spreadData = event.data as { source_id?: string; target_ids?: string[] };
if (spreadData.source_id && spreadData.target_ids) {
const srcPos = nodePositions.get(spreadData.source_id);
if (srcPos) {
for (const targetId of spreadData.target_ids) {
const tgtPos = nodePositions.get(targetId);
if (tgtPos) {
effects.createConnectionFlash(srcPos, tgtPos, new THREE.Color(0x14e8c6));
}
}
}
}
break;
}
}
}

View file

@ -0,0 +1,77 @@
import * as THREE from 'three';
import type { GraphEdge } from '$types';
export class ForceSimulation {
positions: Map<string, THREE.Vector3>;
velocities: Map<string, THREE.Vector3>;
running = true;
step = 0;
private readonly repulsionStrength = 500;
private readonly attractionStrength = 0.01;
private readonly dampening = 0.9;
private readonly maxSteps = 300;
constructor(positions: Map<string, THREE.Vector3>) {
this.positions = positions;
this.velocities = new Map();
for (const id of positions.keys()) {
this.velocities.set(id, new THREE.Vector3());
}
}
tick(edges: GraphEdge[]) {
if (!this.running || this.step > this.maxSteps) return;
this.step++;
const alpha = Math.max(0.001, 1 - this.step / this.maxSteps);
const nodeIds = Array.from(this.positions.keys());
// Repulsion between all nodes
for (let i = 0; i < nodeIds.length; i++) {
for (let j = i + 1; j < nodeIds.length; j++) {
const posA = this.positions.get(nodeIds[i])!;
const posB = this.positions.get(nodeIds[j])!;
const diff = new THREE.Vector3().subVectors(posA, posB);
const dist = diff.length() || 1;
const force = (this.repulsionStrength / (dist * dist)) * alpha;
const dir = diff.normalize().multiplyScalar(force);
this.velocities.get(nodeIds[i])!.add(dir);
this.velocities.get(nodeIds[j])!.sub(dir);
}
}
// Attraction along edges
for (const edge of edges) {
const posA = this.positions.get(edge.source);
const posB = this.positions.get(edge.target);
if (!posA || !posB) continue;
const diff = new THREE.Vector3().subVectors(posB, posA);
const dist = diff.length();
const force = dist * this.attractionStrength * edge.weight * alpha;
const dir = diff.normalize().multiplyScalar(force);
this.velocities.get(edge.source)!.add(dir);
this.velocities.get(edge.target)!.sub(dir);
}
// Centering force + velocity integration
for (const id of nodeIds) {
const pos = this.positions.get(id)!;
const vel = this.velocities.get(id)!;
vel.sub(pos.clone().multiplyScalar(0.001 * alpha));
vel.multiplyScalar(this.dampening);
pos.add(vel);
}
}
reset() {
this.step = 0;
this.running = true;
for (const vel of this.velocities.values()) {
vel.set(0, 0, 0);
}
}
}

View file

@ -0,0 +1,196 @@
import * as THREE from 'three';
import type { GraphNode } from '$types';
import { NODE_TYPE_COLORS } from '$types';
export class NodeManager {
group: THREE.Group;
meshMap = new Map<string, THREE.Mesh>();
positions = new Map<string, THREE.Vector3>();
labelSprites = new Map<string, THREE.Sprite>();
hoveredNode: string | null = null;
selectedNode: string | null = null;
constructor() {
this.group = new THREE.Group();
}
createNodes(nodes: GraphNode[]): Map<string, THREE.Vector3> {
const phi = (1 + Math.sqrt(5)) / 2;
const count = nodes.length;
for (let i = 0; i < count; i++) {
const node = nodes[i];
// Fibonacci sphere distribution for initial positions
const y = 1 - (2 * i) / (count - 1 || 1);
const radius = Math.sqrt(1 - y * y);
const theta = (2 * Math.PI * i) / phi;
const spread = 30 + count * 0.5;
const pos = new THREE.Vector3(
radius * Math.cos(theta) * spread,
y * spread,
radius * Math.sin(theta) * spread
);
if (node.isCenter) pos.set(0, 0, 0);
this.positions.set(node.id, pos);
const size = 0.5 + node.retention * 2;
const color = NODE_TYPE_COLORS[node.type] || '#8B95A5';
// Node mesh
const geometry = new THREE.SphereGeometry(size, 16, 16);
const material = new THREE.MeshStandardMaterial({
color: new THREE.Color(color),
emissive: new THREE.Color(color),
emissiveIntensity: 0.3 + node.retention * 0.5,
roughness: 0.3,
metalness: 0.1,
transparent: true,
opacity: 0.3 + node.retention * 0.7,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(pos);
mesh.userData = { nodeId: node.id, type: node.type, retention: node.retention };
this.meshMap.set(node.id, mesh);
this.group.add(mesh);
// Glow sprite
const spriteMat = new THREE.SpriteMaterial({
color: new THREE.Color(color),
transparent: true,
opacity: 0.15 + node.retention * 0.2,
blending: THREE.AdditiveBlending,
});
const sprite = new THREE.Sprite(spriteMat);
sprite.scale.set(size * 4, size * 4, 1);
sprite.position.copy(pos);
sprite.userData = { isGlow: true, nodeId: node.id };
this.group.add(sprite);
// Text label sprite
const labelText = node.label || node.type;
const labelSprite = this.createTextSprite(labelText, '#e2e8f0');
labelSprite.position.copy(pos);
labelSprite.position.y += size * 2 + 1.5;
labelSprite.userData = { isLabel: true, nodeId: node.id, offset: size * 2 + 1.5 };
this.group.add(labelSprite);
this.labelSprites.set(node.id, labelSprite);
}
return this.positions;
}
private createTextSprite(text: string, color: string): THREE.Sprite {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = 512;
canvas.height = 64;
const label = text.length > 40 ? text.slice(0, 37) + '...' : text;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 6;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 2;
ctx.fillStyle = color;
ctx.fillText(label, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
const mat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 0,
depthTest: false,
sizeAttenuation: true,
});
const sprite = new THREE.Sprite(mat);
sprite.scale.set(12, 1.5, 1);
return sprite;
}
updatePositions() {
this.group.children.forEach((child) => {
if (child.userData.nodeId) {
const pos = this.positions.get(child.userData.nodeId);
if (!pos) return;
if (child.userData.isGlow) {
child.position.copy(pos);
} else if (child.userData.isLabel) {
child.position.copy(pos);
child.position.y += child.userData.offset;
} else if (child instanceof THREE.Mesh) {
child.position.copy(pos);
}
}
});
}
animate(time: number, nodes: GraphNode[], camera: THREE.PerspectiveCamera) {
// Node breathing
this.meshMap.forEach((mesh, id) => {
const node = nodes.find((n) => n.id === id);
if (!node) return;
const breathe =
1 + Math.sin(time * 1.5 + nodes.indexOf(node) * 0.5) * 0.15 * node.retention;
mesh.scale.setScalar(breathe);
const mat = mesh.material as THREE.MeshStandardMaterial;
if (id === this.hoveredNode) {
mat.emissiveIntensity = 1.0;
} else if (id === this.selectedNode) {
mat.emissiveIntensity = 0.8;
} else {
// Low-retention nodes breathe slower
const baseIntensity = 0.3 + node.retention * 0.5;
const breatheIntensity =
baseIntensity + Math.sin(time * (0.8 + node.retention * 0.7)) * 0.1 * node.retention;
mat.emissiveIntensity = breatheIntensity;
}
});
// Distance-based label visibility
this.labelSprites.forEach((sprite, id) => {
const pos = this.positions.get(id);
if (!pos) return;
const dist = camera.position.distanceTo(pos);
const mat = sprite.material as THREE.SpriteMaterial;
const targetOpacity =
id === this.hoveredNode || id === this.selectedNode
? 1.0
: dist < 40
? 0.9
: dist < 80
? 0.9 * (1 - (dist - 40) / 40)
: 0;
mat.opacity += (targetOpacity - mat.opacity) * 0.1;
});
}
getMeshes(): THREE.Mesh[] {
return Array.from(this.meshMap.values());
}
dispose() {
this.group.traverse((obj) => {
if (obj instanceof THREE.Mesh) {
obj.geometry?.dispose();
(obj.material as THREE.Material)?.dispose();
} else if (obj instanceof THREE.Sprite) {
(obj.material as THREE.SpriteMaterial)?.map?.dispose();
(obj.material as THREE.Material)?.dispose();
}
});
}
}

View file

@ -0,0 +1,92 @@
import * as THREE from 'three';
export class ParticleSystem {
starField: THREE.Points;
neuralParticles: THREE.Points;
constructor(scene: THREE.Scene) {
this.starField = this.createStarField();
this.neuralParticles = this.createNeuralParticles();
scene.add(this.starField);
scene.add(this.neuralParticles);
}
private createStarField(): THREE.Points {
const count = 3000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const sizes = new Float32Array(count);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 1000;
positions[i * 3 + 1] = (Math.random() - 0.5) * 1000;
positions[i * 3 + 2] = (Math.random() - 0.5) * 1000;
sizes[i] = Math.random() * 1.5;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const material = new THREE.PointsMaterial({
color: 0x6366f1,
size: 0.5,
transparent: true,
opacity: 0.4,
sizeAttenuation: true,
blending: THREE.AdditiveBlending,
});
return new THREE.Points(geometry, material);
}
private createNeuralParticles(): THREE.Points {
const count = 500;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = (Math.random() - 0.5) * 100;
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
colors[i * 3] = 0.4 + Math.random() * 0.3;
colors[i * 3 + 1] = 0.3 + Math.random() * 0.2;
colors[i * 3 + 2] = 0.8 + Math.random() * 0.2;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.3,
vertexColors: true,
transparent: true,
opacity: 0.4,
blending: THREE.AdditiveBlending,
sizeAttenuation: true,
});
return new THREE.Points(geometry, material);
}
animate(time: number) {
// Star rotation
this.starField.rotation.y += 0.0001;
this.starField.rotation.x += 0.00005;
// Neural particle motion
const positions = this.neuralParticles.geometry.attributes.position as THREE.BufferAttribute;
for (let i = 0; i < positions.count; i++) {
positions.setY(i, positions.getY(i) + Math.sin(time + i * 0.1) * 0.02);
positions.setX(i, positions.getX(i) + Math.cos(time + i * 0.05) * 0.01);
}
positions.needsUpdate = true;
}
dispose() {
this.starField.geometry.dispose();
(this.starField.material as THREE.Material).dispose();
this.neuralParticles.geometry.dispose();
(this.neuralParticles.material as THREE.Material).dispose();
}
}

View file

@ -0,0 +1,116 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
export interface SceneContext {
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;
renderer: THREE.WebGLRenderer;
controls: OrbitControls;
composer: EffectComposer;
bloomPass: UnrealBloomPass;
raycaster: THREE.Raycaster;
mouse: THREE.Vector2;
lights: {
ambient: THREE.AmbientLight;
point1: THREE.PointLight;
point2: THREE.PointLight;
};
}
export function createScene(container: HTMLDivElement): SceneContext {
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050510, 0.008);
const camera = new THREE.PerspectiveCamera(
60,
container.clientWidth / container.clientHeight,
0.1,
2000
);
camera.position.set(0, 30, 80);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: 'high-performance',
});
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.rotateSpeed = 0.5;
controls.zoomSpeed = 0.8;
controls.minDistance = 10;
controls.maxDistance = 500;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.3;
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(container.clientWidth, container.clientHeight),
0.8,
0.4,
0.85
);
composer.addPass(bloomPass);
const ambient = new THREE.AmbientLight(0x1a1a3a, 0.5);
scene.add(ambient);
const point1 = new THREE.PointLight(0x6366f1, 1.5, 200);
point1.position.set(50, 50, 50);
scene.add(point1);
const point2 = new THREE.PointLight(0xa855f7, 1, 200);
point2.position.set(-50, -30, -50);
scene.add(point2);
const raycaster = new THREE.Raycaster();
raycaster.params.Points = { threshold: 2 };
const mouse = new THREE.Vector2();
return {
scene,
camera,
renderer,
controls,
composer,
bloomPass,
raycaster,
mouse,
lights: { ambient, point1, point2 },
};
}
export function resizeScene(ctx: SceneContext, container: HTMLDivElement) {
const w = container.clientWidth;
const h = container.clientHeight;
ctx.camera.aspect = w / h;
ctx.camera.updateProjectionMatrix();
ctx.renderer.setSize(w, h);
ctx.composer.setSize(w, h);
}
export function disposeScene(ctx: SceneContext) {
ctx.scene.traverse((obj: THREE.Object3D) => {
if (obj instanceof THREE.Mesh || obj instanceof THREE.InstancedMesh) {
obj.geometry?.dispose();
if (Array.isArray(obj.material)) {
obj.material.forEach((m: THREE.Material) => m.dispose());
} else if (obj.material) {
(obj.material as THREE.Material).dispose();
}
}
});
ctx.renderer.dispose();
ctx.composer.dispose();
}

View file

@ -0,0 +1,146 @@
import * as THREE from 'three';
// Domain-warped FBM noise nebula background shader
const vertexShader = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`;
const fragmentShader = /* glsl */ `
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform float uDreamIntensity;
varying vec2 vUv;
// Simplex-style hash
vec3 hash33(vec3 p3) {
p3 = fract(p3 * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yxz + 33.33);
return fract((p3.xxy + p3.yxx) * p3.zyx);
}
// 3D value noise
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float n = i.x + i.y * 157.0 + 113.0 * i.z;
vec4 v1 = fract(sin(vec4(n + 0.0, n + 1.0, n + 157.0, n + 158.0)) * 43758.5453);
vec4 v2 = fract(sin(vec4(n + 113.0, n + 114.0, n + 270.0, n + 271.0)) * 43758.5453);
vec4 a = mix(v1, v2, f.z);
vec2 b = mix(a.xy, a.zw, f.y);
return mix(b.x, b.y, f.x);
}
// FBM with 5 octaves
float fbm(vec3 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for (int i = 0; i < 5; i++) {
value += amplitude * noise(p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return value;
}
// IQ cosine palette
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b * cos(6.28318 * (c * t + d));
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution.xy) / min(uResolution.x, uResolution.y);
float t = uTime * 0.05;
// Domain warping: fbm(p + fbm(p + fbm(p)))
vec3 p = vec3(uv * 2.0, t);
float warp1 = fbm(p);
float warp2 = fbm(p + warp1 * 3.0 + vec3(1.7, 9.2, t * 0.3));
float warp3 = fbm(p + warp2 * 2.5 + vec3(8.3, 2.8, t * 0.2));
// Final noise value
float f = fbm(p + warp3 * 2.0);
// Color: cosmic palette that shifts during dream mode
vec3 normalA = vec3(0.02, 0.01, 0.05);
vec3 normalB = vec3(0.03, 0.02, 0.08);
vec3 normalC = vec3(1.0, 1.0, 1.0);
vec3 normalD = vec3(0.70, 0.55, 0.80);
vec3 dreamA = vec3(0.05, 0.01, 0.08);
vec3 dreamB = vec3(0.06, 0.03, 0.12);
vec3 dreamC = vec3(1.0, 0.8, 1.0);
vec3 dreamD = vec3(0.80, 0.40, 0.90);
vec3 a = mix(normalA, dreamA, uDreamIntensity);
vec3 b = mix(normalB, dreamB, uDreamIntensity);
vec3 c = mix(normalC, dreamC, uDreamIntensity);
vec3 d = mix(normalD, dreamD, uDreamIntensity);
vec3 color = palette(f + warp2 * 0.5, a, b, c, d);
// Add subtle star-like highlights
float stars = smoothstep(0.97, 1.0, noise(vec3(uv * 50.0, t * 0.1)));
color += stars * 0.15;
// Intensity modulation
float intensity = 0.15 + 0.1 * uDreamIntensity;
color *= intensity;
// Vignette
float dist = length(uv);
color *= smoothstep(1.5, 0.3, dist);
gl_FragColor = vec4(color, 1.0);
}
`;
export function createNebulaBackground(scene: THREE.Scene): {
mesh: THREE.Mesh;
material: THREE.ShaderMaterial;
} {
const geometry = new THREE.PlaneGeometry(2, 2);
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
uDreamIntensity: { value: 0 },
},
depthWrite: false,
depthTest: false,
transparent: false,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = false;
mesh.renderOrder = -1000;
scene.add(mesh);
return { mesh, material };
}
export function updateNebula(
material: THREE.ShaderMaterial,
time: number,
dreamIntensity: number,
width: number,
height: number
) {
material.uniforms.uTime.value = time;
material.uniforms.uDreamIntensity.value = dreamIntensity;
material.uniforms.uResolution.value.set(width, height);
}

View file

@ -0,0 +1,147 @@
import * as THREE from 'three';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import type { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
// Chromatic Aberration
const ChromaticAberrationShader = {
uniforms: {
tDiffuse: { value: null },
uIntensity: { value: 0.002 },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform sampler2D tDiffuse;
uniform float uIntensity;
varying vec2 vUv;
void main() {
vec2 center = vec2(0.5);
vec2 dir = vUv - center;
float dist = length(dir);
float rOffset = uIntensity * dist;
float gOffset = 0.0;
float bOffset = -uIntensity * dist;
vec2 rUv = vUv + dir * rOffset;
vec2 gUv = vUv + dir * gOffset;
vec2 bUv = vUv + dir * bOffset;
float r = texture2D(tDiffuse, rUv).r;
float g = texture2D(tDiffuse, gUv).g;
float b = texture2D(tDiffuse, bUv).b;
gl_FragColor = vec4(r, g, b, 1.0);
}
`,
};
// Film Grain
const FilmGrainShader = {
uniforms: {
tDiffuse: { value: null },
uTime: { value: 0 },
uIntensity: { value: 0.04 },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform sampler2D tDiffuse;
uniform float uTime;
uniform float uIntensity;
varying vec2 vUv;
float rand(vec2 co) {
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
vec4 color = texture2D(tDiffuse, vUv);
float grain = rand(vUv + vec2(uTime)) * 2.0 - 1.0;
color.rgb += grain * uIntensity;
gl_FragColor = color;
}
`,
};
// Vignette
const VignetteShader = {
uniforms: {
tDiffuse: { value: null },
uRadius: { value: 0.9 },
uSoftness: { value: 0.5 },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform sampler2D tDiffuse;
uniform float uRadius;
uniform float uSoftness;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
vec2 center = vec2(0.5);
float dist = distance(vUv, center) * 1.414;
float vignette = smoothstep(uRadius, uRadius - uSoftness, dist);
color.rgb *= vignette;
gl_FragColor = color;
}
`,
};
export interface PostProcessingStack {
chromatic: ShaderPass;
grain: ShaderPass;
vignette: ShaderPass;
}
export function createPostProcessing(composer: EffectComposer): PostProcessingStack {
const chromatic = new ShaderPass(ChromaticAberrationShader);
const grain = new ShaderPass(FilmGrainShader);
const vignette = new ShaderPass(VignetteShader);
composer.addPass(chromatic);
composer.addPass(grain);
composer.addPass(vignette);
return { chromatic, grain, vignette };
}
export function updatePostProcessing(
stack: PostProcessingStack,
time: number,
dreamIntensity: number
) {
// Chromatic aberration: doubles during dream
const chromaticBase = 0.002;
const chromaticDream = 0.005;
stack.chromatic.uniforms.uIntensity.value =
chromaticBase + (chromaticDream - chromaticBase) * dreamIntensity;
// Film grain: animated
stack.grain.uniforms.uTime.value = time;
stack.grain.uniforms.uIntensity.value = 0.04 + dreamIntensity * 0.02;
// Vignette: tighter during dream
const vignetteBase = 0.9;
const vignetteDream = 0.7;
stack.vignette.uniforms.uRadius.value =
vignetteBase + (vignetteDream - vignetteBase) * dreamIntensity;
}

View file

@ -0,0 +1,93 @@
import type { GraphNode, GraphEdge } from '$types';
export interface TemporalState {
visibleNodes: GraphNode[];
visibleEdges: GraphEdge[];
nodeOpacities: Map<string, number>;
}
/**
* Filter nodes and edges by a temporal cutoff date.
* Nodes are visible if createdAt <= cutoffDate.
* Edges are visible if both endpoints are visible.
*/
export function filterByDate(
nodes: GraphNode[],
edges: GraphEdge[],
cutoffDate: Date
): TemporalState {
const cutoff = cutoffDate.getTime();
const visibleNodeIds = new Set<string>();
const nodeOpacities = new Map<string, number>();
const visibleNodes = nodes.filter((node) => {
const created = new Date(node.createdAt).getTime();
if (created <= cutoff) {
visibleNodeIds.add(node.id);
// Nodes created near the cutoff date get a fade-in opacity
const age = cutoff - created;
const fadeWindow = 24 * 60 * 60 * 1000; // 1 day fade window
const opacity = age < fadeWindow ? 0.3 + 0.7 * (age / fadeWindow) : 1.0;
nodeOpacities.set(node.id, opacity);
return true;
}
return false;
});
const visibleEdges = edges.filter(
(edge) => visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target)
);
return { visibleNodes, visibleEdges, nodeOpacities };
}
/**
* Calculate what retention would have been at a given historical date.
* Uses FSRS-6 decay formula: R(t) = exp(-t / S)
*/
export function retentionAtDate(
currentRetention: number,
stability: number,
nodeCreatedAt: string,
targetDate: Date,
now: Date = new Date()
): number {
const S = Math.max(stability, 0.1);
const nowMs = now.getTime();
const targetMs = targetDate.getTime();
const createdMs = new Date(nodeCreatedAt).getTime();
if (targetMs < createdMs) return 0;
// Time elapsed from creation to target date (in days)
const elapsedDays = (targetMs - createdMs) / (24 * 60 * 60 * 1000);
// R(t) = e^(-t/S)
return Math.exp(-elapsedDays / S);
}
/**
* Get the date range from a set of nodes (oldest to newest).
*/
export function getDateRange(nodes: GraphNode[]): { oldest: Date; newest: Date } {
if (nodes.length === 0) {
const now = new Date();
return { oldest: now, newest: now };
}
let oldest = Infinity;
let newest = -Infinity;
for (const node of nodes) {
const ts = new Date(node.createdAt).getTime();
if (ts < oldest) oldest = ts;
if (ts > newest) newest = ts;
}
return {
oldest: new Date(oldest),
newest: new Date(newest),
};
}