mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-15 23:31:05 +00:00
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
This commit is contained in:
parent
b43c311092
commit
bcf2ff87fa
4 changed files with 91 additions and 44 deletions
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, typeof normalDots>()
|
||||
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<string, typeof normalDots>()
|
||||
for (const d of normalDots) {
|
||||
|
|
|
|||
|
|
@ -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<GraphNode, GraphEdge> | null = null
|
||||
|
|
@ -10,40 +11,54 @@ export class ForceSimulation {
|
|||
try {
|
||||
this.sim = d3
|
||||
.forceSimulation<GraphNode>(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<GraphNode, GraphEdge>(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<GraphNode>().strength(-1000))
|
||||
this.sim.force(
|
||||
"charge",
|
||||
d3.forceManyBody<GraphNode>().strength(FORCE_CONFIG.chargeStrength),
|
||||
)
|
||||
|
||||
this.sim.force(
|
||||
"collide",
|
||||
d3
|
||||
.forceCollide<GraphNode>()
|
||||
.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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue