granite-mem 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,380 +0,0 @@
1
- /**
2
- * Granite — Force-Directed Graph Visualization
3
- *
4
- * Interactive canvas graph showing note connections.
5
- * Physics: repulsion between all nodes, attraction along edges, gravity to center.
6
- * Interactions: drag, zoom, pan, click-to-navigate, hover highlight.
7
- */
8
-
9
- const GraphEngine = (() => {
10
-
11
- // ── Type colors ──
12
- const TYPE_COLORS = {
13
- fleeting: { fill: '#fbbf24', glow: 'rgba(251, 191, 36, 0.3)' },
14
- permanent: { fill: '#a5b4fc', glow: 'rgba(165, 180, 252, 0.3)' },
15
- reference: { fill: '#34d399', glow: 'rgba(52, 211, 153, 0.3)' },
16
- person: { fill: '#f9a8d4', glow: 'rgba(249, 168, 212, 0.3)' },
17
- meeting: { fill: '#93c5fd', glow: 'rgba(147, 197, 253, 0.3)' },
18
- project: { fill: '#c4b5fd', glow: 'rgba(196, 181, 253, 0.3)' },
19
- decision: { fill: '#fca5a5', glow: 'rgba(252, 165, 165, 0.3)' },
20
- _default: { fill: '#6e6e7a', glow: 'rgba(110, 110, 122, 0.3)' },
21
- };
22
-
23
- const BG_COLOR = '#08080a';
24
- const EDGE_COLOR = 'rgba(70, 70, 85, 0.35)';
25
- const EDGE_HIGHLIGHT = 'rgba(165, 180, 252, 0.6)';
26
- const LABEL_COLOR = '#b0b0b8';
27
- const LABEL_DIM = 'rgba(110, 110, 122, 0.3)';
28
- const LABEL_FONT = '11px "Instrument Sans", sans-serif';
29
- const NODE_ACTIVE_RING = '#6366f1';
30
-
31
- // ── State ──
32
- let canvas, ctx;
33
- let nodes = [], edges = [];
34
- let width, height;
35
- let transform = { x: 0, y: 0, scale: 1 };
36
- let animFrame = null;
37
- let hoveredNode = null;
38
- let dragNode = null;
39
- let isPanning = false;
40
- let panStart = { x: 0, y: 0 };
41
- let activeSlug = null;
42
- let onNavigate = null;
43
- let isRunning = false;
44
- let simulationAlpha = 1;
45
-
46
- // ── Physics constants ──
47
- const REPULSION = 800;
48
- const ATTRACTION = 0.008;
49
- const GRAVITY = 0.02;
50
- const DAMPING = 0.88;
51
- const MIN_ALPHA = 0.001;
52
- const VELOCITY_LIMIT = 8;
53
-
54
- function getTypeColor(type) {
55
- return TYPE_COLORS[type] || TYPE_COLORS._default;
56
- }
57
-
58
- function init(canvasEl, graphData, options = {}) {
59
- canvas = canvasEl;
60
- ctx = canvas.getContext('2d');
61
- onNavigate = options.onNavigate || null;
62
- activeSlug = options.activeSlug || null;
63
-
64
- // Build nodes
65
- const connectionCount = {};
66
- for (const e of graphData.edges) {
67
- connectionCount[e.source] = (connectionCount[e.source] || 0) + 1;
68
- connectionCount[e.target] = (connectionCount[e.target] || 0) + 1;
69
- }
70
-
71
- nodes = graphData.nodes.map((n, i) => {
72
- const connections = connectionCount[n.slug] || 0;
73
- const angle = (i / graphData.nodes.length) * Math.PI * 2;
74
- const spread = Math.min(300, graphData.nodes.length * 15);
75
- return {
76
- ...n,
77
- x: Math.cos(angle) * spread * (0.5 + Math.random() * 0.5),
78
- y: Math.sin(angle) * spread * (0.5 + Math.random() * 0.5),
79
- vx: 0, vy: 0,
80
- radius: Math.max(4, Math.min(14, 4 + connections * 1.5)),
81
- connections,
82
- };
83
- });
84
-
85
- // Build edge references
86
- const nodeMap = {};
87
- nodes.forEach(n => nodeMap[n.slug] = n);
88
- edges = graphData.edges
89
- .filter(e => nodeMap[e.source] && nodeMap[e.target])
90
- .map(e => ({ source: nodeMap[e.source], target: nodeMap[e.target] }));
91
-
92
- // Reset
93
- transform = { x: 0, y: 0, scale: 1 };
94
- simulationAlpha = 1;
95
-
96
- resize();
97
- setupEvents();
98
- start();
99
- }
100
-
101
- function resize() {
102
- const dpr = window.devicePixelRatio || 1;
103
- const container = canvas.parentElement;
104
- width = container.clientWidth;
105
- height = container.clientHeight;
106
- canvas.width = width * dpr;
107
- canvas.height = height * dpr;
108
- canvas.style.width = width + 'px';
109
- canvas.style.height = height + 'px';
110
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
111
- }
112
-
113
- // ── Physics simulation ──
114
- function simulate() {
115
- if (simulationAlpha < MIN_ALPHA) return;
116
-
117
- // Repulsion (all pairs)
118
- for (let i = 0; i < nodes.length; i++) {
119
- for (let j = i + 1; j < nodes.length; j++) {
120
- const a = nodes[i], b = nodes[j];
121
- let dx = b.x - a.x;
122
- let dy = b.y - a.y;
123
- let dist = Math.sqrt(dx * dx + dy * dy) || 1;
124
- const force = REPULSION / (dist * dist) * simulationAlpha;
125
- const fx = (dx / dist) * force;
126
- const fy = (dy / dist) * force;
127
- a.vx -= fx; a.vy -= fy;
128
- b.vx += fx; b.vy += fy;
129
- }
130
- }
131
-
132
- // Attraction (edges)
133
- for (const edge of edges) {
134
- const dx = edge.target.x - edge.source.x;
135
- const dy = edge.target.y - edge.source.y;
136
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
137
- const force = dist * ATTRACTION * simulationAlpha;
138
- const fx = (dx / dist) * force;
139
- const fy = (dy / dist) * force;
140
- edge.source.vx += fx; edge.source.vy += fy;
141
- edge.target.vx -= fx; edge.target.vy -= fy;
142
- }
143
-
144
- // Gravity toward center
145
- for (const node of nodes) {
146
- node.vx -= node.x * GRAVITY * simulationAlpha;
147
- node.vy -= node.y * GRAVITY * simulationAlpha;
148
- }
149
-
150
- // Integrate
151
- for (const node of nodes) {
152
- if (node === dragNode) continue;
153
- node.vx *= DAMPING;
154
- node.vy *= DAMPING;
155
- // Clamp velocity
156
- const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy);
157
- if (speed > VELOCITY_LIMIT) {
158
- node.vx = (node.vx / speed) * VELOCITY_LIMIT;
159
- node.vy = (node.vy / speed) * VELOCITY_LIMIT;
160
- }
161
- node.x += node.vx;
162
- node.y += node.vy;
163
- }
164
-
165
- simulationAlpha *= 0.995;
166
- }
167
-
168
- // ── Render ──
169
- function draw() {
170
- ctx.clearRect(0, 0, width, height);
171
- ctx.fillStyle = BG_COLOR;
172
- ctx.fillRect(0, 0, width, height);
173
-
174
- ctx.save();
175
- ctx.translate(width / 2 + transform.x, height / 2 + transform.y);
176
- ctx.scale(transform.scale, transform.scale);
177
-
178
- const hoveredNeighbors = new Set();
179
- if (hoveredNode) {
180
- for (const e of edges) {
181
- if (e.source === hoveredNode) hoveredNeighbors.add(e.target);
182
- if (e.target === hoveredNode) hoveredNeighbors.add(e.source);
183
- }
184
- hoveredNeighbors.add(hoveredNode);
185
- }
186
-
187
- const dimming = hoveredNode !== null;
188
-
189
- // Draw edges
190
- for (const edge of edges) {
191
- const isHighlighted = hoveredNode && (edge.source === hoveredNode || edge.target === hoveredNode);
192
- ctx.strokeStyle = isHighlighted ? EDGE_HIGHLIGHT : (dimming ? 'rgba(50, 50, 60, 0.15)' : EDGE_COLOR);
193
- ctx.lineWidth = isHighlighted ? 1.5 : 0.75;
194
- ctx.beginPath();
195
- ctx.moveTo(edge.source.x, edge.source.y);
196
- ctx.lineTo(edge.target.x, edge.target.y);
197
- ctx.stroke();
198
- }
199
-
200
- // Draw nodes
201
- for (const node of nodes) {
202
- const colors = getTypeColor(node.type);
203
- const isNeighbor = hoveredNeighbors.has(node);
204
- const isDimmed = dimming && !isNeighbor;
205
- const isActive = node.slug === activeSlug;
206
-
207
- // Glow for hovered/active
208
- if ((node === hoveredNode || isActive) && !isDimmed) {
209
- ctx.fillStyle = colors.glow;
210
- ctx.beginPath();
211
- ctx.arc(node.x, node.y, node.radius + 6, 0, Math.PI * 2);
212
- ctx.fill();
213
- }
214
-
215
- // Active ring
216
- if (isActive) {
217
- ctx.strokeStyle = NODE_ACTIVE_RING;
218
- ctx.lineWidth = 2;
219
- ctx.beginPath();
220
- ctx.arc(node.x, node.y, node.radius + 3, 0, Math.PI * 2);
221
- ctx.stroke();
222
- }
223
-
224
- // Node circle
225
- ctx.globalAlpha = isDimmed ? 0.15 : 1;
226
- ctx.fillStyle = colors.fill;
227
- ctx.beginPath();
228
- ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
229
- ctx.fill();
230
-
231
- // Label
232
- ctx.font = LABEL_FONT;
233
- ctx.fillStyle = isDimmed ? LABEL_DIM : LABEL_COLOR;
234
- ctx.textAlign = 'center';
235
- ctx.textBaseline = 'top';
236
- const label = node.title.length > 24 ? node.title.slice(0, 22) + '…' : node.title;
237
- ctx.fillText(label, node.x, node.y + node.radius + 5);
238
- ctx.globalAlpha = 1;
239
- }
240
-
241
- ctx.restore();
242
- }
243
-
244
- function tick() {
245
- simulate();
246
- draw();
247
- if (isRunning) animFrame = requestAnimationFrame(tick);
248
- }
249
-
250
- function start() {
251
- if (isRunning) return;
252
- isRunning = true;
253
- tick();
254
- }
255
-
256
- function stop() {
257
- isRunning = false;
258
- if (animFrame) cancelAnimationFrame(animFrame);
259
- }
260
-
261
- // ── Coordinate transforms ──
262
- function screenToWorld(sx, sy) {
263
- return {
264
- x: (sx - width / 2 - transform.x) / transform.scale,
265
- y: (sy - height / 2 - transform.y) / transform.scale,
266
- };
267
- }
268
-
269
- function findNodeAt(sx, sy) {
270
- const { x, y } = screenToWorld(sx, sy);
271
- for (let i = nodes.length - 1; i >= 0; i--) {
272
- const n = nodes[i];
273
- const dx = n.x - x, dy = n.y - y;
274
- if (dx * dx + dy * dy < (n.radius + 4) * (n.radius + 4)) return n;
275
- }
276
- return null;
277
- }
278
-
279
- // ── Events ──
280
- function setupEvents() {
281
- canvas.addEventListener('mousedown', (e) => {
282
- const rect = canvas.getBoundingClientRect();
283
- const sx = e.clientX - rect.left, sy = e.clientY - rect.top;
284
- const node = findNodeAt(sx, sy);
285
- if (node) {
286
- dragNode = node;
287
- dragNode.vx = 0;
288
- dragNode.vy = 0;
289
- simulationAlpha = Math.max(simulationAlpha, 0.3);
290
- } else {
291
- isPanning = true;
292
- panStart = { x: e.clientX - transform.x, y: e.clientY - transform.y };
293
- }
294
- });
295
-
296
- canvas.addEventListener('mousemove', (e) => {
297
- const rect = canvas.getBoundingClientRect();
298
- const sx = e.clientX - rect.left, sy = e.clientY - rect.top;
299
-
300
- if (dragNode) {
301
- const { x, y } = screenToWorld(sx, sy);
302
- dragNode.x = x;
303
- dragNode.y = y;
304
- simulationAlpha = Math.max(simulationAlpha, 0.1);
305
- } else if (isPanning) {
306
- transform.x = e.clientX - panStart.x;
307
- transform.y = e.clientY - panStart.y;
308
- } else {
309
- const node = findNodeAt(sx, sy);
310
- if (node !== hoveredNode) {
311
- hoveredNode = node;
312
- canvas.style.cursor = node ? 'pointer' : 'grab';
313
- // Show tooltip
314
- const tooltip = document.getElementById('graph-tooltip');
315
- if (node) {
316
- tooltip.textContent = node.title;
317
- tooltip.style.left = (e.clientX + 12) + 'px';
318
- tooltip.style.top = (e.clientY - 8) + 'px';
319
- tooltip.style.opacity = '1';
320
- } else {
321
- tooltip.style.opacity = '0';
322
- }
323
- } else if (node) {
324
- const tooltip = document.getElementById('graph-tooltip');
325
- tooltip.style.left = (e.clientX + 12) + 'px';
326
- tooltip.style.top = (e.clientY - 8) + 'px';
327
- }
328
- }
329
- });
330
-
331
- canvas.addEventListener('mouseup', (e) => {
332
- if (dragNode) {
333
- // If barely moved, treat as click
334
- const rect = canvas.getBoundingClientRect();
335
- const node = findNodeAt(e.clientX - rect.left, e.clientY - rect.top);
336
- if (node && node === dragNode && onNavigate) {
337
- onNavigate(node.slug);
338
- }
339
- dragNode = null;
340
- }
341
- isPanning = false;
342
- });
343
-
344
- canvas.addEventListener('mouseleave', () => {
345
- hoveredNode = null;
346
- isPanning = false;
347
- dragNode = null;
348
- document.getElementById('graph-tooltip').style.opacity = '0';
349
- });
350
-
351
- canvas.addEventListener('wheel', (e) => {
352
- e.preventDefault();
353
- const scaleFactor = e.deltaY > 0 ? 0.92 : 1.08;
354
- const newScale = Math.max(0.1, Math.min(5, transform.scale * scaleFactor));
355
-
356
- // Zoom toward cursor
357
- const rect = canvas.getBoundingClientRect();
358
- const mx = e.clientX - rect.left - width / 2;
359
- const my = e.clientY - rect.top - height / 2;
360
- transform.x = mx - (mx - transform.x) * (newScale / transform.scale);
361
- transform.y = my - (my - transform.y) * (newScale / transform.scale);
362
- transform.scale = newScale;
363
- }, { passive: false });
364
-
365
- // Resize observer
366
- const ro = new ResizeObserver(() => { resize(); });
367
- ro.observe(canvas.parentElement);
368
- }
369
-
370
- function setActiveSlug(slug) {
371
- activeSlug = slug;
372
- }
373
-
374
- function reheat() {
375
- simulationAlpha = 0.5;
376
- if (!isRunning) start();
377
- }
378
-
379
- return { init, start, stop, resize, setActiveSlug, reheat, draw };
380
- })();