Skip to content

Commit ec08e44

Browse files
committed
feat(visualize): 3D nebula showpiece as the default view
Per the steering toward visual impact, make the 3D force graph a living nebula and the default: - gentle auto-rotation once idle (pauses on interaction) so it's alive at rest - soft per-node bloom + faint twinkle + bright core sparks → nebula glow - ambient light 'comets' drifting along random filaments Treemap kept behind the TREEMAP toggle for the informative view.
1 parent fc00323 commit ec08e44

1 file changed

Lines changed: 37 additions & 6 deletions

File tree

openkb/templates/graph.html

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@
270270
let didDrag = false;
271271
let alpha = 1; // simulated-annealing "temperature"; cools to 0 so motion stops
272272
let spread = 1; // spacing knob: >1 spreads nodes apart, <1 packs them tighter
273-
let mode = "treemap"; // "treemap" = document/concept infographic (default), "graph" = 3D force
273+
let mode = "graph"; // "graph" = 3D nebula showpiece (default), "treemap" = infographic
274+
let idleFrames = 0; // frames since last interaction → drives idle auto-rotation
274275

275276
document.getElementById("ncount").textContent = nodes.length;
276277
document.getElementById("ecount").textContent = edges.length;
@@ -478,6 +479,7 @@
478479

479480
canvas.addEventListener("wheel", e => {
480481
e.preventDefault();
482+
idleFrames = 0; // pause auto-rotation while the user is interacting
481483
const f = Math.exp(-e.deltaY * 0.0015);
482484
const ns = Math.max(0.3, Math.min(9, scale*f)); // wider range so you can dive deep in
483485
const k = ns/scale;
@@ -538,6 +540,26 @@
538540
ctx.globalAlpha = 1;
539541
}
540542

543+
// ambient "comets" drifting along random filaments → the nebula feels alive even at rest
544+
const pulses = Array.from({length: Math.min(10, Math.max(0, edges.length))},
545+
() => ({ ei: Math.floor(Math.random()*edges.length), k: Math.random(), sp: 0.003 + Math.random()*0.004 }));
546+
function drawPulses(t){
547+
for(const p of pulses){
548+
let e = edges[p.ei], a = e && byId[e.source], b = e && byId[e.target];
549+
if(!e || a.alpha < 0.06 || b.alpha < 0.06){ p.ei = Math.floor(Math.random()*edges.length); p.k = 0; continue; }
550+
p.k += p.sp;
551+
if(p.k >= 1){ p.k = 0; p.ei = Math.floor(Math.random()*edges.length); continue; }
552+
const fade = Math.sin(p.k*Math.PI); // fade in/out along the filament
553+
const fog = Math.max(0.25, Math.min(1, 1 - (a.rz+b.rz)/1300));
554+
const fx = a.sx + (b.sx-a.sx)*p.k, fy = a.sy + (b.sy-a.sy)*p.k;
555+
ctx.globalAlpha = 0.85 * fade * fog;
556+
ctx.shadowColor = "rgba(120,200,255,.9)"; ctx.shadowBlur = 7;
557+
ctx.fillStyle = "rgba(190,225,255,.95)";
558+
ctx.beginPath(); ctx.arc(fx, fy, 1.7, 0, TAU); ctx.fill();
559+
ctx.shadowBlur = 0; ctx.globalAlpha = 1;
560+
}
561+
}
562+
541563
function drawNode(n, t){
542564
if(n.alpha < 0.02) return;
543565
const col = n.col;
@@ -566,11 +588,17 @@
566588
}
567589
// disc + glow (glow eases with highlight so hovering doesn't flash the whole scene)
568590
ctx.beginPath(); ctx.arc(x, y, r, 0, TAU);
591+
const twinkle = 0.82 + 0.18*Math.sin(t*0.0013 + n.r*3.1); // gentle per-node shimmer
569592
ctx.fillStyle = `rgba(${col},${0.5 + 0.5*hl})`;
570-
ctx.shadowColor = `rgba(${col},.7)`;
571-
ctx.shadowBlur = (n.id===hover ? 9 : 0) * fog * hl; // flat crisp discs at rest; a touch of glow only on hover
593+
ctx.shadowColor = `rgba(${col},.9)`;
594+
ctx.shadowBlur = (n.id===hover ? 18 : 8) * fog * hl * twinkle; // soft bloom → nebula glow
572595
ctx.fill();
573596
ctx.shadowBlur = 0;
597+
// bright core spark for the nearer/brighter nodes
598+
if(hl > 0.5 && fog > 0.5){
599+
ctx.beginPath(); ctx.arc(x - r*0.28, y - r*0.28, r*0.3, 0, TAU);
600+
ctx.fillStyle = `rgba(255,255,255,${0.35*fog})`; ctx.fill();
601+
}
574602
// (flat discs — no glossy core highlight)
575603
// label — declutter: hubs at rest; hovered node + neighbors; selection; zoomed in; near depth only
576604
const showLabel = (scale > 1.4) || (n.id === selected) || (hover ? hl > 0.55 : labelIds.has(n.id));
@@ -589,6 +617,8 @@
589617
function frame(t){
590618
if(mode === "graph"){ // treemap is DOM — the canvas idles
591619
if(alpha > 0.005) step();
620+
idleFrames = (orbiting || drag) ? 0 : idleFrames + 1;
621+
if(idleFrames > 150) yaw += 0.0011; // gentle auto-rotation once idle → a living nebula
592622
project();
593623
const focus = hover || selected; // hover/selection spotlights a node + its neighbours
594624
for(const n of nodes){
@@ -598,6 +628,7 @@
598628
ctx.setTransform(DPR,0,0,DPR,0,0);
599629
ctx.clearRect(0,0,W(),H());
600630
edges.forEach(e => drawEdge(e, t));
631+
drawPulses(t); // ambient light travelling the filaments
601632
// draw nodes far → near so nearer discs occlude farther ones (painter's algorithm)
602633
nodes.filter(visible).sort((a,b) => b.rz - a.rz).forEach(n => drawNode(n, t));
603634
}
@@ -625,7 +656,7 @@
625656
});
626657
canvas.addEventListener("mousedown", e => {
627658
const n = pick(e.offsetX, e.offsetY);
628-
didDrag = false;
659+
didDrag = false; idleFrames = 0;
629660
oStartX = e.offsetX; oStartY = e.offsetY; // press origin — for the drag threshold and orbit
630661
if(n){ drag = n; hover = n.id; canvas.style.cursor = "grabbing"; }
631662
else { orbiting = true; yawStart = yaw; pitchStart = pitch; canvas.style.cursor = "grabbing"; }
@@ -734,8 +765,8 @@
734765

735766
/* ===================== boot ===================== */
736767
window.addEventListener("resize", () => { size(); if(mode === "treemap") buildTreemap(); });
737-
size(); seed(); // seed positions so a later switch to 3D has a layout to settle
738-
setMode("treemap"); // open on the treemap infographic
768+
size(); seed();
769+
setMode("graph"); // open on the 3D nebula
739770
requestAnimationFrame(frame);
740771
</script>
741772
</body>

0 commit comments

Comments
 (0)