Style second brain graph like live cluster map
This commit is contained in:
@@ -166,9 +166,10 @@ body {
|
|||||||
#graphCanvas{
|
#graphCanvas{
|
||||||
display:block;
|
display:block;
|
||||||
margin: 8px auto 0;
|
margin: 8px auto 0;
|
||||||
background:#12121a;
|
background:#02040a;
|
||||||
border:1px solid #252533;
|
border:1px solid #172033;
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 28px rgba(34,211,238,0.08), inset 0 0 42px rgba(124,58,237,0.08);
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +193,15 @@ body {
|
|||||||
border-color:#6c8af5;
|
border-color:#6c8af5;
|
||||||
box-shadow:0 0 0 1px rgba(108,138,245,0.18) inset;
|
box-shadow:0 0 0 1px rgba(108,138,245,0.18) inset;
|
||||||
}
|
}
|
||||||
|
.graph-mode{
|
||||||
|
color:#8ef6e4;
|
||||||
|
font-size:0.78rem;
|
||||||
|
font-weight:700;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border:1px solid #173c42;
|
||||||
|
background:#07151b;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
.graph-legend{
|
.graph-legend{
|
||||||
margin: 8px 12px 0;
|
margin: 8px 12px 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -58,14 +58,8 @@
|
|||||||
<!-- Graph -->
|
<!-- Graph -->
|
||||||
<div class="graph" id="graph" style="display:none;">
|
<div class="graph" id="graph" style="display:none;">
|
||||||
<div class="graph-controls">
|
<div class="graph-controls">
|
||||||
<button class="btn primary" id="btnGraphPhysics" onclick="toggleGraphPhysics()">Physics: off</button>
|
|
||||||
<button class="btn" onclick="resetGraphView()">Reset view</button>
|
<button class="btn" onclick="resetGraphView()">Reset view</button>
|
||||||
<button class="btn" onclick="fitGraphView()">Fit</button>
|
<button class="btn" onclick="fitGraphView()">Fit</button>
|
||||||
<label class="muted small" style="display:flex;align-items:center;gap:8px">
|
|
||||||
<span>Physics</span>
|
|
||||||
<input id="physicsStrength" type="range" min="0" max="100" value="60" oninput="setPhysicsStrength(this.value)" style="width:140px">
|
|
||||||
<span id="physicsStrengthVal">60</span>
|
|
||||||
</label>
|
|
||||||
<select class="btn" id="graphLimit" onchange="reloadGraph()" title="Wie viele Knoten laden? 0=all">
|
<select class="btn" id="graphLimit" onchange="reloadGraph()" title="Wie viele Knoten laden? 0=all">
|
||||||
<option value="0">Nodes: all</option>
|
<option value="0">Nodes: all</option>
|
||||||
<option value="200">Nodes: 200</option>
|
<option value="200">Nodes: 200</option>
|
||||||
@@ -73,6 +67,7 @@
|
|||||||
<option value="5000">Nodes: 5000</option>
|
<option value="5000">Nodes: 5000</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn" onclick="reloadGraph()">Reload</button>
|
<button class="btn" onclick="reloadGraph()">Reload</button>
|
||||||
|
<span class="graph-mode">Live physics on</span>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="graphCanvas" width="440" height="520"></canvas>
|
<canvas id="graphCanvas" width="440" height="520"></canvas>
|
||||||
<div class="graph-hint muted small" id="graphHint">Lade Graph…</div>
|
<div class="graph-hint muted small" id="graphHint">Lade Graph…</div>
|
||||||
@@ -81,7 +76,7 @@
|
|||||||
<div id="graphLiveFeed" class="graph-live-feed"></div>
|
<div id="graphLiveFeed" class="graph-live-feed"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="graph-legend">
|
<div class="graph-legend">
|
||||||
<div><strong>Graph</strong>: Zoom per Pinch (2 Finger), Pan per Drag (1 Finger). Tap auf Engram öffnet Details, Tap auf Tag setzt Suche.</div>
|
<div><strong>Graph</strong>: Live Cluster wie Obsidian. Zoom per Pinch, Pan per Drag. Tap auf Engram öffnet Details, Tap auf Tag setzt Suche.</div>
|
||||||
<div class="legend-row"><span class="legend-dot engram"></span> Engram</div>
|
<div class="legend-row"><span class="legend-dot engram"></span> Engram</div>
|
||||||
<div class="legend-row"><span class="legend-dot tag"></span> Tag</div>
|
<div class="legend-row"><span class="legend-dot tag"></span> Tag</div>
|
||||||
<div class="legend-row"><span class="legend-dot source"></span> Quelle</div>
|
<div class="legend-row"><span class="legend-dot source"></span> Quelle</div>
|
||||||
@@ -177,7 +172,12 @@ function setView(view) {
|
|||||||
document.getElementById('graph').style.display = view === 'graph' ? '' : 'none';
|
document.getElementById('graph').style.display = view === 'graph' ? '' : 'none';
|
||||||
document.getElementById('status').style.display = view === 'status' ? '' : 'none';
|
document.getElementById('status').style.display = view === 'status' ? '' : 'none';
|
||||||
|
|
||||||
if (view === 'graph') loadGraph();
|
if (view === 'graph') {
|
||||||
|
loadGraph();
|
||||||
|
} else if (graphState && graphState.raf) {
|
||||||
|
cancelAnimationFrame(graphState.raf);
|
||||||
|
graphState.raf = null;
|
||||||
|
}
|
||||||
if (view === 'status') loadStatus();
|
if (view === 'status') loadStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ let graphState = {
|
|||||||
nodeById: new Map(),
|
nodeById: new Map(),
|
||||||
simById: new Map(),
|
simById: new Map(),
|
||||||
degree: new Map(),
|
degree: new Map(),
|
||||||
physicsOn: false,
|
physicsOn: true,
|
||||||
draggingId: null,
|
draggingId: null,
|
||||||
selectedId: null,
|
selectedId: null,
|
||||||
panning: false,
|
panning: false,
|
||||||
@@ -414,13 +414,16 @@ let graphState = {
|
|||||||
pinchStartZoom: null,
|
pinchStartZoom: null,
|
||||||
pinchStartPan: null,
|
pinchStartPan: null,
|
||||||
down: null, // {pointerId, cx, cy, t}
|
down: null, // {pointerId, cx, cy, t}
|
||||||
physicsStrength: parseInt(localStorage.getItem('physicsStrength') || '60', 10),
|
|
||||||
edgeKeys: new Set(),
|
edgeKeys: new Set(),
|
||||||
|
sourceIndex: new Map(),
|
||||||
|
sourceCounts: new Map(),
|
||||||
|
nextSourceIndex: 0,
|
||||||
loadingToken: 0,
|
loadingToken: 0,
|
||||||
totalEngrams: 0,
|
totalEngrams: 0,
|
||||||
loadedEngrams: 0,
|
loadedEngrams: 0,
|
||||||
lastModified: null,
|
lastModified: null,
|
||||||
liveFeed: [],
|
liveFeed: [],
|
||||||
|
lastFrameAt: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
function _graphCanvas() { return document.getElementById('graphCanvas'); }
|
function _graphCanvas() { return document.getElementById('graphCanvas'); }
|
||||||
@@ -444,18 +447,17 @@ function _graphResetData() {
|
|||||||
graphState.simById = new Map();
|
graphState.simById = new Map();
|
||||||
graphState.degree = new Map();
|
graphState.degree = new Map();
|
||||||
graphState.edgeKeys = new Set();
|
graphState.edgeKeys = new Set();
|
||||||
|
graphState.sourceIndex = new Map();
|
||||||
|
graphState.sourceCounts = new Map();
|
||||||
|
graphState.nextSourceIndex = 0;
|
||||||
graphState.totalEngrams = 0;
|
graphState.totalEngrams = 0;
|
||||||
graphState.loadedEngrams = 0;
|
graphState.loadedEngrams = 0;
|
||||||
graphState.lastModified = null;
|
graphState.lastModified = null;
|
||||||
graphState.physicsOn = false;
|
graphState.physicsOn = true;
|
||||||
graphState.panX = 0;
|
graphState.panX = 0;
|
||||||
graphState.panY = 0;
|
graphState.panY = 0;
|
||||||
graphState.zoom = 1;
|
graphState.zoom = 1;
|
||||||
const b = document.getElementById('btnGraphPhysics');
|
graphState.lastFrameAt = 0;
|
||||||
if (b) {
|
|
||||||
b.textContent = 'Physics: off';
|
|
||||||
b.classList.remove('primary');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _graphHashUnit(s) {
|
function _graphHashUnit(s) {
|
||||||
@@ -468,33 +470,61 @@ function _graphHashUnit(s) {
|
|||||||
return ((h >>> 0) / 4294967295);
|
return ((h >>> 0) / 4294967295);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _graphPalette(seed) {
|
||||||
|
const palette = [
|
||||||
|
[34, 211, 238], // cyan
|
||||||
|
[244, 114, 182], // pink
|
||||||
|
[168, 85, 247], // purple
|
||||||
|
[74, 222, 128], // green
|
||||||
|
[248, 113, 113], // red
|
||||||
|
[96, 165, 250], // blue
|
||||||
|
[251, 191, 36], // amber
|
||||||
|
[45, 212, 191], // teal
|
||||||
|
];
|
||||||
|
return palette[Math.floor(_graphHashUnit(seed) * palette.length) % palette.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _graphSourceCenter(source) {
|
||||||
|
const key = source || 'unknown';
|
||||||
|
if (!graphState.sourceIndex.has(key)) {
|
||||||
|
graphState.sourceIndex.set(key, graphState.nextSourceIndex++);
|
||||||
|
}
|
||||||
|
const i = graphState.sourceIndex.get(key);
|
||||||
|
const angle = i * 2.399963;
|
||||||
|
const ring = i < 1 ? 0 : 260 + Math.sqrt(i) * 95;
|
||||||
|
return {
|
||||||
|
x: Math.cos(angle) * ring,
|
||||||
|
y: Math.sin(angle) * ring * 0.82,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function _graphPlaceNode(n) {
|
function _graphPlaceNode(n) {
|
||||||
const canvas = _graphCanvas();
|
|
||||||
const idx = graphState.sim.length + 1;
|
|
||||||
if (n.kind === 'source') {
|
if (n.kind === 'source') {
|
||||||
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
return _graphSourceCenter((n.label || n.id || '').replace(/^source:/, ''));
|
||||||
const ring = 70 + (_graphHashUnit(n.id + ':r') * 45);
|
|
||||||
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
|
||||||
}
|
}
|
||||||
if (n.kind === 'tag') {
|
if (n.kind === 'tag') {
|
||||||
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
||||||
const ring = 210 + (_graphHashUnit(n.id + ':r') * 170);
|
const ring = 520 + (_graphHashUnit(n.id + ':r') * 420);
|
||||||
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
||||||
}
|
}
|
||||||
if (n.kind === 'host') {
|
if (n.kind === 'host') {
|
||||||
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
const angle = _graphHashUnit(n.id) * Math.PI * 2;
|
||||||
const ring = 360 + (_graphHashUnit(n.id + ':r') * 160);
|
const ring = 760 + (_graphHashUnit(n.id + ':r') * 260);
|
||||||
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
return {x: Math.cos(angle) * ring, y: Math.sin(angle) * ring};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obsidian-like "brain": a golden-angle cloud with source-aware lobes.
|
// Obsidian-like "brain": each source gets a lobe, entries fill it as a
|
||||||
const sourceSeed = _graphHashUnit(n.source || 'unknown') * Math.PI * 2;
|
// golden-angle cloud so 50k+ nodes appear immediately without edge layout.
|
||||||
const angle = idx * 2.399963 + sourceSeed * 0.45;
|
const source = n.source || 'unknown';
|
||||||
const radius = 120 + Math.sqrt(idx) * 15 + _graphHashUnit(n.id) * 70;
|
const local = (graphState.sourceCounts.get(source) || 0) + 1;
|
||||||
const lobe = 1 + (_graphHashUnit(n.source || 'unknown') - 0.5) * 0.26;
|
graphState.sourceCounts.set(source, local);
|
||||||
|
const center = _graphSourceCenter(source);
|
||||||
|
const angle = local * 2.399963 + _graphHashUnit(n.id) * 0.7;
|
||||||
|
const radius = 18 + Math.sqrt(local) * 9 + _graphHashUnit(n.id + ':r') * 38;
|
||||||
|
const squash = 0.72 + _graphHashUnit(source) * 0.46;
|
||||||
return {
|
return {
|
||||||
x: Math.cos(angle) * radius * lobe,
|
x: center.x + Math.cos(angle) * radius * squash,
|
||||||
y: Math.sin(angle) * radius * (2 - lobe),
|
y: center.y + Math.sin(angle) * radius * (1.55 - squash),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,6 +542,8 @@ function _graphEnsureSimNode(n) {
|
|||||||
last_accessed: n.last_accessed ?? existing.last_accessed,
|
last_accessed: n.last_accessed ?? existing.last_accessed,
|
||||||
source: n.source ?? existing.source,
|
source: n.source ?? existing.source,
|
||||||
predict_locked: n.predict_locked ?? existing.predict_locked,
|
predict_locked: n.predict_locked ?? existing.predict_locked,
|
||||||
|
createdMs: Date.parse(n.created || existing.created || '') || existing.createdMs || 0,
|
||||||
|
modifiedMs: Date.parse(n.modified || existing.modified || '') || existing.modifiedMs || 0,
|
||||||
});
|
});
|
||||||
graphState.nodeById.set(n.id, {...(graphState.nodeById.get(n.id) || {}), ...n});
|
graphState.nodeById.set(n.id, {...(graphState.nodeById.get(n.id) || {}), ...n});
|
||||||
return existing;
|
return existing;
|
||||||
@@ -526,6 +558,8 @@ function _graphEnsureSimNode(n) {
|
|||||||
confidence: n.confidence,
|
confidence: n.confidence,
|
||||||
created: n.created,
|
created: n.created,
|
||||||
modified: n.modified,
|
modified: n.modified,
|
||||||
|
createdMs: Date.parse(n.created || '') || 0,
|
||||||
|
modifiedMs: Date.parse(n.modified || '') || 0,
|
||||||
last_accessed: n.last_accessed,
|
last_accessed: n.last_accessed,
|
||||||
source: n.source,
|
source: n.source,
|
||||||
predict_locked: n.predict_locked,
|
predict_locked: n.predict_locked,
|
||||||
@@ -562,6 +596,9 @@ function _graphMergePayload(payload, opts = {}) {
|
|||||||
const iters = graphState.sim.length < 900 ? 12 : 3;
|
const iters = graphState.sim.length < 900 ? 12 : 3;
|
||||||
for (let i = 0; i < iters; i++) _graphStepPhysics(0.28);
|
for (let i = 0; i < iters; i++) _graphStepPhysics(0.28);
|
||||||
}
|
}
|
||||||
|
if (state.view === 'graph' && graphState.physicsOn && !graphState.raf) {
|
||||||
|
graphState.raf = requestAnimationFrame(_graphLoop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _graphPushLive(text) {
|
function _graphPushLive(text) {
|
||||||
@@ -575,10 +612,11 @@ function _graphPushLive(text) {
|
|||||||
|
|
||||||
function _graphNodeRadius(n) {
|
function _graphNodeRadius(n) {
|
||||||
const d = graphState.degree.get(n.id) || 0;
|
const d = graphState.degree.get(n.id) || 0;
|
||||||
const base = n.kind === 'tag' ? 4 : (n.kind === 'host' ? 5 : (n.kind === 'source' ? 11 : 7));
|
const huge = graphState.sim.length > 20000;
|
||||||
|
const base = n.kind === 'tag' ? (huge ? 3 : 4) : (n.kind === 'host' ? 5 : (n.kind === 'source' ? 13 : (huge ? 1.9 : 5.5)));
|
||||||
const w = (n.weight || 0);
|
const w = (n.weight || 0);
|
||||||
const bonus = Math.min(6, Math.sqrt(Math.max(0, w)) * 0.8);
|
const bonus = Math.min(huge ? 2.5 : 6, Math.sqrt(Math.max(0, w)) * 0.8);
|
||||||
return Math.max(3, Math.min(18, base + Math.sqrt(d) + bonus));
|
return Math.max(huge ? 1.4 : 3, Math.min(huge ? 9 : 18, base + Math.sqrt(d) * (huge ? 0.35 : 1) + bonus));
|
||||||
}
|
}
|
||||||
|
|
||||||
function _graphNodeFill(n) {
|
function _graphNodeFill(n) {
|
||||||
@@ -595,7 +633,25 @@ function _graphNodeFill(n) {
|
|||||||
return `rgb(${r},${g},${b})`;
|
return `rgb(${r},${g},${b})`;
|
||||||
}
|
}
|
||||||
if (n.kind === 'source') {
|
if (n.kind === 'source') {
|
||||||
const [r,g,b] = mix([20, 184, 166]);
|
const [r,g,b] = mix(_graphPalette(n.label || n.id));
|
||||||
|
return `rgb(${r},${g},${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.kind === 'engram') {
|
||||||
|
let base = _graphPalette(n.source || n.id);
|
||||||
|
const verdict = (n.verdict || '').toString();
|
||||||
|
if (verdict === 'confirmed_false') base = [248, 113, 113];
|
||||||
|
else if (verdict === 'confirmed_true') {
|
||||||
|
const src = _graphPalette(n.source || n.id);
|
||||||
|
base = [Math.round((src[0] + 74) / 2), Math.round((src[1] + 222) / 2), Math.round((src[2] + 128) / 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = graphState.drawNow || Date.now();
|
||||||
|
const created = n.createdMs || 0;
|
||||||
|
const ageMin = created ? (now - created) / 60000 : 999999;
|
||||||
|
const rec = Math.max(0, Math.min(0.45, (30 - ageMin) / 30 * 0.45));
|
||||||
|
const bump = (c) => Math.round(c + (255 - c) * rec);
|
||||||
|
const [r,g,b] = mix(base).map(bump);
|
||||||
return `rgb(${r},${g},${b})`;
|
return `rgb(${r},${g},${b})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,14 +846,6 @@ function renderGraph(nodes, edges) {
|
|||||||
const hint = document.getElementById('graphHint');
|
const hint = document.getElementById('graphHint');
|
||||||
const ctx = _graphCtx();
|
const ctx = _graphCtx();
|
||||||
|
|
||||||
// sync physics slider
|
|
||||||
const slider = document.getElementById('physicsStrength');
|
|
||||||
const sliderVal = document.getElementById('physicsStrengthVal');
|
|
||||||
const s = Math.max(0, Math.min(100, parseInt(graphState.physicsStrength || 60, 10)));
|
|
||||||
graphState.physicsStrength = s;
|
|
||||||
if (slider) slider.value = String(s);
|
|
||||||
if (sliderVal) sliderVal.textContent = String(s);
|
|
||||||
|
|
||||||
const w = canvas.parentElement.clientWidth - 24;
|
const w = canvas.parentElement.clientWidth - 24;
|
||||||
canvas.width = Math.max(320, w);
|
canvas.width = Math.max(320, w);
|
||||||
canvas.height = Math.max(520, Math.min(900, (window.innerHeight || 900) - 260));
|
canvas.height = Math.max(520, Math.min(900, (window.innerHeight || 900) - 260));
|
||||||
@@ -966,6 +1014,10 @@ function renderGraph(nodes, edges) {
|
|||||||
|
|
||||||
function _graphStepPhysics(alpha = 1.0) {
|
function _graphStepPhysics(alpha = 1.0) {
|
||||||
const canvas = _graphCanvas();
|
const canvas = _graphCanvas();
|
||||||
|
if (graphState.sim.length > 6000) {
|
||||||
|
_graphStepAmbient(alpha);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const repulsion = (graphState.sim.length > 700) ? 120 : 180;
|
const repulsion = (graphState.sim.length > 700) ? 120 : 180;
|
||||||
const damping = 0.86;
|
const damping = 0.86;
|
||||||
const target = 80;
|
const target = 80;
|
||||||
@@ -1036,6 +1088,36 @@ function _graphStepPhysics(alpha = 1.0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _graphStepAmbient(alpha = 1.0) {
|
||||||
|
const now = Date.now();
|
||||||
|
const t = now / 1000;
|
||||||
|
const sim = graphState.sim;
|
||||||
|
const count = sim.length;
|
||||||
|
if (!count) return;
|
||||||
|
// Keep the full 50k+ graph alive without doing an expensive all-node force
|
||||||
|
// solve: tiny deterministic drift plus stronger movement on fresh/hub nodes.
|
||||||
|
const sample = Math.min(count, 900);
|
||||||
|
const start = Math.floor((t * 97) % count);
|
||||||
|
for (let k = 0; k < sample; k++) {
|
||||||
|
const n = sim[(start + k * 13) % count];
|
||||||
|
const amp = (n.kind === 'engram') ? 0.018 : 0.05;
|
||||||
|
n.x += Math.sin(t * 0.7 + _graphHashUnit(n.id) * 6.283) * amp * alpha;
|
||||||
|
n.y += Math.cos(t * 0.6 + _graphHashUnit(n.id + ':y') * 6.283) * amp * alpha;
|
||||||
|
}
|
||||||
|
const recentCutoff = now - 12 * 60 * 1000;
|
||||||
|
for (const n of sim) {
|
||||||
|
if (n.kind === 'source' || n.kind === 'tag' || (n.modifiedMs || n.createdMs || 0) > recentCutoff) {
|
||||||
|
const home = n.kind === 'engram' ? _graphSourceCenter(n.source || 'unknown') : null;
|
||||||
|
if (home) {
|
||||||
|
n.vx += (home.x - n.x) * 0.00003 * alpha;
|
||||||
|
n.vy += (home.y - n.y) * 0.00003 * alpha;
|
||||||
|
}
|
||||||
|
n.vx *= 0.92; n.vy *= 0.92;
|
||||||
|
n.x += n.vx; n.y += n.vy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _graphEdgeColor(kind) {
|
function _graphEdgeColor(kind) {
|
||||||
const k = (kind || '').toString().toLowerCase();
|
const k = (kind || '').toString().toLowerCase();
|
||||||
if (k.includes('tag')) return '#7c3aed';
|
if (k.includes('tag')) return '#7c3aed';
|
||||||
@@ -1050,16 +1132,27 @@ function _graphDraw() {
|
|||||||
const hint = document.getElementById('graphHint');
|
const hint = document.getElementById('graphHint');
|
||||||
|
|
||||||
ctx.clearRect(0,0,canvas.width,canvas.height);
|
ctx.clearRect(0,0,canvas.width,canvas.height);
|
||||||
|
const bg = ctx.createRadialGradient(canvas.width * 0.52, canvas.height * 0.48, 10, canvas.width * 0.5, canvas.height * 0.5, Math.max(canvas.width, canvas.height) * 0.72);
|
||||||
|
bg.addColorStop(0, '#121a2b');
|
||||||
|
bg.addColorStop(0.55, '#070b16');
|
||||||
|
bg.addColorStop(1, '#02040a');
|
||||||
|
ctx.fillStyle = bg;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
graphState.drawNow = Date.now();
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(graphState.panX, graphState.panY);
|
ctx.translate(graphState.panX, graphState.panY);
|
||||||
ctx.scale(graphState.zoom, graphState.zoom);
|
ctx.scale(graphState.zoom, graphState.zoom);
|
||||||
|
|
||||||
for (const l of graphState.links) {
|
|
||||||
const term = (graphState.search || '').trim();
|
const term = (graphState.search || '').trim();
|
||||||
|
const dense = graphState.sim.length > 12000;
|
||||||
|
const drawEdges = !dense || term || graphState.selectedId;
|
||||||
|
if (drawEdges) for (const l of graphState.links) {
|
||||||
const isMatchEdge = term && (_graphMatches(l.a, term) || _graphMatches(l.b, term));
|
const isMatchEdge = term && (_graphMatches(l.a, term) || _graphMatches(l.b, term));
|
||||||
|
const selectedEdge = graphState.selectedId && (l.a.id === graphState.selectedId || l.b.id === graphState.selectedId);
|
||||||
|
if (dense && !isMatchEdge && !selectedEdge && l.kind !== 'link') continue;
|
||||||
const w = Math.max(0.2, Math.min(3.0, (l.weight || 1.0)));
|
const w = Math.max(0.2, Math.min(3.0, (l.weight || 1.0)));
|
||||||
ctx.lineWidth = (0.6 + w) / graphState.zoom;
|
ctx.lineWidth = (0.6 + w) / graphState.zoom;
|
||||||
ctx.globalAlpha = isMatchEdge ? 0.85 : (0.25 + Math.min(0.35, w * 0.18));
|
ctx.globalAlpha = isMatchEdge || selectedEdge ? 0.9 : (dense ? 0.12 : (0.20 + Math.min(0.35, w * 0.18)));
|
||||||
ctx.strokeStyle = isMatchEdge ? '#f7d154' : _graphEdgeColor(l.kind);
|
ctx.strokeStyle = isMatchEdge ? '#f7d154' : _graphEdgeColor(l.kind);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(l.a.x, l.a.y);
|
ctx.moveTo(l.a.x, l.a.y);
|
||||||
@@ -1068,7 +1161,6 @@ function _graphDraw() {
|
|||||||
}
|
}
|
||||||
ctx.globalAlpha = 1.0;
|
ctx.globalAlpha = 1.0;
|
||||||
|
|
||||||
const term = (graphState.search || '').trim();
|
|
||||||
let matches = 0;
|
let matches = 0;
|
||||||
for (const n of graphState.sim) {
|
for (const n of graphState.sim) {
|
||||||
const r = _graphNodeRadius(n);
|
const r = _graphNodeRadius(n);
|
||||||
@@ -1091,10 +1183,23 @@ function _graphDraw() {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fill = _graphNodeFill(n);
|
||||||
|
const important = n.kind !== 'engram' || isMatch || graphState.selectedId === n.id || ((graphState.drawNow - (n.modifiedMs || n.createdMs || 0)) < 10 * 60 * 1000);
|
||||||
|
if (important) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.shadowColor = fill;
|
||||||
|
ctx.shadowBlur = (n.kind === 'source' ? 14 : 8) / graphState.zoom;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = _graphNodeFill(n);
|
ctx.fillStyle = fill;
|
||||||
ctx.arc(n.x, n.y, r, 0, Math.PI*2);
|
ctx.arc(n.x, n.y, r, 0, Math.PI*2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
} else {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.arc(n.x, n.y, r, 0, Math.PI*2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
// status/recency/lock border
|
// status/recency/lock border
|
||||||
let stroke = null;
|
let stroke = null;
|
||||||
@@ -1103,9 +1208,9 @@ function _graphDraw() {
|
|||||||
else if (v === 'confirmed_false') stroke = '#fecaca';
|
else if (v === 'confirmed_false') stroke = '#fecaca';
|
||||||
else if (v) stroke = '#c7d2fe';
|
else if (v) stroke = '#c7d2fe';
|
||||||
|
|
||||||
const now = Date.now();
|
const now = graphState.drawNow;
|
||||||
const created = Date.parse(n.created || '') || 0;
|
const created = n.createdMs || 0;
|
||||||
const modified = Date.parse(n.modified || '') || 0;
|
const modified = n.modifiedMs || 0;
|
||||||
const isNew = created && (now - created) < (30 * 60 * 1000);
|
const isNew = created && (now - created) < (30 * 60 * 1000);
|
||||||
const isHot = modified && (now - modified) < (10 * 60 * 1000);
|
const isHot = modified && (now - modified) < (10 * 60 * 1000);
|
||||||
if (isNew) stroke = '#f7d154';
|
if (isNew) stroke = '#f7d154';
|
||||||
@@ -1128,34 +1233,23 @@ function _graphDraw() {
|
|||||||
|
|
||||||
function _graphLoop() {
|
function _graphLoop() {
|
||||||
if (!graphState.physicsOn) return;
|
if (!graphState.physicsOn) return;
|
||||||
const speed = 0.25 + (Math.max(0, Math.min(100, graphState.physicsStrength || 60)) / 100) * 0.95;
|
const now = performance.now();
|
||||||
_graphStepPhysics(speed);
|
const fps = graphState.sim.length > 25000 ? 8 : (graphState.sim.length > 8000 ? 14 : 30);
|
||||||
|
if (!graphState.lastFrameAt || now - graphState.lastFrameAt >= (1000 / fps)) {
|
||||||
|
graphState.lastFrameAt = now;
|
||||||
|
_graphStepPhysics(0.75);
|
||||||
_graphDraw();
|
_graphDraw();
|
||||||
|
}
|
||||||
graphState.raf = requestAnimationFrame(_graphLoop);
|
graphState.raf = requestAnimationFrame(_graphLoop);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPhysicsStrength(v) {
|
function setPhysicsStrength(v) {
|
||||||
const n = Math.max(0, Math.min(100, parseInt(v || '0', 10)));
|
// Kept for older cached pages; physics is now always live.
|
||||||
graphState.physicsStrength = n;
|
|
||||||
localStorage.setItem('physicsStrength', String(n));
|
|
||||||
const el = document.getElementById('physicsStrengthVal');
|
|
||||||
if (el) el.textContent = String(n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleGraphPhysics() {
|
function toggleGraphPhysics() {
|
||||||
graphState.physicsOn = !graphState.physicsOn;
|
graphState.physicsOn = true;
|
||||||
const b = document.getElementById('btnGraphPhysics');
|
if (!graphState.raf) graphState.raf = requestAnimationFrame(_graphLoop);
|
||||||
const fast = (graphState.sim || []).length > 700;
|
|
||||||
b.textContent = `Physics: ${graphState.physicsOn ? ('on' + (fast ? ' (fast)' : '')) : 'off'}`;
|
|
||||||
b.classList.toggle('primary', graphState.physicsOn);
|
|
||||||
if (graphState.physicsOn) {
|
|
||||||
if (graphState.raf) cancelAnimationFrame(graphState.raf);
|
|
||||||
graphState.raf = requestAnimationFrame(_graphLoop);
|
|
||||||
} else {
|
|
||||||
if (graphState.raf) cancelAnimationFrame(graphState.raf);
|
|
||||||
graphState.raf = null;
|
|
||||||
_graphDraw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetGraphView() {
|
function resetGraphView() {
|
||||||
|
|||||||
Reference in New Issue
Block a user