From bcf2ff87fa485d58aae11bbbd1be0fb228ed47b1 Mon Sep 17 00:00:00 2001 From: Vorflux AI Date: Sat, 28 Mar 2026 07:41:03 +0000 Subject: [PATCH] feat: improve graph aesthetics - subtle edges, luminous nodes, better spacing - Edge opacity: derives 0.18, updates 0.45, extends 0.12 for gossamer web effect - Edge colors: sky blue (#38BDF8), soft violet (#A78BFA), teal (#2DD4BF) - Node glow: subtle radial glow behind memory dots for luminous effect - Force layout: stronger repulsion (-2500), wider spacing (450/200), gentler centering - Refactor simulation to use FORCE_CONFIG constants instead of hardcoded values - Update MCP app edge colors to match --- apps/mcp/src/ui/mcp-app.ts | 12 ++-- packages/memory-graph/src/canvas/renderer.ts | 58 ++++++++++++++----- .../memory-graph/src/canvas/simulation.ts | 43 +++++++++----- packages/memory-graph/src/constants.ts | 22 +++---- 4 files changed, 91 insertions(+), 44 deletions(-) diff --git a/apps/mcp/src/ui/mcp-app.ts b/apps/mcp/src/ui/mcp-app.ts index bde44e15..504f190d 100644 --- a/apps/mcp/src/ui/mcp-app.ts +++ b/apps/mcp/src/ui/mcp-app.ts @@ -99,14 +99,14 @@ const MEMORY_BORDER = { const EDGE_COLORS = { dark: { - derives: "#3B82F6", - updates: "#8B5CF6", - extends: "#14B8A6", + derives: "#38BDF8", + updates: "#A78BFA", + extends: "#2DD4BF", }, light: { - derives: "#60A5FA", - updates: "#8B5CF6", - extends: "#2DD4BF", + derives: "#7DD3FC", + updates: "#A78BFA", + extends: "#5EEAD4", }, } diff --git a/packages/memory-graph/src/canvas/renderer.ts b/packages/memory-graph/src/canvas/renderer.ts index 07575288..169b7ff0 100644 --- a/packages/memory-graph/src/canvas/renderer.ts +++ b/packages/memory-graph/src/canvas/renderer.ts @@ -41,18 +41,22 @@ export function renderFrame( function edgeStyle( edge: GraphEdge, colors: GraphThemeColors, -): { color: string; width: number } { +): { color: string; width: number; opacity: number } { if (edge.edgeType === "derives") - return { color: colors.edgeDerives, width: 1.5 } + return { color: colors.edgeDerives, width: 0.8, opacity: 0.18 } if (edge.edgeType === "updates") - return { color: colors.edgeUpdates, width: 2 } + return { color: colors.edgeUpdates, width: 1.2, opacity: 0.45 } if (edge.edgeType === "extends") - return { color: colors.edgeExtends, width: 1 } - return { color: colors.edgeExtends, width: 1 } + return { color: colors.edgeExtends, width: 0.6, opacity: 0.12 } + return { color: colors.edgeExtends, width: 0.6, opacity: 0.12 } } -function batchKey(style: { color: string; width: number }): string { - return `${style.color}|${style.width}` +function batchKey(style: { + color: string + width: number + opacity: number +}): string { + return `${style.color}|${style.width}|${style.opacity}` } interface PreparedEdge { @@ -61,7 +65,7 @@ interface PreparedEdge { endX: number endY: number connected: boolean - style: { color: string; width: number } + style: { color: string; width: number; opacity: number } isUpdates: boolean arrowSize: number } @@ -161,14 +165,13 @@ function drawEdges( if (!first) continue const isDimmed = key.endsWith("|d") - // Draw glow pass behind updates edges for visual emphasis - // Updates edges share a distinct style key, so a batch is all-updates or none + // Draw subtle glow pass behind updates edges const isUpdatesBatch = first.isUpdates if (isUpdatesBatch && !isDimmed) { ctx.save() - ctx.globalAlpha = 0.3 + ctx.globalAlpha = first.style.opacity * 0.4 ctx.strokeStyle = first.style.color - ctx.lineWidth = first.style.width + 4 + ctx.lineWidth = first.style.width + 2 ctx.beginPath() for (const e of batch) { ctx.moveTo(e.startX, e.startY) @@ -178,7 +181,10 @@ function drawEdges( ctx.restore() } - ctx.globalAlpha = isDimmed ? 1 - state.dimProgress * 0.8 : 1 + const baseAlpha = first.style.opacity + ctx.globalAlpha = isDimmed + ? baseAlpha * (1 - state.dimProgress * 0.8) + : baseAlpha ctx.strokeStyle = first.style.color ctx.lineWidth = first.style.width @@ -190,6 +196,9 @@ function drawEdges( ctx.stroke() if (isUpdatesBatch) { + ctx.globalAlpha = isDimmed + ? first.style.opacity * 0.6 * (1 - state.dimProgress * 0.8) + : first.style.opacity * 0.6 ctx.fillStyle = first.style.color for (const e of batch) { drawArrowHead(ctx, e.startX, e.startY, e.endX, e.endY, e.arrowSize) @@ -343,6 +352,28 @@ function drawNodes( const dimmedDots = memDots.filter((d) => d.dimmed) if (normalDots.length > 0) { + // Subtle glow behind memory dots for luminous effect + ctx.globalAlpha = dimAlpha * 0.25 + const byColorGlow = new Map() + for (const d of normalDots) { + let batch = byColorGlow.get(d.color) + if (!batch) { + batch = [] + byColorGlow.set(d.color, batch) + } + batch.push(d) + } + for (const [color, batch] of byColorGlow) { + ctx.fillStyle = color + ctx.beginPath() + for (const d of batch) { + ctx.moveTo(d.x + d.r * 2.5, d.y) + ctx.arc(d.x, d.y, d.r * 2.5, 0, Math.PI * 2) + } + ctx.fill() + } + + // Filled dot ctx.globalAlpha = dimAlpha ctx.fillStyle = colors.memFill ctx.beginPath() @@ -352,6 +383,7 @@ function drawNodes( } ctx.fill() + // Colored border ctx.lineWidth = 1.5 const byColor = new Map() for (const d of normalDots) { diff --git a/packages/memory-graph/src/canvas/simulation.ts b/packages/memory-graph/src/canvas/simulation.ts index f7d0db4d..918ccced 100644 --- a/packages/memory-graph/src/canvas/simulation.ts +++ b/packages/memory-graph/src/canvas/simulation.ts @@ -1,5 +1,6 @@ import * as d3 from "d3-force" import type { GraphEdge, GraphNode } from "../types" +import { FORCE_CONFIG } from "../constants" export class ForceSimulation { private sim: d3.Simulation | null = null @@ -10,40 +11,54 @@ export class ForceSimulation { try { this.sim = d3 .forceSimulation(nodes) - .alphaDecay(0.04) - .alphaMin(0.001) - .velocityDecay(0.6) + .alphaDecay(FORCE_CONFIG.alphaDecay) + .alphaMin(FORCE_CONFIG.alphaMin) + .velocityDecay(FORCE_CONFIG.velocityDecay) this.sim.force( "link", d3 .forceLink(edges) .id((d) => d.id) - .distance((link) => (link.edgeType === "derives" ? 150 : 300)) + .distance((link) => + link.edgeType === "derives" + ? FORCE_CONFIG.docMemoryDistance + : FORCE_CONFIG.linkDistance, + ) .strength((link) => { - if (link.edgeType === "derives") return 0.8 - if (link.edgeType === "updates") return 1.0 - if (link.edgeType === "extends") return 0.1 + if (link.edgeType === "derives") + return FORCE_CONFIG.linkStrength.docMemory + if (link.edgeType === "updates") + return FORCE_CONFIG.linkStrength.version + if (link.edgeType === "extends") + return FORCE_CONFIG.linkStrength.docDocBase return 0.3 }), ) - this.sim.force("charge", d3.forceManyBody().strength(-1000)) + this.sim.force( + "charge", + d3.forceManyBody().strength(FORCE_CONFIG.chargeStrength), + ) this.sim.force( "collide", d3 .forceCollide() - .radius((d) => (d.type === "document" ? 80 : 40)) + .radius((d) => + d.type === "document" + ? FORCE_CONFIG.collisionRadius.document + : FORCE_CONFIG.collisionRadius.memory, + ) .strength(0.7), ) - this.sim.force("x", d3.forceX().strength(0.05)) - this.sim.force("y", d3.forceY().strength(0.05)) + this.sim.force("x", d3.forceX().strength(0.03)) + this.sim.force("y", d3.forceY().strength(0.03)) this.sim.stop() this.sim.alpha(1) - for (let i = 0; i < 100; i++) this.sim.tick() + for (let i = 0; i < FORCE_CONFIG.preSettleTicks; i++) this.sim.tick() this.sim.alphaTarget(0).restart() } catch (e) { console.error("ForceSimulation.init failed:", e) @@ -59,7 +74,7 @@ export class ForceSimulation { } reheat(): void { - this.sim?.alphaTarget(0.3).restart() + this.sim?.alphaTarget(FORCE_CONFIG.alphaTarget).restart() } coolDown(): void { @@ -67,7 +82,7 @@ export class ForceSimulation { } isActive(): boolean { - return (this.sim?.alpha() ?? 0) > 0.001 + return (this.sim?.alpha() ?? 0) > FORCE_CONFIG.alphaMin } destroy(): void { diff --git a/packages/memory-graph/src/constants.ts b/packages/memory-graph/src/constants.ts index 0d45ef3b..1e4d43da 100644 --- a/packages/memory-graph/src/constants.ts +++ b/packages/memory-graph/src/constants.ts @@ -9,19 +9,19 @@ export const MEMORY_BORDER_KEYS = { export const FORCE_CONFIG = { linkStrength: { - docMemory: 0.8, - version: 1.0, - docDocBase: 0.3, + docMemory: 0.6, + version: 0.8, + docDocBase: 0.15, }, - linkDistance: 300, - docMemoryDistance: 150, - chargeStrength: -1000, - collisionRadius: { document: 80, memory: 40 }, + linkDistance: 450, + docMemoryDistance: 200, + chargeStrength: -2500, + collisionRadius: { document: 90, memory: 45 }, alphaDecay: 0.04, alphaMin: 0.001, velocityDecay: 0.6, alphaTarget: 0.3, - preSettleTicks: 100, + preSettleTicks: 120, } export const GRAPH_SETTINGS = { @@ -45,9 +45,9 @@ export const DEFAULT_COLORS: GraphThemeColors = { textPrimary: "#ffffff", textSecondary: "#e2e8f0", textMuted: "#94a3b8", - edgeDerives: "#3B82F6", - edgeUpdates: "#8B5CF6", - edgeExtends: "#14B8A6", + edgeDerives: "#38BDF8", + edgeUpdates: "#A78BFA", + edgeExtends: "#2DD4BF", memBorderForgotten: "#EF4444", memBorderExpiring: "#F59E0B", memBorderRecent: "#10B981",